1. React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>

React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。
JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。

实现合成事件的目的如下:
● 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
● 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

2. React的事件和普通的HTML事件有什么不同?

区别:
● 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
● 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
● react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
● 兼容所有浏览器,更好的跨平台;
● 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
● 方便 react 统一管理和事务机制。

事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。

3. React 组件中怎么做事件代理?它的原理是什么?

React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在React底层,主要对合成事件做了两件事:
● 事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
● 自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

如何⽤webpack来优化前端性能?

⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css

  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

如何提⾼webpack的打包速度?

  • happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项⽬,类似的替代者是thread-loader
  • 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐如jQuery⽤script标签引⼊
  • dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
  • 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼rebuild效率缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname,'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼build了node_modules⽂件

如何提⾼webpack的构建速度?

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

vue3.0 Composition API 中 setup 使用

介绍

setup 简单来说就像 react 一样,你的数据定义不用放到 data, 方法不用放到 methods, 只需要一个 setup 全部搞定,不用像有些复杂的项目,接口一堆,写一会忘了上面定义的变量参数,可以一块一块来写,结合jsx 更加方便,写起来很舒服

使用

import { defineComponent, ref, reactive } from "vue";

export const VisualEditor = defineComponent({
    props: {},
    setup (props) {
                const readersNumber = ref(0)
                const book = reactive({ title: 'Vue 3 Guide' })
                const readerHtml = () => {
            <span class="text">
               hello world
            </span>
        }
        return {
                    readersNumber,
                    book,
                    readerHtml
                }
    }
})

jSON.stringify()

JSON.stringify() 方法能将一个 JavaScript 对象或值转换成一个 JSON 字符串。

第二个参数(数组)

JSOn.stringify 有第二个参数,不知道大家有没有用过,那么第二个参数作用是什么了?
例如要在控制台中打印对象的键数组。我们有一个对象 product 并且我们想知道 product 的 name 属性值。

    console.log(JSON.stringify(product));

它会输出下面的结果。

{"id":"01", name: "天启", code: "1234"}

如果数据太多在日志中很难找到 name 键,因为控制台上显示了很多没用的信息。当对象变大时,查找属性的难度增加。stringify 函数的第二个参数这时就有用了。让我们重写代码并查看结果。

    console.log(JSON.stringify(obj, ["name"]))
    // 结果
    // "{"name":"天启"}"

问题解决了,与打印整个 JSON 对象不同,我们可以在第二个参数中将所需的键作为数组传递,从而只打印所需的属性

第二个参数(函数)

我们还可以传入函数作为第二个参数。它根据函数中写入的逻辑来计算每个键值对。如果返回 undefined,则不会打印键值对。请参考示例以获得更好的理解。

const obj = {
    "name": "天启",
    "age": 999
}

JSON.stringify(obj,function (key, value) {
    if (typeof value === "string") {
        return undefined;
    }
    return value;
})

// 结果
// {"age": 26}

只有 age 被打印出来,因为函数判断 typeOf 为 String 的值返回 undefined。

第三个参数(数字)

第三个参数控制最后一个字符串的间距。如果参数是一个数字,则字符串化中的每个级别都将缩进这个数量的空格字符。

// 注意:为了达到理解的目的,使用 '--' 替代了空格
JSON.stringify(obj, null, 2)
//{
//--"name": "天启",
//--"age": 26,
//--"country": "India"
//}

第三个参数(字符串)

如果第三个参数是 string,那么将使用它来代替上面显示的空格字符。

JSON.stringify(obj, null,'**');
//{
//**"name": "天启",
//**"age": 26,
//**"country": "India"
//}
// 这里 * 取代了空格字符

toJSON 方法

我们有一个叫 toJSON 的方法,它可以作为任意对象的属性。JSON.stringify 返回这个函数的结果并对其进行序列化,而不是将整个对象转换为字符串。参考下面的例子。

const obj = {
    firstName : "天启",
    lastName : "坦克",
    age: 999,
    toJSON : function () {
        return {
            fullName: `${this.firstName} + ${this.lastName}`
        }
    }
}

console.log(JSON.stringify(obj));
// 结果
// "{"fullName": "天启坦克"}"

这里我们可以看到,它只打印 toJSON 函数的结果,而不是打印整个对象。

vue监听属性 watch

vue 监听属性 watch 有三种用法

watch 用法

普通用法

<input type="text" v-model="userName"/>
new Vue({
  el: '#box',
  data: {
    userName: '中国'
  },
  watch: {
    userName(newName, oldName) {
      // ...
    }
  }
})

vue 监听处理函数,当每次监听到 userName 值发生改变时,执行函数。

 watch: {
   userName(newName, oldName) {
     // ...
   }
 }

deep 用法

当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,此时就需要deep属性对对象进行深度监听。

<input type="text" v-model="userName"/>
new Vue({
  el: '#box',
  data: {
    userName: {
            name: '张三',
            age: 18
        }
  },
  watch: {
    userName: {
                handler(newName, oldName) {
                    // ...
                },
                deep: true,
                immediate: true
            }
        }
  }
})

设置deep: true 则可以监听到 userName.name 的变化,此时会给 userName 的所有属性都加上这个监听器,当对象属性较多时,每个属性值的变化都会执行handler。如果只需要监听对象中的一个属性值,则可以做以下优化:使用字符串的形式监听对象属性:

watch: {
    'userName.name': {
      handler(newName, oldName) {
      // ...
      },
      deep: true,
      immediate: true
    }
  }

immediate 属性

这样使用watch时有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,则就需要用到immediate属性。
比如当父组件向子组件动态传值时,子组件props首次获取到父组件传来的默认值时,也需要执行函数,此时就需要将immediate设为true。

new Vue({
  el: '#root',
  data: {
    cityName: ''
  },
  watch: {
    cityName: {
      handler(newName, oldName) {
        // ...
      },
      immediate: true
    }
  }
})

监听的数据后面写成对象形式,包含handler方法和immediate,之前我们写的函数其实就是在写这个handler方法;

immediate表示在watch中首次绑定的时候,是否执行handler,值为true则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据发生变化的时候才执行handler。

JavaScript 运行机制:Event Loop(转)

一、为什么 JavaScript 是单线程?

JavaScript 是单线程运行的,也就是说同一时间只能干一件事。那么为什么 JavaScript 不能是多线程呢?这样可以高效运行啊!

首先 JavaScript 单线程和它的用的有关,作为浏览器脚本语言,他的用途是和用户互动以及操作 DOM 。这决定了它只能是单线程运行,否则会带来很复杂的同步问题。比如,JavaScript 有个俩个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这是浏览器应该以哪个线程为主?

所以避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这问语言的核心特征,将来也不会改变。

为了利用多核 CPU 的计算能力,HTML5 提出一个 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM ,所以这个新的标准并没有改变 JavaScript 单线程的本质。

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行下一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。
img
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。


三、事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
img
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。

    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。


五、定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者@BusyRich)。

img

根据上图,Node.js的运行机制如下。

(1)V8 引擎解析 JavaScript 脚本。

(2)解析后的代码,调用 Node API。

(3)libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8 引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

现在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!

process.nextTick(function foo() {
  process.nextTick(foo);
});

事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。

另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

js 垃圾回收机制

内存的生命周期

内存分配 -> 使用内存 -> 释放内存

js 环境中分配内存有如下声明周期:
  1. 内存分配:在我们申明变量,函数,对象的时候,系统会为我们自动分配内存。
  2. 内存使用:读写内存,也就是使用变量,函数等。
  3. 释放内存:使用完毕,由垃圾回收机制自动回收不在使用的内存。

js 内存的分配

为了让程序员不在费劲的分配内存,JavaScript 在定义变量的时候就完成了内存分配。
    var n = 123; // 给数值变量分配内存
    var s = "azerty"; // 给字符串分配内存

    var o = {
        a: 1,
        b: null
    }; // 给对象及其包含的值分配内存

    // 给数组及其包含的值分配内存(就像对象一样)
    var a = [1, null, "abra"]; 

    function f(a){
        return a + 2;
    } // 给函数(可调用的对象)分配内存

    // 函数表达式也能分配一个对象
    someElement.addEventListener('click', function(){
        someElement.style.backgroundColor = 'blue';
    }, false);
有些函数调用结果是分配函数的内存:
    var date = new Date(); // 分配一个 Date 对象

    var ele = document.createElement("div"); // 分配一个 DOM 对象
有些方法分配新的变量或者新对象:
    var s = "asdfsfd"
    var s2 = s.substr(0,3) // s2 是一个新的字符串
    // 因为字符串是不变量
    // JavaScript 可能决定不分配内存
    // 只是存储了[0-3] 的范围

    var a = ['abc', 'cbd'];
    var a2 = ['box', 'abs'];
    var a3 = a.concat(a2);
    // 新数组有四个元素, 是 a 连接 a2 的结果

js 内存的使用

使用值的过程实际上是对分配内存进行读取和写入操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
    var a = 10; // 分配内存
    console.log(a); // 对内存的读取使用

js 的内存回收

js 有自动垃圾回收机制, 那么这个垃圾自动回收机制的原理是什么呢? 其实很简单,就是找出那些不再继续使用的值,然后释放其内存。
多少内存管理的问题都在这个阶段。在这里最艰的任务是找到不再需要使用的变量。
不再需要使用的变量也就是在生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。
全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。
因为自动垃圾回收机制的存在,开发人员可以不关系也不注意内存释放的有关问题,但是无用内存释放这件事是客观存在的。不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况。

垃圾回收

引用

垃圾回收算法主要依赖于引用的概念
在内存管理感觉中,一个对象如果有访问另一个对象的权限(显示或者隐式),叫做一个对象引用另一个对象。
例如,一个javascript 对象具有对他原型的引用(隐式引用)和对它属性的引用(显示引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)

引用计数垃圾收集

这是最初的垃圾回收算法。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明对象已经不再需要了。
    var o = {
        a: {
            b: 2
        }
    };
    // 两个对象被创建, 一个作为另一个的属性被引用, 另一个被分配给变量。
    // 很显然,没有一个可以被垃圾收集

    var o2 = o; // o2 变量是第二个对“对象”的引用

    o = 1; // 现在, “这个对象” 的原始引用 o 被 o2 替换了

    var oa = o2.a; // 引用“这个对象”的 a 属性
    // 现在,“这个对象”有两个引用了, 一个是 o2, 一个是 oa

    o2 = "ya"; // 最初的对象现在已经是零引用了
                         // 它可以被垃圾回收了
                         // 然后它的属性 a 的对象还在被 oa 引用, 所以还不能回收
    oa = null; // a 属性的那个喜爱那个现在也是零引用了
                         // 它可以被垃圾回收了
由上面可以看出,引用计算算法是个简单有效的算法,但它却存在一个致命的问题: 循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
来看一个循环引用的例子:
    function f() {
        var o = {};
        var o2 = {};
        o.a = a2; // o 引用 o2
        o2.a = o; // o2 引用 o 这里

        return "azerty";
    }

    f();
上面我们申明了一个函数 f,其中包含两个相互引用的对象。在调用函数结束后,对象 o1 和 o2 实际已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。
再来看一个实际的例子:
    var div = document.createElement("div");
    div.onclick = function() {
            console.log("click");
    };
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。 此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。 一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。
为了解决循环引用造成的问题,现代浏览器通过使用标记清除算法来实现垃圾回收。

标记清除算法

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。 但反之未必成立。

工作流程

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

图片

循环引用不再是问题了

在看之前循环引用的例子:
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

函数调用返回之后,两个循环引用的对象在垃圾收集时从全局对象出发无法再获取他们的引用。 因此,他们将会被垃圾回收器回收。

内存泄露泄漏

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。
否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

本质上讲,内存泄漏就是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。

内存泄漏的识别方法

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。 这就要求实时查看内存的占用情况。

在 Chrome 浏览器中,我们可以这样查看内存占用情况

  1. 打开开发者工具,选择 Performance 面板
  2. 在顶部勾选 Memory
  3. 点击左上角的 record 按钮
  4. 在页面上进行各种操作,模拟用户的使用情况
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
来看一张效果图:

我们有两种方式来判定当前是否有内存泄漏:

  1. 多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏
  2. 某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏

在服务器环境中使用 Node 提供的 process.memoryUsage 方法查看内存情况

console.log(process.memoryUsage());
// { 
//     rss: 27709440,
//     heapTotal: 5685248,
//     heapUsed: 3449392,
//     external: 8772 
// }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。
该对象包含四个字段,单位是字节,含义如下:

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

意外的全局变量
function foo() {
    bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
    this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();

在这个例子中,意外的创建了两个全局变量 bar1 和 bar2

被遗忘的定时器和回调函数

在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数。 要记得回收这些回调函数。举一个 setInterval的例子:
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

闭包

在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露:
    var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 对于 'originalThing'的引用
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。
同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。
当这段代码被反复执行时,内存会持续增长。

DOM 引用

很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。

var elements = {
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    document.body.removeChild(document.getElementById('image'));
    // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}

上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。
举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。
但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。
这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。

如何避免内存泄漏

记住一个原则:不用的东西,及时归还。
  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

参考

MDN-内存管理
JavaScript高级程序设计
JavaScript权威指南
JavaScript 内存泄漏教程
一种有趣的JavaScript内存泄漏
内存泄露

  1. 适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>files-h5</title>
</head>
<body>
  <input type="file" id="file" onchange="showPreview(this, 'portrait')" />
  <img src="" id="portrait" style="width: 200px; height: 200px; display: block;" />
  <script>
  function showPreview(source, imgId) {
    var file = source.files[0];
    if(window.FileReader) {
      var fr = new FileReader();
      fr.onloadend = function(e) {
        document.getElementById(imgId).src = e.target.result;
      }
      fr.readAsDataURL(file);
    }
  }
  </script>
</body>
</html>
  1. 更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>files-pc</title>
</head>
<body>
  <input type="file" id="file" onchange="showPreview(this.id,'portrait')" />
  <img src="" id="portrait" style="width: 200px; height: 200px; display: block;" />
  <script type="text/javascript">
  /* 图片预览 */
  function showPreview(fileId, imgId) {
    var file = document.getElementById(fileId);
    var ua = navigator.userAgent.toLowerCase();
    var url = '';
    if(/msie/.test(ua)) {
      url = file.value;
    } else {
      url = window.URL.createObjectURL(file.files[0]);
    }
    document.getElementById(imgId).src = url;
  }
  </script>
</body>
</html>

最近遇到一个头疼的问题,跨域!!!

跨域9种解决方案

什么是跨域

说起跨域,就要知道什么是浏览器同源策略

浏览器同源策略:必须是 协议、域名、端口完全一致的 才符合同源策略

如果以上三项,有一项不同都涉及到跨域问题


为什么浏览器要设置同源策略呢?

没有同源策略限制的两大危险场景

浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。

没有同源策略限制的接口请求

有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
这就是传说中的CSRF攻击浅谈CSRF攻击方式
看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookieCookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookieWeb安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

没有同源策略限制的Dom查询

1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

跨域解决方案

1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域

跨域9种方式

1. jsonp :最常见的 jsonp 方法,利用浏览器请求静态资源

node 代码

const express = require("express");
const app = express();

app.get("/push", function (req, res) {
    console.log(req.query);
    res.send(`${req.query.callback}(${JSON.stringify({ data: req.query, status: 200 })})`);
});

app.listen(3001, () => {
    console.log("jsonp 跨域");
}); 

简单版前端

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

简单封装一下前端这个套路

function jsonp(obj) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        let dataString = obj.url.indexOf('?') == -1 ? '?' : '&';
        for (let i in obj.data) {
            dataString += i + "=" + obj.data[i] + "&";
        }

        const jsonp = 'json_cd' + (Math.random().toString().replace('.', ''));
        script.src = obj.url + dataString + 'callback=' + jsonp;
        document.body.appendChild(script);
        window[jsonp] = (data) => {
            document.body.removeChild(script);
            resolve(data);
        }
    })
}

jsonp({
    url: 'http://localhost:3001/push',
    data: {
        name: 'dz',
        age: '26'
    }
}).then((data) => {
    console.log(data);
})

jsonp 缺点是只能发送 get 请求


2. document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景

实现原理: 两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

a. 父窗口: (http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

b. 子窗口: http://child.domain.com/b.html

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

3. location.hash + iframe 跨域

实现原理: a 与 b 跨域相互同行,通过中间页面 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现: A域:a.html -> B域:b.html -> A域: c.html。a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信, 但 c 与 a 同域,所以 c 可以通过 parent.parent 访问 a 页面所有的对象。

a.html:  (http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);

    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

b.html: (http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

c.html: (http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

4. window.name + iframe 跨域

window.name 属性的独特之处: name 值在不同页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

a.html: (http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

proxy.html: (http://www.domain1.com/proxy.html)

中间代理页,与 a.html 同域,内容为空即可。

b.html: (http;//www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结: 通过 iframe 的 src 属性由外域向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙的绕过了浏览器的跨域限制,但同时它又是安全操作。

5. postMessage 跨域

postMessage 是HTML5 XMLHttpRequest Level 2 中的 API,而且是为数不多可以跨域操作的 window 属性之一, 它可用于解决以下方面的问题;

a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的 iframe 消息传递
d.) 上面三个场景的跨域数据传递

用法: postmessage(data, origin) 方法接受俩个参数
data : html5 规范支持任意基本类型或可复制的兑现,但部分浏览器只支持字符串,所以传参时最好用 JSON.string() 序列化。
origin: 协议 + 主机 + 端口号, 也可以设置为 "*",  表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

6.跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

1.  前端设置

1.)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

3.)vue框架

a.) axios设置:

axios.defaults.withCredentials = true

b.) vue-resource设置:

Vue.http.options.credentials = true
2、 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1.)Java后台:

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2.)Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

7. nginx代理跨域

nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}
2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:
#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

8. Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。

1.)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域)

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

9. WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.)前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2.)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

一、 简介

1. websocket 推送技术在 2008 诞生,2011 成为标准,现在所有浏览器都支持了。

2. 最大特点 是真正的双向平等对话,属于服务器推送技术的一种

扩展:服务器推送技术 

1. webpush 

Internet工程任务组的Webpush提议是一种使用HTTP版本2的简单协议,用于提供可以及时传递(或“推送”)的实时事件,例如传入呼叫或消息。该协议将所有实时事件合并到一个会话中,以确保更有效地使用网络和无线电资源。单个服务整合所有事件,在应用程序到达时将这些事件分发给应用程序。这只需要一个会话,避免重复的开销成本。

2. HTTP 服务器推送

HTTP服务器推送(也称为HTTP流)是一种用于将未经请求的(异步)数据从Web服务器发送到Web浏览器的机制。HTTP服务器推送可以通过几种机制中的任何一种来实现。

作为HTML5的一部分,WebSocket API允许Web服务器和客户端通过全双工 TCP连接进行通信。

3. pushlet 

在这种技术中,服务器利用持久的HTTP连接,使响应永久“打开”(即,服务器永远不会终止响应),有效地欺骗浏览器在初始页面加载被考虑之后保持“加载”模式完成。然后,服务器定期发送JavaScript片段以更新页面内容,从而实现推送功能。通过使用这种技术,客户端不需要Java applet或其他插件来保持与服务器的开放连接; 客户端会自动通知服务器推送的新事件。[11] [12]然而,这种方法的一个严重缺点是服务器缺乏对浏览器超时的控制; 如果浏览器端发生超时,则始终需要页面刷新。

4. 长轮询

5. Flash XMLSockt 中继

Cbox和其他聊天应用程序使用的这种技术在单像素Adobe Flash电影中使用了XMLSocket对象。在控制的JavaScript,客户端建立一个TCP连接到一个单向的服务器上的继电器。中继服务器不从该套接字读取任何内容; 相反,它立即向客户端发送唯一标识符。接下来,客户端发出HTTP请求到web服务器,包括它的这个标识符。然后,Web应用程序可以将发往客户端的消息推送到中继服务器的本地接口,中继服务器通过Flash套接字对它们进行中继。这种方法的优点在于它欣赏许多Web应用程序(包括聊天)的典型自然读写不对称性,因此它提供了高效率。因为它不接受即将离任的插座数据,中继服务器不需要轮询传出TCP连接,在所有的,从而能够容纳的并发连接打开数万人。在此模型中,规模限制是底层服务器操作系统的TCP堆栈。

6. 可靠的集团数据交付(RGDD)

在云计算等服务中,为了提高数据的可靠性和可用性,通常会将其推送(复制)到多台计算机上。例如,Hadoop分布式文件系统(HDFS)为存储的任何对象生成2个额外副本。RGDD专注于有效地将对象从一个位置转换为多个位置,同时通过在网络上的任何链接上发送对象的最小数量的副本(在最佳情况下只有一个)来节省带宽。例如,Datacast [15]是一种向数据中心内的许多节点传递的方案,它依赖于常规和结构化拓扑,DCCast [16]是一种跨数据中心交付的类似方法。

7. 推送通知

推送通知是从后端服务器或应用程序“推送”到用户界面的消息,例如(但不限于)移动应用程序和桌面应用程序。Apple于2009年首次推出推送通知。[17] 2010年,Google发布了自己的服务,即Google Cloud to Device Messaging。(此后它已被Google云消息传递Firebase云消息传递所取代。)[18] 2015年11月,微软宣布推出Windows通知服务将扩展为使用通用Windows平台架构,允许使用通用API调用和POST请求将推送数据发送到Windows 10,Windows 10移动版,Xbox以及其他支持的平台。[19]

特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

ws://example.com:80/some/path


二、客户端示例

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};    


三、客户端 API

4.1 构造函数

var ws = new WebSocket('ws://localhost:8080');

4.2 webSocket.readyState

readyState 属性返回实例对象的当前状态,共有四种

CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

示例

switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

4.3 webSocket.onopen

实例对象的 onopen 属性,用于指定连接成功后的回调函数

ws.onopen = function () {
  ws.send('Hello Server!');
}

如果指定多个回调函数,可以使用 addEventListnenr 方法

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});

4.4 webSocket.onclose 

实例对象的 onclose 属性,用于指定连接关闭后的回调函数

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

4.5 webSocket.onmessage

 实例对象的 onmessage 属性,用于指定收到服务器数据后的回调函数

ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob 对象或 Arraybuffer 对象)

ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

出来动态判断收到的数据类型,也可以使用 binaryType 属性,显式指定收到的二进制数据类

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

型。

// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

4.6 webSocket.sed()

实例对象的 send() 方法用于向服务器发送数据

发送文本的例子

ws.send('your message');

发送 Blob 对象的例子

var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

4.7 webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

4.8 webSocket.onerror 

实例对象的onerror属性,用于指定报错时的回调函数。

socket.onerror = function(event) {
  // handle error event
};

socket.addEventListener("error", function(event) {
  // handle error event
});

1. oncontextmenu="window.event.returnValue=false" 将彻底屏蔽鼠标右键 

    < table border oncontextmenu=return(false)>< td>no< /table> 可用于 Table 

2. < body onselectstart="return false"> 取消选取、防止复制 
 
3. onpaste="return false" 不准粘贴 
 
4. oncopy="return false;" oncut="return false;" 防止复制 
 
5. < link rel="Shortcut Icon" href="favicon.ico"> IE 地址栏前换成自己的图标 
 
6. < link rel="Bookmark" href="favicon.ico"> 可以在收藏夹中显示出你的图标 
 
7. < input style="ime-mode:disabled"> 关闭输入法 
 
8. 永远都会带着框架 < script language="JavaScript">< !-- if (window == top)top.location.href = "frames.htm"; //frames.htm 为框架网页 // -->< /script> 
 
9. 防止被人 frame < SCRIPT LANGUAGE=JAVASCRIPT>< !--  if (top.location != self.location)top.location=self.location; // -->< /SCRIPT> 
 
10. 网页将不能被另存为 

<noscript>< iframe src=*.html>< /iframe></noscript>  

11.  查看网页源代码

<input type=button value=查看网页源代码 onclick="window.location='view-source:' + window.location.href">

12.删除时确认 

 <a href="javascript:if(confirm('确实要删除吗?'))location='boos.asp?&areyou=删除&page=1'">删除</a>

13. 取得控件的绝对位置  // Javascript

    function getIE(e) {
        var t = e.offsetTop; var l = e.offsetLeft; while (e = e.offsetParent) {
            t += e.offsetTop; l += e.offsetLeft;  } alert("top=" + t + "/nleft=" + l);
    } 

//VBScript < script language="VBScript">< !-- function getIE() dim t,l,a,b set a=document.all.img1 t=document.all.img1.offsetTop l=document.all.img1.offsetLeft while a.tagName< >"BODY" set a = a.offsetParent t=t+a.offsetTop l=l+a.offsetLeft wend msgbox "top="&t&chr(13)&"left="&l,64,"得到控件的位置" end function -->< /script> 
 

14. 光标是停在文本框文字的最后

    <input type=text name=text1 value=”123″ onfocus="cc()">
    <script language=”javascript”>
        function cc() {
            var e = event.srcElement;
            var r = e.createTextRange();
            r.moveStart("character", e.value.length);
            r.collapse(true);
            r.select();
        }
    </script>

15. 判断上一页的来源 

 document.referrer 

16. 最小化、最大化、关闭窗口 < object id=hh1 classid="clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11">  < param name="Command" value="Minimize">< /object> < object id=hh2 classid="clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11">  < param name="Command" value="Maximize">< /object> < OBJECT id=hh3 classid="clsid:adb880a6-d8ff-11cf-9377-00aa003b7a11"> 
< PARAM NAME="Command" VALUE="Close">< /OBJECT> < input type=button value=最小化 onclick=hh1.Click()> < input type=button value=最大化 onclick=hh2.Click()> < input type=button value=关闭 onclick=hh3.Click()> 本例适用于 IE 
 

17.屏蔽功能键 

 function look() {
      if (event.shiftKey) alert("禁止按 Shift 键!"); //可以换成 ALT CTRL 
 }
document.onkeydown = look;

 

18. 网页不会被缓存 

<META HTTP-EQUIV="pragma" CONTENT="no-cache"> <META HTTP-EQUIV="Cache-Control" CONTENT="no-cache, must-revalidate"> <META HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT"> 或者<META HTTP-EQUIV="expires" CONTENT="0"> 

19.怎样让表单没有凹凸感? 

<input type=text style="border:1 solid #000000">  
或 
<input type=text style="border-left:none; border-right:none; border-top:none; border-bottom: 1px solid #000000">< /textarea> 

 
20.< div>< span>&< layer>的区别?  < div>(division)用来定义大段的页面元素,会产生转行  < span>用来定义同一行内的元素,跟< div>的唯一区别是不产生转行  < layer>是 ns 的标记,ie 不支持,相当于< div> 
 

21.让弹出窗口总是在最上面: 

<body onblur="this.focus();"> 

22.不要滚动条?

//  让竖条没有:  
<body style="overflow:scroll;overflow-y:hidden">  </body> 
// 让横条没有: 
 <body style="overflow:scroll;overflow-x:hidden">  </body>  
// 两个都去掉?更简单了  
<body scroll="no">  </body>  

23.怎样去掉图片链接点击后,图片周围的虚线?

 <a href="#" onFocus="this.blur()"><img src="logo.jpg" border=0></a> 

24.电子邮件处理提交表单 

<form name="form1" method="post" action="mailto:****@***.com" enctype="text/plain">  
   <input type=submit> 
</form> 

 
25.在打开的子窗口刷新父窗口的代码里如何写? window.opener.location.reload()  
 
26.如何设定打开页面的大小 < body onload="top.resizeTo(300,200);"> 打开页面的位置< body onload="top.moveBy(300,200);"> 
 
27.在页面中如何加入不是满铺的背景图片,拉动页面时背景图不动  < STYLE>  body  {background-image:url(logo.gif); background-repeat:no-repeat;  background-position:center;background-attachment: fixed}  < /STYLE>  
 
28. 检查一段字符串是否全由数字组成 < script language="Javascript">< !-- function checkNum(str){return str.match(//D/)==null} alert(checkNum("1232142141")) alert(checkNum("123214214a1")) // -->< /script> 
 
29. 获得一个窗口的大小 document.body.clientWidth; document.body.clientHeight 
 
30. 怎么判断是否是字符 if (/[^/x00-/xff]/g.test(s)) alert("含有汉字"); else alert("全是字符"); 
 
31.TEXTAREA 自适应文字行数的多少 < textarea rows=1 name=s1 cols=27 onpropertychange="this.style.posHeight=this.scrollHeight"> < /textarea> 
 
32. 日期减去天数等于第二个日期 
< script language=Javascript> function cc(dd,dadd) { //可以加上错误处理 var a = new Date(dd) a = a.valueOf() a = a - dadd * 24 * 60 * 60 * 1000 a = new Date(a) alert(a.getFullYear() + "年" + (a.getMonth() + 1) + "月" + a.getDate() + "日") } cc("12/23/2002",2) < /script> 
 
33. 选择了哪一个 Radio < HTML>< script language="vbscript"> function checkme() for each ob in radio1 if ob.checked then window.alert ob.value next end function < /script>< BODY> < INPUT name="radio1" type="radio" value="style" checked>Style < INPUT name="radio1" type="radio" value="barcode">Barcode < INPUT type="button" value="check" onclick="checkme()"> < /BODY>< /HTML> 
 
34.脚本永不出错 < SCRIPT LANGUAGE="JavaScript">  < !-- Hide  function killErrors() {  return true;  }  window.onerror = killErrors;  // -->  < /SCRIPT> 
 
35.ENTER 键可以让光标移到下一个输入框 <input onkeydown="if(event.keyCode==13)event.keyCode=9"> 
 
36. 检测某个网站的链接速度: 把如下代码加入< body>区域中: < script language=Javascript> tim=1 setInterval("tim++",100) 
b=1 var autourl=new Array() autourl[1]="www.njcatv.net" autourl[2]="javacool.3322.net" autourl[3]="www.sina.com.cn" autourl[4]="www.nuaa.edu.cn" autourl[5]="www.cctv.com" function butt(){ document.write("< form name=autof>") for(var i=1;i< autourl.length;i++) document.write("< input type=text name=txt"+i+" size=10 value=测试中……> =》< input type=text  name=url"+i+" size=40> =》< input type=button value=GO  
onclick=window.open(this.form.url"+i+".value)> ") document.write("< input type=submit value=刷新>< /form>") } butt() function auto(url){ document.forms[0]["url"+b].value=url if(tim>200) {document.forms[0]["txt"+b].value="链接超时"} else {document.forms[0]["txt"+b].value="时间"+tim/10+"秒"} b++ } function run(){for(var i=1;i< autourl.length;i++)document.write("< img src=http://"+autourl+"/"+Math.random()+"  
 
width=1 height=1  
 
onerror=auto("http://"+autourl+"")>")} run()< /script> 
 
37. 各种样式的光标 auto :标准光标 default :标准箭头 hand :手形光标 wait :等待光标 text :I 形光标 vertical-text :水平 I 形光标 no-drop :不可拖动光标 not-allowed :无效光标 
help :?帮助光标 all-scroll :三角方向标 move :移动标 crosshair :十字标 e-resize n-resize nw-resize w-resize s-resize se-resize sw-resize 
 
38.页面进入和退出的特效 进入页面< meta http-equiv="Page-Enter" content="revealTrans(duration=x, transition=y)"> 推出页面< meta http-equiv="Page-Exit" content="revealTrans(duration=x, transition=y)">   这个是页面被载入和调出时的一些特效。duration 表示特效的持续时间,以秒为单位。 transition 表示使用哪种特效,取值为 
 
1-23:   0 矩形缩小    1 矩形扩大    2 圆形缩小   3 圆形扩大    4 下到上刷新    5 上到下刷新   6 左到右刷新    7 右到左刷新    8 竖百叶窗   9 横百叶窗    10 错位横百叶窗    11 错位竖百叶窗   12 点扩散    13 左右到中间刷新    14 中间到左右刷新   15 中间到上下   16 上下到中间    17 右下到左上   18 右上到左下    19 左上到右下    20 左下到右上   21 横条    22 竖条    23 以上22 种随机选择一种 
 
39.在规定时间内跳转 < META http-equiv=V="REFRESH" content="5;URL=http://www.51js.com">  
 
40.网页是否被检索 < meta name="ROBOTS" content="属性值">   其中属性值有以下一些:   属性值为"all": 文件将被检索,且页上链接可被查询;   属性值为"none": 文件不被检索,而且不查询页上的链接;   属性值为"index": 文件将被检索;   属性值为"follow": 查询页上的链接;   属性值为"noindex": 文件不检索,但可被查询链接;   属性值为"nofollow": 文件不被检索,但可查询页上的链接。 
 
41.回车 用客户端脚本在页面添加document的onkeydown事件,让页面在接受到回车事件后,进行Tab 键的功能,即只要把 event 的 keyCode 由 13 变为 9 Javascript 代码如下: 
 
<script language="javascript" for="document" event="onkeydown"> <!--   if(event.keyCode==13)      event.keyCode=9; --> </script> 这样的处理方式,可以实现焦点往下移动,但对于按钮也起同样的作用,一般的客户在输入完 资料以后,跳到按钮后,最好能直接按"回车"进行数据的提交.因此,对上面的方法要进行一下 修改,应该对于"提交"按钮不进行焦点转移.而直接激活提交. 因此我对上面的代码进行了一个修改,即判断事件的"源",是否为提交按钮,代码如下: <script language="javascript" for="document" event="onkeydown"> <!--   if(event.keyCode==13 && event.srcElement.type!='button' && event.srcElement.type!='submit' && event.srcElement.type!='reset' && event.srcElement.type!='textarea' && event.srcElement.type!='')      event.keyCode=9; --> </script> 判断是否为 button, 是因为在 HTML 上会有 type="button" 判断是否为 submit,是因为 HTML 上会有 type="submit" 判断是否为reset,是因为 HTML 上的"重置"应该要被执行 判断是否为空,是因为对于 HTML 上的"<a>链接"也应该被执行,这种情况发生的情况不多,可 以使用"tabindex=-1"的方式来取消链接获得焦点. 

组件传值

1. 父向子组件传值,和传递方法

父组件 app-home 

子组件 app-header

// --- app-home.html 页面

<app-header [msg]="msg" [run]='run' [getDataFromChild]="getDataFromChild"></app-header>


// --- app-home.ts 

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './app-home.html',
  styleUrls: ['./app-home.css']
})
export class HomeComponent implements OnInit {

  public msg="我是home组件的msg";

  constructor() { }

  ngOnInit() {
  }

  run(){

    alert('这是home组件的run方法');
  }

  getDataFromChild(childData){  /*父组件*/
    alert(childData+"1111");
  }

}


// --- app.header.ts 页面

import { Component, OnInit,Input} from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './app-header.html',
  styleUrls: ['./app-header.css']
})
export class FooterComponent implements OnInit {

  @Input()  msg;


  @Input()  run;  /*接收父组件传过来的run方法*/

  @Input()  getDataFromChild; 


  public msginfo='这是子组件的数据';

  constructor() { }

  ngOnInit() {
  }

  sendParent(){  /*子组件自己的方法*/
      this.getDataFromChild(this.msginfo);  /*子组件调用父组件的方法*/

  }

}

1.2 子组件用广播方式调用父组件方法

// --- app-home.html

<app-footer [msg]="msg" [run]='run' (toparent)="getDataFromChild"></app-footer>

// --- app-home.ts 

import { Component, OnInit } from '@angular/core';
import {Http,Jsonp} from "@angular/http";
@Component({
  selector: 'app-news',
  templateUrl: './app-home.html',
  styleUrls: ['./app-home.css']
})
export class NewsComponent implements OnInit {
   public msg="msg";
   public name="张三";

   public list=[];
  constructor(private http:Http,private jsonp:Jsonp) { }

  ngOnInit() {
  }

  getDataFromChild(childData){  /*父组件*/
    alert(childData+"1111");
  }
}

//--- app-header.ts

import { Component, OnInit,Input,Output,EventEmitter } from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './app-header.html',
  styleUrls: ['./app-header.css']
})
export class HeaderComponent implements OnInit {

   @Input()  msg:string;  /*通过 Input  接收父组件传过来的msg*/

   @Input()  name:string; 


   //EventEmitter实现子组件给父组件传值

   @Output() toparent=new EventEmitter();

  constructor() { }
  ngOnInit() {}

  //requestData

  requestData(){

    //调用父组件的方法请求数据

    this.toparent.emit('这是子组件的值');
  }

}

2. 父组件获取子组件值

// app-home.html

<app-header #header></app-header>

// app-home.ts

//ViewChild
import { Component, OnInit, ViewChild } from "@angular/core";

@Component({
  selector: "app-home",
  templateUrl: "./app-home.html",
  styleUrls: ["./app-home.css"]
})
export class ProductComponent implements OnInit {
  @ViewChild("header") header; /*定义子组件 注意括号里面的东西和 #header 对应起来  */

  constructor() {}

  ngOnInit() {}

  getChildData() {
    // this.header.run();   /*执行子组件的方法*/

    alert(this.header.msg); /*获取子组件的数据*/

    //  alert(this.header.name);

    this.header.name = "我是父组件改变后的cart name";
  }
}

// app-header.ts

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-header",
  templateUrl: "./app-header.html",
  styleUrls: ["./app-header.css"]
})
export class CartComponent implements OnInit {
  constructor() {}

  public msg = "我是子组件的数据";

  public name = "子组件";

  ngOnInit() {}

  run() {
    alert("这是子组件的方法11");
  }
}

路由之间传参

在网上找到一个不错的写的,分享给大家:

一般我们页面跳转传递参数都是这样的格式:

http://angular.io/api?uid=1&username=moon

但是在SPA单页应用中却是下面的结果居多【初级视频都是这样敷衍的】

http://angular.io/api/1/moon

一、 地址栏传参

实现从product页面跳转到product-detail页面

1:在app-routing.module.ts中配置路由。

const routes: Routes = [
{
    path: 'product',
    component: ProductComponent,
 },
 {
    path: 'product-detail',
    component: ProductDetailComponent,
  }
];

2:在app-routing.module.ts中配置路由。

constructor(
    private router: Router,   //这里需要注入Router模块
){}
 
jumpHandle(){
    //这是在html中绑定的click跳转事件
    this.router.navigate(['product-detail'], {
        queryParams: {
            productId: '1',
            title: 'moon'
        }
    });
}


3:在product-detail.ts中获取传递过来的参数productId、title

constructor(
    private activatedRoute: ActivatedRoute,   //这里需要注入ActivatedRoute模块
) {
    activatedRoute.queryParams.subscribe(queryParams => {
        let productId = queryParams.productId;
        let title = queryParams.title;
    });
}


二、 在查询中传递参数

 



//传递数据
<a [routerLink]="['/stock']" [queryParams]="{id: 1}">股票详情</a>
// http://localhost:4200/stock?id=1



// 接受参数
import { ActivatedRoute } from '@amgular/router';

export class StockComponent implements OnInit {

    private stockId: number;    
    
    constructor(private routeInfo: ActivatedRoute)
    
    ngOnInit() {
        this.stockId = this.routeInfo.snapshot.queryParams['id'];
    }
    
}

三、 在路径中传递参数

//修改配置
const routes: Routes = [
  {path: '', redirectTo: '/index', pathMatch: 'full'},
  {path: 'index', component: IndexComponent},
  {path: 'stock/:id', component: StocksComponent },
  {path: '**', component: ErrorPageComponent }
];


//传递数据
...
<a [routerLink]="['/stock', 1]">股票详情</a>
// http://localhost:4200/stock/1



//接受参数
...
import { ActivatedRoute } from '@amgular/router';

export class StockComponent implements OnInit {

    private stockId: number;    
    
    constructor(private activedRoute: ActivatedRoute)
    
    ngOnInit() {
        this.stockId = this.activedRoute.snapshot.params['id'];
    }
    
}

使用snapshot快照的方式传递数据,因为初始化一次,路由到自身不能传递参数,需要使用订阅模式。

this.activedRoute.params.subscribe((params: Params) => this.stockId = params['id']);

 

四、在路由中配置

//路由配置配置
const routes: Routes = [
  {path: '', redirectTo: '/index', pathMatch: 'full'},
  {path: 'index', component: IndexComponent, data: {title: 'Index Page'}},
  {path: 'stock/:id', component: StocksComponent, data: {title: 'Stock Page'}},
  {path: '**', component: ErrorPageComponent, data: {title: 'Stock Page'}}
];

//接受参数
this.title = this.routeInfo.snapshot.date[0]['title'];

angular6

1. 简单介绍下项目开始,官方文档上都有详细介绍

1. 然后全局安装 angularCLI

npm install -g @angular/cli

2. 用脚手架创建项目

ng new my-app 

这样就生成一个项目

3. 进入项目目录运行服务并打开

cd my-app
ng serve --open

注:项目目录官方文档很详细我就不介绍了

2. 创建组件

ng generate component heroes

会在 app 文件下创建 一个 hreoes 组件并将其引入到 app.moduls.ts 根文件

hreoes 组件文件夹:文件夹目录下会有四个文件,css 样式文件,html 文件,ts 文件,剩下一个文件没有用.

// 用 ng generate component 创建组件过多时,app.mosuls.ts 会出现乱码或引入错误代码跑偏,需要自己整理!!!

@Component 是个装饰器函数,用于为该组件指定 Angular 所需的元数据。

CLI 自动生成了三个元数据属性:

  1. selector— 组件的选择器(CSS 元素选择器)

  2. templateUrl— 组件模板文件的位置。

  3. styleUrls— 组件私有 CSS 样式表文件的位置。

还有很多扩展属性:
    1. encapsulation: 

      2. changeDetection: 实例化组件时,Angular会创建一个更改检测器

      3.  viewProviders定义其视图DOM子项可见的可注入对象集。

      4. moduleId ,

      5. templateUrl 与 template :不用多说指向 html ,templateUrl 指向 html 路径, template 直接来写 html

      6. styleUrls 与 styles :同上

      7. animations :动画,很厉害的东西,官方介绍通过类似动画的DSL在组件上定义动画。这种描述动画的DSL方法允许灵活性,既有利于开发人员,也有利于框架。动画通过侦听模板中元素上发生的状态更改来工作。当发生状态变化时,Angular可以利用并在其间设置动画。这与CSS转换的工作方式类似,但是,通过编程DSL,动画不仅限于特定于DOM的环境。(Angular也可以在幕后执行优化,使动画效果更高。)要使动画可供使用,动画状态更改将放置在动画触发器中,动画触发器位于动画注释元数据内。在触发器内,可以放置状态和转换条目。

      8. interpolation:覆盖默认的封装开始和结束分隔符 (respectively {{ and }})

      9. entryComponents:定义在定义此组件时应编译的组件。对于此处列出的每个组件,Angular将创建一个ComponentFactory并将其存储在ComponentFactoryResolver中。

       10. preserveWhitespaces: boolean 

CSS 元素选择器 app-heroes 用来在父组件的模板中匹配 HTML 元素的名称,以识别出该组件。

ngOnInit 是一个生命周期钩子,Angular 在创建完组件后很快就会调用 ngOnInit。这里是放置初始化逻辑的好地方。

始终要 export 这个组件类,以便在其它地方(比如 AppModule)导入它。

3. 创建服务

ng generate service hero

使用 Angular CLI 创建一个名叫 hero 的服务。

@Injectable() 服务


注意,这个新的服务导入了 Angular 的 Injectable 符号,并且给这个服务类添加了 @Injectable() 装饰器。 它把这个类标记为依赖注入系统的参与者之一。HeroService 类将会提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖。 目前它还没有依赖,但是很快就会有了

@Injectable() 装饰器会接受该服务的元数据对象,就像 @Component() 对组件类的作用一样。

问题汇总:

    1. 侧滑返回如何禁止?

    2. 侧滑出现上一页面 (页面 js 未执行)

    3. 侧滑出现侧拉,页面空白或页面未刷新,(js 未执行)

个人感觉是框架问题,也是机制问题,ios 很容易出现这样的问题,页面禁用侧滑事件无用,出现上一页面,上一页面也检测不到,显示为空白页面

侧拉拉出侧拉的页面,页面内没有检测到任何事件,内部 js 也没有执行!

可以试试 ios 边缘拖拽关闭禁止


ios的边缘拖拽关闭可以通过下列代码来禁止:

plus.webview.currentWebview().setStyle({
                        'popGesture': 'none'
                    });

在软件中,性能一直扮演着重要的角色。在Web应用中,性能变得更加重要,因为如果页面速度很慢的话,用户就会很容易转去访问我们的竞争对手的网站。作为专业的web开发人员,我们必须要考虑这个问题。有很多“古老”的关于性能优化的最佳实践在今天依然可行,例如最小化请求数目,使用CDN以及不编写阻塞页面渲染的代码。然而,随着越来越多的web应用都在使用JavaScript,确保我们的代码运行的很快就变得很重要。

假设你有一个正在工作的函数,但是你怀疑它运行得没有期望的那样快,并且你有一个改善它性能的计划。那怎么去证明这个假设呢?在今天,有什么最佳实践可以用来测试JavaScript函数的性能呢?一般来说,完成这个任务的最佳方式是使用内置的performance.now()函数,来衡量函数运行前和运行后的时间。

在这篇文章中,我们会讨论如何衡量代码运行时间,以及有哪些技术可以避免一些常见的“陷阱”。

Performance.now()

高分辨率时间API提供了一个名为now()的函数,它返回一个DOMHighResTimeStamp对象,这是一个浮点数值,以毫秒级别(精确到千分之一毫秒)显示当前时间。单独这个数值并不会为你的分析带来多少价值,但是两个这样的数值的差值,就可以精确描述过去了多少时间。

这个函数除了比内置的Date对象更加精确以外,它还是“单调”的,简单说,这意味着它不会受操作系统(例如,你笔记本上的操作系统)周期性修改系统时间影响。更简单的说,定义两个Date实例,计算它们的差值,并不代表过去了多少时间。

“单调性”的数学定义是“(一个函数或者数值)以从不减少或者从不增加的方式改变”。

我们可以从另外一种途径来解释它,即想象使用它来在一年中让时钟向前或者向后改变。例如,当你所在国家的时钟都同意略过一个小时,以便最大化利用白天的时间。如果你在时钟修改之前创建了一个Date实例,然后在修改之后创建了另外一个,那么查看这两个实例的差值,看上去可能像“1小时零3秒又123毫秒”。而使用两个performance.now()实例,差值会是“3秒又123毫秒456789之一毫秒”。

在这一节中,我不会涉及这个API的过多细节。如果你想学习更多相关知识或查看更多如何使用它的示例,我建议你阅读这篇文章:Discovering the High Resolution Time API。

既然你知道高分辨率时间API是什么以及如何使用它,那么让我们继续深入看一下它有哪些潜在的缺点。但是在此之前,我们定义一个名为makeHash()的函数,在这篇文章剩余的部分,我们会使用它。

functionmakeHash(source){

varhash=;

if(source.length===)returnhash;

for(vari=;i

varchar=source.charCodeAt(i);

hash=((hash

hash=hash&hash;// Convert to 32bit integer

}

returnhash;

}

我们可以通过下面的代码来衡量这个函数的执行效率:

vart0=performance.now();

varresult=makeHash('Peter');

vart1=performance.now();

console.log('Took',(t1-t0).toFixed(4),'milliseconds to generate:',result);

如果你在浏览器中运行这些代码,你应该看到类似下面的输出:

Took 0.2730 milliseconds to generate: 77005292

这段代码的在线演示如下所示:

记住这个示例后,让我们开始下面的讨论。

缺陷1 – 意外衡量不重要的事情

在上面的示例中,你可以注意到,我们在两次调用performance.now()中间只调用了makeHash()函数,然后将它的值赋给result变量。这给我们提供了函数的执行时间,而没有其他的干扰。我们也可以按照下面的方式来衡量代码的效率:

vart0=performance.now();

console.log(makeHash('Peter'));// bad idea!

vart1=performance.now();

console.log('Took',(t1-t0).toFixed(4),'milliseconds');

这个代码片段的在线演示如下所示:

但是在这种情况下,我们将会测量调用makeHash(‘Peter’)函数花费的时间,以及将结果发送并打印到控制台上花费的时间。我们不知道这两个操作中每个操作具体花费多少时间, 只知道总的时间。而且,发送和打印输出的操作所花费的时间会依赖于所用的浏览器,甚至依赖于当时的上下文。

或许你已经完美的意识到console.log方式是不可以预测的。但是执行多个函数同样是错误的,即使每个函数都不会触发I/O操作。例如:

vart0=performance.now();

varname='Peter';

varresult=makeHash(name.toLowerCase()).toString();

vart1=performance.now();

console.log('Took',(t1-t0).toFixed(4),'milliseconds to generate:',result);

同样,我们不会知道执行时间是怎么分布的。它会是赋值操作、调用toLowerCase()函数或者toString()函数吗?

缺陷 #2 – 只衡量一次

另外一个常见的错误是只衡量一次,然后汇总花费的时间,并以此得出结论。很可能执行不同的次数会得出完全不同的结果。执行时间依赖于很多因素:

编辑器热身的时间(例如,将代码编译成字节码的时间)

主线程可能正忙于其它一些我们没有意识到的事情

你的电脑的CPU可能正忙于一些会拖慢浏览器速度的事情

持续改进的方法是重复执行函数,就像这样:

vart0=performance.now();

for(vari=;i

makeHash('Peter');

}

vart1=performance.now();

console.log('Took',((t1-t0)/10).toFixed(4),'milliseconds to generate');

这个示例的在线演示如下所示:

这种方法的风险在于我们的浏览器的JavaScript引擎可能会使用一些优化措施,这意味着当我们第二次调用函数时,如果输入时相同的,那么JavaScript引擎可能会记住了第一次调用的输出,然后简单的返回这个输出。为了解决这个问题,你可以使用很多不同的输入字符串,而不用重复的使用相同的输入(例如‘Peter’)。显然,使用不同的输入进行测试带来的问题就是我们衡量的函数会花费不同的时间。或许其中一些输入会花费比其它输入更长的执行时间。

缺陷 #3 – 太依赖平均值

在上一节中,我们学习到的一个很好的实践是重复执行一些操作,理想情况下使用不同的输入。然而,我们要记住使用不同的输入带来的问题,即某些输入的执行时间可能会花费所有其它输入的执行时间都长。这样让我们退一步来使用相同的输入。假设我们发送同样的输入十次,每次都打印花费了多长时间。我们会得到像这样的输出:

Took0.2730millisecondstogenerate:77005292

Took0.0234millisecondstogenerate:77005292

Took0.0200millisecondstogenerate:77005292

Took0.0281millisecondstogenerate:77005292

Took0.0162millisecondstogenerate:77005292

Took0.0245millisecondstogenerate:77005292

Took0.0677millisecondstogenerate:77005292

Took0.0289millisecondstogenerate:77005292

Took0.0240millisecondstogenerate:77005292

Took0.0311millisecondstogenerate:77005292

请注意第一次时间和其它九次的时间完全不一样。这很可能是因为浏览器中的JavaScript引擎使用了优化措施,需要一些热身时间。我们基本上没有办法避免这种情况,但是会有一些好的补救措施来阻止我们得出一些错误的结论。

一种方式是去计算后面9次的平均时间。另外一种更加使用的方式是收集所有的结果,然后计算“中位数”。基本上,它会将所有的结果排列起来,对结果进行排序,然后取中间的一个值。这是performance.now()函数如此有用的地方,因为无论你做什么,你都可以得到一个数值。

让我们再试一次,这次我们使用中位数函数:

varnumbers=[];

for(vari=;i

vart0=performance.now();

makeHash('Peter');

vart1=performance.now();

numbers.push(t1-t0);

}

functionmedian(sequence){

sequence.sort();// note that direction doesn't matter

returnsequence[Math.ceil(sequence.length/2)];

}

console.log('Median time',median(numbers).toFixed(4),'milliseconds');

缺陷 #4 – 以可预测的方式比较函数

我们已经理解衡量一些函数很多次并取平均值总会是一个好主意。而且,上面的示例告诉我们使用中位数要比平均值更好。

在实际中,衡量函数执行时间的一个很好的用处是来了解在几个函数中,哪个更快。假设我们有两个函数,它们的输入参数类型一致,输出结果相同,但是它们的内部实现机制不一样。

functionisIn(haystack,needle){

varfound=false;

haystack.forEach(function(element){

if(element.toLowerCase()===needle.toLowerCase()){

found=true;

}

});

returnfound;

}

console.log(isIn(['a','b','c'],'B'));// true

console.log(isIn(['a','b','c'],'d'));// false

我们可以立刻发现这个方法有改进的地方,因为haystack.forEach循环总会遍历所有的元素,即使我们可以很快找到一个匹配的元素。现在让我们使用for循环来编写一个更好的版本。

functionisIn(haystack,needle){

for(vari=,len=haystack.length;i

if(haystack[i].toLowerCase()===needle.toLowerCase()){

returntrue;

}

}

returnfalse;

}

console.log(isIn(['a','b','c'],'B'));// true

console.log(isIn(['a','b','c'],'d'));// false

现在我们来看哪个函数更快一些。我们可以分别运行每个函数10次,然后收集所有的测量结果:

functionisIn1(haystack,needle){

varfound=false;

haystack.forEach(function(element){

if(element.toLowerCase()===needle.toLowerCase()){

found=true;

}

});

returnfound;

}

functionisIn2(haystack,needle){

for(vari=,len=haystack.length;i

if(haystack[i].toLowerCase()===needle.toLowerCase()){

returntrue;

}

}

returnfalse;

}

console.log(isIn1(['a','b','c'],'B'));// true

console.log(isIn1(['a','b','c'],'d'));// false

console.log(isIn2(['a','b','c'],'B'));// true

console.log(isIn2(['a','b','c'],'d'));// false

functionmedian(sequence){

sequence.sort();// note that direction doesn't matter

returnsequence[Math.ceil(sequence.length/2)];

}

functionmeasureFunction(func){

varletters='a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z'.split(',');

varnumbers=[];

for(vari=;i

vart0=performance.now();

func(letters,letters[i]);

vart1=performance.now();

numbers.push(t1-t0);

}

console.log(func.name,'took',median(numbers).toFixed(4));

}

measureFunction(isIn1);

measureFunction(isIn2);

我们运行上面的代码, 可以得出如下的输出:

true

false

true

false

isIn1took0.0050

isIn2took0.0150

这个示例的在线演示如下所示:

到底发生了什么?第一个函数的速度要快3倍!那不是我们假设的情况。

其实假设很简单,但是有些微妙。第一个函数使用了haystack.forEach方法,浏览器的JavaScript引擎会为它提供一些底层的优化,但是当我们使用数据索引技术时,JavaScript引擎没有提供对应的优化。这告诉我们:在真正测试之前,你永远不会知道。

结论

在我们试图解释如何使用performance.now()方法得到JavaScript精确执行时间的过程中,我们偶然发现了一个基准场景,它的运行结果和我们的直觉相反。问题在于,如果你想要编写更快的web应用,我们需要优化JavaScript代码。因为计算机(几乎)是一个活生生的东西,它很难预测,有时会带来“惊喜”,所以如果了解我们代码是否运行更快,最可靠的方式就是编写测试代码并进行比较。

当我们有多种方式来做一件事情时,我们不知道哪种方式运行更快的另一个原因是要考虑上下文。在上一节中,我们执行一个大小写不敏感的字符串查询来寻找1个字符串是否在其它26个字符串中。当我们换一个角度来比较1个字符串是否在其他100,000个字符串中时,结论可能是完全不同的。

上面的列表不是很完整的,因为还有更多的缺陷需要我们去发现。例如,测试不现实的场景或者只在JavaScript引擎上测试。但是确定的是对于JavaScript开发者来说,如果你想编写更好更快的Web应用,performance.now()是一个很棒的方法。最后但并非最不重要,请谨记衡量执行时间只是“更好的代码”的一反面。我们还要考虑内存消耗以及代码复杂度。

three.js 学习

创建场景

    三个要素: 场景,相机,渲染器

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight ); // setSize 
document.body.appendChild( renderer.domElement );

PerspectiveCamera 场景

现在已经设置好了场景,相机,渲染器。 three  有几个不同的渲染器, 我们使用 PerspectiveCamera.

1. 第一个属性是视野, FOV 是任何给定时刻显示器上看到的场景的范围。该值以度为单位。

2. 第二个是设置宽高,

3. 第三个属性与第四个属性是近和远裁剪平面。这意味着,远离相机和物体远远超过或接近近处的物体不会被渲染,

WebGLRenderer 渲染器

 除了 webGLRenderer 之外 还有其他一些功能,通常用作旧版浏览器的用户的回退功能,或者由于某些原因而无法支持 WebGL 的用户。

添加到页面

我们将 renderer 元素添加到 HTML 文档中。这是渲染器用来向我们显示场景的 <canvas> 元素。

setSize 

除了创建渲染器实例外,设置 程序显示的实际大小,如果想保持应用程序的大小,但以较低的分辨率渲染它,可以通过 false 作为 updateStyle (第三个参数)调用 setSize 来实现,例如 setSize(window.innerWidth / 2, window.innerHeight / 2, false)

Array.prototype 属性表示 Array 构造函数的原型,并允许您向所有的 Array 对象添加新的属性和方法。

描述

Array 实例继承自 Array.prototype 。 与所有构造函数一样,可以更改构造函数的原型对象,以对所有的 Array 实例进行更改。 例如,可以添加新的方法和属性以扩展所有 Array 对象。 

Array.prototype 本身也是一个 Array

Array.isArray(Array.prototype)

属性

Array.prototype.constructor

     所有的数组实例都继承了这个属性,它的值就是 Array, 表明了所有的数组都是由 Array 构造函数出来的。

Array.prototype.length

    因为 Array.prototype 也是个数组,所以它也有 length 属性, 这个值为0, 因为它是个空数组。

方法

会改变自身的方法

下面这些方法会改变调用它们的对象自身的值:

Array.prototype.copyWithin()

    在数组内部,将一段元素序列拷贝到另一段元素序列上,覆盖原有的值。

Array.prototype.fill()

    将数组中指定区间的所有元素的值,都替换成某个固定的值。

Array.prototype.pop()

    删除数组的最后一个元素,并返回这个元素。

Array.prototype.push()

    在数组的末尾增加一个或多个元素,并返回数组的新长度。

Array.prototype.reverse()

    颠倒数组中元素的排列顺序,即原先的第一个变为最后一个,原先的最后一个变为第一个。

Array.prototype.shift()

    删除数组的第一个元素,并返回这个元素。

Array.prototype.sort()

    对数组元素进行排序,并返回当前数组。

Array.prototype.splice()

    在任意的位置给数组添加或删除任意个元素。

Array.prototype.unshift()

    在数组的开头增加一个或多个元素,并返回数组的新长度。

不会改变自身的方法

下面的这些方法绝对不会改变调用它们的对象的值,只会返回一个新的数组或者返回一个其他的期望值

Array.prototype.concat()

    返回一个由当前数组和其它若干个数组或者若干个非数组值组合而成的新数组。

Array.prototype.includes()

    判断单签数字是否包含某指定值,如果是返回 true ,否则返回 false。

Array.prototype.join()

    连接所有数组元素组成一个字符串。

Array.prototype.slice()

    抽取当前数组中的一段元素组合成一个新的数组。

Array.prototype.toSource()  // 这个 API 并没有标准化

    返回一个表示当前数组字面量的字符串。遮蔽了原型链上的 Object.prototype.toSource() 方法。

Array.prototype.toString()

    返回一个由所有数组元素组合而成的字符串。遮蔽了原型链上的 Object.prototype.toString() 方法

Array.prototype.toLocaleString()

   返回一个由所有数组元素组合而成的本地化后的字符串。遮蔽了原型链上的 Object.prototype.toLocaleString() 方法。

Array.prototype.indexOf()

    返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。

Array.prototype.lastIndexOf()

    返回数组中最后一个(从右边数第一个)与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。


遍历方法

    在下面的众多遍历方法中,有很多方法都需要指定一个回调函数作为参数。在回调函数执行之前,数组的长度会被缓存在某个地方,所以,如果你在回调函数中为当前数组添加了新的元素,那么那些新添加的元素是不会被遍历到的。此外,如果在回调函数中对当前数组进行了其它修改,比如改变某个元素的值或者删掉某个元素,那么随后的遍历操作可能会受到未预期的影响。总之,不要尝试在遍历过程中对原数组进行任何修改,虽然规范对这样的操作进行了详细的定义,但为了可读性和可维护性,请不要这样做

Array.prototype.forEach()

    为数组中的每个元素执行一次回调函数。

Array.prototype.entries() 

    返回一个数组迭代器对象,该迭代器会包含所有数组元素的键值对。

Array.prototype.every()

    如果数组中的每个元素都满足测试函数,则返回 true,否则返回 false。

Array.prototype.some()

    如果数组中至少有一个元素满足测试函数,则返回 true,否则返回 false。

Array.prototype.filter()

    将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回。

Array.prototype.find() 

    找到第一个满足测试函数的元素并返回那个元素的值,如果找不到,则返回 undefined。

Array.prototype.findIndex() 

    找到第一个满足测试函数的元素并返回那个元素的索引,如果找不到,则返回 -1。

 Array.prototype.keys() 

    返回一个数组迭代器对象,该迭代器会包含所有数组元素的键。Array.prototype.map() 返回一个由回调函数的返回值组成的新数组。

Array.prototype.reduce()

    从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值。

Array.prototype.reduceRight()

    从右到左为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值。

 Array.prototype.values() 

    返回一个数组迭代器对象,该迭代器会包含所有数组元素的值。

Array.prototype[@@iterator]()

    和上面的 values() 方法是同一个函数。


vue 中显示隐藏问题

vue  使用当用 遇到一个 temple 模板里面的坑,就是 v-if  和 v-show 不显示的问题,需要在标签中加入 style="display:black"

vue 中组建之间的传参问题

1.父向子组件传参

App.vue为父,引入componetA组件之后,则可以在App.vue中使用标签(注意驼峰写法要改成componet-a写法,因为html对大小写不敏感,componenta与componentA对于它来说是一样的,不好区分,所以使用小写-小写这种写法)。

而子组件componetA中,声明props参数’msgfromfa’之后,就可以收到父向子组件传的参数了。例子中将msgfromfa显示在<p>标签中。

App.vue中
<component-a msgfromfa="( 不积跬步,无以至千里;不积小流,无以成江海)"></component-a>
import componentA from './components/componentA'
export default {
    new Vue({
        components: {
          componentA
        }
    })
}

componentA.vue中
<p>{{ msgfromfa }}</p>
export default {
    props: ['msgfromfa']
}

2.子组件向父传参

2.1  .$emit

用法:vm.$emit( event, […args] ),触发当前实例上的事件。附加参数都会传给监听器回调。
例子:

App.vue中component-a绑定了自定义事件”child-say”。

子组件componentA中,单击按钮后触发”child-say”事件,并传参msg给父组件。父组件中listenToMyBoy方法把msg赋值给childWords,显示在<p>标签中。

App.vue中
<p>Do you like me? {{childWords}}</p>
<component-a msgfromfa="(Just Say U Love Me)" v-on:child-say="listenToMyBoy"></component-a>
import componentA from './components/componentA'
export default {
    new Vue({
        data: function () {
            return {
              childWords: ""
            }
        },
        components: {
          componentA
        },
        methods: {
            listenToMyBoy: function (msg){
              this.childWords = msg
            }
        }
    })
}
componentA.vue中
<button v-on:click="onClickMe">like!</button>
import componentA from './components/componentA'
export default {
    data: function () {
        return {
          msg: 'I like you!'
        }
    },
    methods: {
      onClickMe: function(){
        this.$emit('child-say',this.msg);
      }
    }
}

2.2  .$dispatch

用法:vm.$dispatch( event, […args] ),派发事件,首先在实例上触发它,然后沿着父链向上冒泡在触发一个监听器后停止。
例子:App.vue中events中注册”child-say”事件。子组件componentA中,单击按钮后触发”child-say”事件,并传参msg给父组件。父组件中”child-say”方法把msg赋值给childWords,显示在<p>标签中。

App.vue中
<p>Do you like me? {{childWords}}</p>
<component-a msgfromfa="(Just Say U Love Me)"></component-a>
import componentA from './components/componentA'
export default {
    new Vue({
        events: {
            'child-say' : function(msg){
              this.childWords = msg
            }
        }
    })
}
componentA.vue中
<button v-on:click="onClickMe">like!</button>
import componentA from './components/componentA'
export default {
    data: function () {
        return {
          msg: 'I like you!'
        }
    },
    methods: {
      onClickMe: function(){
        this.$dispatch('child-say',this.msg);
      }
    }
}

vue 中 axios 请求

axios 请求的坑:请求当中的 axios 在浏览器中需要 URLSearchParams 的支持

var params = new URLSearchParams();
params.append('param1','value1');
params.append('param2','value2');
axios.post('/foo',params);

// 注意: URLSearchParams  有兼容性问题 ios 10.11 以下不支持 需要兼容,请看 dome

一、DOM操作影响页面性能的核心问题

通过js操作DOM的代价很高,影响页面性能的主要问题有如下几点:

  • 访问和修改DOM元素
  • 修改DOM元素的样式,导致重绘重排
  • 通过对DOM元素的事件处理,完成与用户的交互功能

DOM的修改会导致重绘重排

  • 重绘是指一些样式的修改,元素的位置和大小都没有改变;
  • 重排是指元素的位置或尺寸发生了变化,浏览器需要重新计算渲染树,而新的渲染树建立后,浏览器会重新绘制受影响的元素。

页面重绘的速度要比页面重排的速度快,在页面交互中要尽量避免页面的重排操作。浏览器不会在js执行的时候更新DOM,而是会把这些DOM操作存放在一个队列中,在js执行完之后按顺序一次性执行完毕,因此在js执行过程中用户一直在被阻塞。

1.页面渲染过程

一个页面更新时,渲染过程大致如下:

  • JavaScript: 通过js来制作动画效果或操作DOM实现交互效果
  • Style: 计算样式,如果元素的样式有改变,在这一步重新计算样式,并匹配到对应的DOM上
  • Layout: 根据上一步的DOM样式规则,重新进行布局(重排
  • Paint: 在多个渲染层上,对新的布局重新绘制(重绘
  • Composite: 将绘制好的多个渲染层合并,显示到屏幕上

在网页生成的时候,至少会进行一次布局和渲染,在后面用户的操作时,不断的进行重绘或重排,因此如果在js中存在很多DOM操作,就会不断地出发重绘或重排,影响页面性能。

2.DOM操作对页面性能的影响

如前面所说,DOM操作影响页面性能的核心问题主要在于DOM操作导致了页面的重绘重排,为了减少由于重绘和重排对网页性能的影响,我们要知道都有哪些操作会导致页面的重绘或者重排。

2.1 导致页面重排的一些操作:

  • 内容改变
    • 文本改变或图片尺寸改变
  • DOM元素的几何属性的变化
    • 例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM重新排建渲染树中的相关节点。如果父节点的几何属性变化时,还会使其子节点及后续兄弟节点重新计算位置等,造成一系列的重排。
  • DOM树的结构变化
    • 添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会造成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素造成影响,但是如果在所有的节点前添加一个新的元素,则后续的所有元素都要进行重排。
  • 获取某些属性
    • 除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:offsetTopoffsetLeft、 offsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeight、 clientTopclientLeftclientWidthclientHeightgetComputedStyle()
  • 浏览器窗口尺寸改变
    • 窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,因此会造成重排。

2.2 导致页面重绘的操作

  • 应用新的样式或者修改任何影响元素外观的属性
    • 只改变了元素的样式,并未改变元素大小、位置,此时只涉及到重绘操作。
  • 重排一定会导致重绘
    • 一个元素的重排一定会影响到渲染树的变化,因此也一定会涉及到页面的重绘。

二、高频操作DOM会导致的问题

接下来会分享一下在平时项目中由于高频操作DOM影响网页性能的问题。

1. 抽奖项目的高频操作DOM问题

1.1 存在的问题

在最近做的抽奖项目中,就遇到了这样的由于高频操作DOM,导致页面性能变差的问题。在经历几轮抽奖后,文字滚动速度越来越慢,肉眼能感受到与第一次抽奖时文字滚动速度的明显差别,如持续时间过长或轮次过多,还会造成浏览器假死现象。

实现 demo

1.2 问题分析

下图为抽奖时文字滚动过程中的timeline记录。

timeline分析:

  1. FPS:最上面一栏为绿色柱形为帧率(FPS),顶点值为60fps,上方红色方块表示长帧,这些长帧被Chrome称为jank(卡顿)。
  2. CPU:第二栏为CPU,蓝色表示loading(网络通信和HTML解析),黄色表示scripting(js执行时间),紫色表示rendering(样式计算和布局,即重排), 绿色为painting(即重绘)。

更多timeline使用方法可参考:如何使用Chrome Timeline 工具(译)

由上图可以看出,在文字滚动过程中红色方块出现频繁,页面中存在的卡顿过多。帧率的值越低,人眼感受到的效果越差。

接下来选择一段长帧区域放大来看

在这段区域内最大一帧达到了49.7ms,帧率只有20fps,接下来看看这一帧里是什么因素耗时过长


由上图可以看出,耗时最大的在scripting,js的执行时间达到了44.9ms,占总时间的93.2%,因为主要靠js计算控制DOM的显示内容,所以js运行时间过长。

选取一段FPS值很低的部分查看造成这段值低的原因

由下图可看出主要为dom.html中的js执行占用时间。

点进dom.html文件,即可定位到该函数

由此可知,主要是rolling这个函数执行时间过长,对该部分失帧影响较大。而这个函数的主要作用就是实现文字的滚动效果,也可以从代码中看出,这个函数利用的setTimeout来反复执行,并且在这个函数中存在着循环以及大量的DOM操作,造成了页面的失帧等问题。

1.3 优化方案

针对该项目中的问题,采取的解决方法是:

  • 一次性生成全部<li>,并且隐藏这些<li>,随机生成一组随机数数组,只有index与数组里面的随机数相等时,才显示该位置的<li>,虽然也会触发重排和重绘,但是性能要远远高于直接操作DOM的添加和删除。
  • 用requestAnimationFrame取代setTimeout不断生成随机数。

requestAnimationFrame与setTimeout和setInterval类似,都是通过递归调用同一个方法不断更新页面。

  • setTimeout():在特定的时间后执行函数,而且只执行一次,如果在特定时间前想取消执行函数,可以用clearTimeout立即取消执行。但是并不是每次执行setTimeout都会在特定的时间后执行,页面加载后js会按照主线程中的顺序按序执行那个,如果在延迟时间内主线程不空闲,setTimeout里面的函数是不会执行的,它会延迟到主线程空闲时才执行。
  • setInterval():在特定的时间间隔内重复执行函数,除非主动清除它,不然会一直执行下去,清除函数可以使用clearInterval。setInterval也会等到主线程空闲了再执行,但是setInterval去排队时,如果发现自己还在队列中未执行,就会被drop掉,所以可能会造成某段时间的函数未被执行。
  • requestAnimationFrame():它不需要设置时间间隔,它会在浏览器每次刷新之前执行回调函数的任务。这样我们动画的更新就能和浏览器的刷新频率保持一致。requestAnimationFrame在运行时,浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

在采用上面的方法进行优化后,在经历多轮抽奖后,文字滚动速度依旧正常,网页性能良好,不会出现文字滚动速度越来越慢,最后导致浏览器假死的现象。

实现demo: https://gxt19940130.github.io/demo/demo_gxt/dom_by_vue.html

1.4 优化前后FPS对比

优化前文字滚动时的timeline


优化后文字滚动时的timeline

优化前的代码对DOM操作很频繁,因此FPS值普遍偏低,而优化后可以看出红色方块明显减少,FPS值一直处于高值。

1.5 优化前后CPU占用对比

优化前文字滚动时的timeline


优化后文字滚动时的timeline


优化前js的CPU占用率较高,而优化后占用CPU的主要为渲染时间,因为优化后的代码只是控制了节点的显示和隐藏,所以在js上消耗较少,在渲染上消耗较大。

2.吸顶导航条相关及scroll滚动优化

2.1 存在的问题

吸顶导航条要求当页面滚动到某个区域时,对应该区域的导航条在设置的显示范围内保持吸顶显示。涉及到的操作:

  • 监听页面的scroll事件
  • 在页面滚动时进行计算和DOM操作
    • 计算:计算当前所在位置是否为对应导航条的显示范围
    • DOM操作:显示在范围内的导航条并且隐藏其他导航条

由于scroll事件被触发的频率高、间隔近,如果此时进行DOM操作或计算并且这些DOM操作和计算无法在下一次scroll事件发生前完成,就会造成掉帧、页面卡顿,影响用户体验。

2.2 优化方案

针对该项目中的问题,采取的解决方法是:

  • 尽量控制DOM的显示或隐藏,而不是删除或添加:

    页面加载时根据当前页面中吸顶导航的数量复制对应的DOM,并且隐藏这些导航。当页面滚动到指定区域后,显示对应的导航。

  • 一次性操作DOM:

    将复制的DOM存储到数组中,将该数组append到对应的父节点下,而不是根据复制得到DOM的数量依次循环插入到父节点下。

  • 多做缓存:

    如果某个节点将在后续进行多次操作,可以将该节点利用变量存储起来,而不是每次进行操作时都去查找一遍该节点。

  • 使用 requestAnimationFrame优化页面滚动

// 在页面滚动时对显示范围进行计算

  // 延迟到整个dom加载完后再调用,并且异步到所有事件后执行

  $(function(){

  //animationShow优化滚动效果,scrollShow为实际计算显示范围及操作DOM的函数

      setTimeout( function() {

          window.Scroller.on('scrollend', animationShow);

          window.Scroller.on('scrollmove', animationShow);

      })

  });

  function animationShow(){

      return window.requestAnimationFrame ?window.requestAnimationFrame(scrollShow) : scrollShow();

  }

对于scroll的滚动优化还可以采用防抖(Debouncing)和节流(Throttling)的方式,但是防抖和节流的方式还是要借助于setTimeout,因此和requestAnimationFrame相比,还是requestAnimationFrame实现效果好一些。

三、针对操作DOM的性能优化方法总结

为了减少DOM操作对页面性能产生的影响,在实现页面的交互效果时一定要注意一下几点:

1.减少在循环内进行DOM操作,在循环外部进行DOM缓存

//优化前代码

function Loop() {

   console.time("loop1");

   for (var count = 0; count < 15000; count++) {

       document.getElementById('text').innerHTML += 'dom';

   }

   console.timeEnd("loop1");

}

//优化后代码

function Loop2() {

    console.time("loop2");

    var content = '';

    for (var count = 0; count < 15000; count++) {

        content += 'dom';

    }

    document.getElementById('text2').innerHTML += content;

    console.timeEnd("loop2");

}

两个函数的执行时间对比:

优化前的代码中,每进行一次循环,都会读取一次divinnerHtml属性,并且对这个属性进行了重新赋值,即每循环一次就会操作两次DOM,因此执行时间很长,页面性能差。

在优化后的代码中,将要更新的DOM内容进行缓存,在循环时只操作字符串,循环结束后字符串的值写入到div中,只进行了一次查找innerHtml属性和一次对该属性重新赋值的操作,因此同样的循环次数先,优化后的方法执行时间远远少于优化前。

2.只控制DOM节点的显示或隐藏,而不是直接去改变DOM结构

在抽奖项目中频繁操作DOM来控制文字滚动的方法(demo:https://gxt19940130.github.io/demo/dom.html 导致页面性能很差,最后修改为如下代码。

<div class="staff-list" :class="list">

   <ul class="staff-list-ul">

       <li v-for="item in staffList" v-show="isShow($index)">

           <div>{{{item.staff_name | addSpace}}} </div>

           <div class="staff_phone">{{item.phone_no}} </div>

       </li>

   </ul>

</div>

上面代码的优化原理即先生成所有DOM节点,但是所有节点均不显示出来,利用vue.js中的v-show,根据计算的随机数来控制显示某个<li>,来达到文字滚动效果。

如果采用jquery,则需要将生成的所有<li>全部存放在<ul>下,并且隐藏它们,在根据生成的随机数组,利用jquery查找index与生成的随机数对应的<li>并显示,达到文字滚动效果。

优化后demo: https://gxt19940130.github.io/demo/demo_gxt/dom_by_vue.html

对比结果可查看2.4

3.操作DOM前,先把DOM节点删除或隐藏

var list1 = $(".list1");

list1.hide();

for (var i = 0; i < 15000; i++) {

    var item = document.createElement("li");

    item.append(document.createTextNode('0'));

    list1.append(item);

}

list1.show();

display属性值为none的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。

页面rendering时间对比:

下图为同样的循环次数下未隐藏节点直接进行DOM操作的rendering时间(图一)和隐藏节点再进行DOM操作的rendering时间(图二)

由对比图可以看出,总时间、js执行时间以及rendering时间都明显减少,并且避免了painting以及其他的一些操作。

4. 最小化重绘和重排

//优化前代码

var element = document.getElementById('mydiv');

element.style.height = "100px";  

element.style.borderLeft = "1px";  

element.style.padding = "20px";

在上面的代码中,每对element进行一次样式更改都会影响该元素的集合结构,最糟糕情况下会触发三次重排。

优化方式:利用js或jquery对该元素的class重新赋值,获得新的样式,这样减少了多次的DOM操作。

//优化后代码

//js操作

.newStyle {  

    height: 100px;  

    border-left: 1px;  

    padding: 20px;  

}  

element.className = "newStyle";

//jquery操作

$(element).css({

    height: 100px;  

    border-left: 1px;  

    padding: 20px;

})

到此本文结束,如果对于问题分析存在不正确的地方,还请及时指出,多多交流。

参考文章:

  • 高性能JS-DOM
  • Effective前端6:避免页面卡顿
  • 前端性能优化
  • 高性能滚动 scroll 及页面渲染优化
  • 脑洞大开:为啥帧率达到 60 fps 就流畅?
  • 如何使用Chrome Timeline 工具(译)