宏任务和微任务
微任务 --- microtask称为jobs, 宏任务 --- macrotask称为task, 宏任务是由宿主发起的,而微任务由JavaScript自身发起。
常见的面试题:
console.log('start') setTimeout(()=>{ console.log('setTimeout') },0) new Promise((resolve)=>{ console.log("promise"); resolve(); }).then(()=>{ console.log("then1"); }).then(()=>{ console.log("then2"); }) console.log("end");
它的顺序是
start promise end then1 then2 setTimeout
实践
解:
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
执行栈选择最先进入队列的宏任务,执行其同步代码直至结束;
检查是否存在微任务,有则会执行微任务队列为空;
如果宿主为浏览器,可能会渲染页面;
开始下一轮tick,执行宏任务中的异步代码;
setImmediate 和 process.nextTick 为Node环境下常用的方法,所以基于Node宿主分析。
( Node.js是运行在服务端的js,虽然用到也是v8引擎,但由于服务目的和环境不同,导致了它的API和原生JS有些区别,其EventLoop还要处理一些I/O,比如新的网络连接等。)
执行顺序如下:
1、timers: 执行setTimeout和setInterval的回调。
2、pending callbacks:执行延迟到下一个循环迭代的I/O回调。
3、idle prepare:仅系统内部使用。
4、poll:检索新的I/O事件;执行与I/O事件相关的回调。
5、check: setImmediate在这里执行。
6、close callbacks: 一些关闭的函调函数
setImmediate和setTimeout
console.log("outer"); setTimeout(() => { setTimeout(() => { console.log("setTimeout"); },0); setImmediate(() => { console.log("setImmediate") }) },0)
其执行顺序为:
1、外层是一个setTimeout,所以执行它的回调函数已经在timers阶段了。
2、处理里边的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下一级timers阶段
3、处理里边的setImmediate,将它的回调加入check阶段的队列。
4、外层timers阶段处理完,进入pending callbacks,idle,prepare,poll,这几个队列都是空的,所以继续住下到了check阶段,发现了setImmediate的回调,拿出来执行
5、然后是close callbacks,队列是空的,跳过
在Node里边对setTimeout的特殊处理,setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)
1、遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入timers阶段。
2、遇到setImmediate塞入check阶段。
3、同步代码执行完毕,进入Event Loop
4、先进入timers阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过14毫秒,跳过。
5、跳过空的阶段,进入check阶段,执行setImmediate
可见,1毫秒是个关键点,所以,setImmediate不一定在setTimeout之前执行了。
promise 和 process.nextTick
因为 process.nextTick 为node环境下的方法,是一个特殊的异步API,其不属于任何的Event Loop阶段。
事实上Node在遇到这个API时,Event Loop根本不会继续进行,会马上停下来执行process.nextTick( ), 这个执行完才会继续Event Loop。所以,nextTick 和 Promise 同时出现,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。
vue实例的nextTick( )方法接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。这个API就是基于事件循环实现的。下次DOM更新就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中。