一、基础知识
-
js是单线程运行,同一时间只能做一件事情,这是因为js是浏览器脚本语言,用途是与用户互动进行DOM操作,单线程运行可以避免同时操作同一个DOM的矛盾问题
-
js的单线程中,将任务分为2种:一种是同步任务,一种是异步任务
同步任务:在主线程上排队进行的,按顺序执行
异步任务:不进入主线程,而进入event table执行,异步事件完成有结果后把结果回调函数放入“任务队列”(taskqueue),只有“任务队列”通知主线程某个异步任务可以执行了,该任务才会进入主线程执行 -
js代码运行分为2个阶段:编译阶段和执行阶段
– 编译阶段:由编译器完成,把代码翻译成可执行代码,这个阶段作用域规则会确定(函数的定义,变量的声明提前)
– 执行阶段:由引擎完成,任务是执行可执行代码(按照js运行机制),上下文是在执行阶段被创建的 -
js中的异步操作
– setTimeOut 和 setInterval
– ajax
– promise
– DOM事件
二、Event Loop事件循环
- 同步任务在主线程上执行形成“执行栈”(execution context stack)
- 异步任务进入Event Table中,并在里面注册回调函数,当异步事件完成时,Event Table将这个函数移到在“主线程”之外的“任务队列”中
- 一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,如果有任务,就拿到相应的回调函数去主线程执行
上述过程不断重复,这就是Event Loop事件循环
三、宏任务or微任务
异步任务可以分为宏任务和微任务。在异步操作promise中,new promise会被放到主线程中立即执行,promise.then会被放到微任务队列中。
- 宏任务(macro-task):整体代码script、setTimeOut、setInterval
- 微任务(mincro-task):promise.then、promise.nextTick(node)
事件循环的顺序:进入整体代码(宏任务)后开始第一次循环,接着执行所有微任务,然后再从宏任务开始执行。在同一个事件循环里面。微任务是优先于宏任务的,当微任务没有完成的时候,是不会执行宏任务的。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
这段代码的执行顺序:
1、进入整体代码
2、setTimeout回调函数会被注册发放到宏任务列表中
3、new Promise立即执行,then发放到微任务中
4、主线程执行完毕,执行微任务列表
5、第一轮循环结束,从宏任务开始,执行setTimeout
在执行微任务的时候,若发现新的微任务,会把这个新的微任务添加到队列尾部,微任务队列依次执行完毕后,才会执行下一个循环;
console.log(1);
setTimeout(function() {
console.log(2);
})
new Promise(function(resolve) {
console.log(3);
}).then(function() {
console.log(4);
}).then(function() {
console.log('5.我是新增的微任务');
});
console.log(6);
// 1,6,3,4,5.我是新增的微任务,2
四、单线程的优缺点
- 优点:系统稳定,不会产生严重的同步问题,比如一个线程操作DOM的增加另一个线程操作DOM的删除
- 缺点:容易出现代码的阻塞
五、举栗子
1、for循环中的定时器
for(var i=0; i<5; i++){
setTimeout(function() {
console.log(i);
},1000)
}
//5,5,5,5,5
setTimeout会被拿到异步队列中,当“执行栈”中的所有同步任务执行完毕(这个时候for循环结束,i已经为5),系统读取“任务队列”的setTimeout(循环了5次,有5个)
- 补充:让输出结果为0-4
1.将var变为let
2.加个立即执行函数
3.加闭包
//使用let
for(let i=0; i<5; i++){
setTimeout(function() {
console.log(i);
},1000)
}
//使用立即执行函数
for(var i=0; i<5; i++){
(function(i){
setTimeout(function() {
console.log(i);
},1000)
})(i)
}
//使用闭包
for(var i=0; i<5; i++){
var a = function(){
var j = i;
setTimeout(function() {
console.log(j);
},1000)
}
a();
}
2、执行顺序
function add(x, y) {
console.log(1)
setTimeout(function() { // timer1
console.log(2)
}, 1000)
}
add();
setTimeout(function() { // timer2
console.log(3)
})
new Promise(function(resolve) {
console.log(4)
setTimeout(function() { // timer3
console.log(5)
}, 100)
for(var i = 0; i < 100; i++) {
i == 99 && resolve()
}
}).then(function() {
setTimeout(function() { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
执行结果
//1,4,8,7,3,6,5,2
主线程任务:1,4,8
微任务:7
宏任务:timer1,timer2,timer3,timer4(其中按照定时器延迟时间顺序为timer2,timer4,timer3,timer1)
- 定时器的补充:
1.在到达指定时间时,定时器就会将相应回调函数插入“任务队列”尾部。这就是“定时器(timer)”功能。定时器的第二个参数是指定其回调函数推迟/每隔多少毫秒数后执行。
2.当第二个参数缺省时,默认为 0;当指定的值小于 4 毫秒,则增加到 4ms(4ms 是 HTML5 标准指定的,对于 2010 年及之前的浏览器则是 10ms);也就是说至少需要4毫秒,该setTimeout()拿到任务队列中。
所以在上面的例子中,第二个参数缺省比第二参数为0的先执行。