JS事件循环机制(Event Loop)
众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但本质上JavaScript
是单线程,,可是浏览器又能很好的处理异步请求,那么到底是为什么呢?
任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout
定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。
当主线程的任务执行完毕,就回去任务队列读取相应的任务,并推入主线程执行。上述过程不断重复也即是我们所说的Event
Loop(事件循环机制)(Tick)
并且推入主线程的任务可以分为宏任务、微任务
宏任务与微任务
最先进入任务队列的宏任务,执行其同步任务,然后判断是否存在微任务,有的话则执行微任务至微任务队列为空,开始下一轮任务循环(Tick),执行宏任务中的异步代码(setTimout等异步回调)
是否可以理解为优先级: 无论宏任务还是微任务同步任务>异步任务,微任务>宏任务
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
// script start
// promise1
// script end
// then1
// timeout1
// timeout2
首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;当遇到任务源
(task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出
script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到
promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout
,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到
console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1
再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出
timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script
end, then1, timeout1, timeout2
这方面一些拓展相关知识
async与await是怎么处理异步任务的
简单说async是通过Promise包装异步任务
调用async1,遇见await这时候就会暂停async1的执行,让出线程去执行async2的代码,等async2执行完毕再回到async1的线程继续执行;
因此输出async1()得到pedding状态的promise,因为console.log是同步任务,会立即执行,而此时async1()去执行async2()了,返回一个Promise对象,并且状态是pedding
Promise、process.nextTick谁先执行?
process.nextTick为一个特殊的异步api,它不属于任何的Event
Loop阶段,并且为Node环境下的方法,当Node遭遇这个api时,Event
Loop根本不会继续执行,而是暂停,先执行process.nextTick,因此process.nextTick遭遇Promise,肯定是技高一筹,nextTick的队列优先级会更高
Vue中的vm.$nextTick
先要注意这里的vm.$nextTick与上面的process.nextTick完全不是同一个东西
vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。
它是基于事件循环实现的,‘下次DOM更新周期’指的是下次微任务执行更新DOM时候,vm.$nextTick会将回调函数添加的微任务中,并且在微任务之后运行
//改变数据
vm.message = 'changed'
//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'
//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})
简单说,如果你需要更新DOM后获取DOM,或者在DOM更新后执行某一块代码,你必须将这块代码放到下一次事件循环,比如setTimeout(fn,0),vm.$nextTick(fn),这样DOM在更新之后就会立即执行这块代码。
事件循环:
简单来说,Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
第一个tick(本次更新循环)
首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及DOM.
Vue开启一个异步队列,并缓冲在此事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入队列中一次。
第二个tick(‘下次更新循环’)
同步任务执行完毕,开始执行异步watcher队列的任务,更新DOM。Vue在内部尝试对异步队列使用原生的Promise.then和MessageChannel 方法,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
第三个tick(下次 DOM 更新循环结束之后)
最后再来一点小练习吧
求求你一定要快、准、狠地做出来!!!
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')