前言
学习概念之前让我们来看几行简单的代码:
console.log(1)setTimeout(()=>{console.log(2);},0);constpromise=newPromise((resolve,reject)=>{console.log(3);resolve();});promise.then(()=>{console.log(4);});console.log(5);
输出结果如下:
13542
和你预想的结果一样吗? 我经常在工作中遇到这种类似的问题,我明明想让它先执行,他就是后执行,导致页面渲染的各种问题。 想要弄清楚为什么输出结果是这样的,我们就需要了解浏览器的事件循环机制。
事件循环机制
JavaScript 事件循环机制分为: 浏览器事件循环机制和 Node 事件循环机制,两者的实现技术不一样,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。 我们只讲浏览器事件循环机制。
浏览器执行 js 代码大致可以分为三个步骤,而这三个步骤的循环构成了 js 的事件循环机制(如上图所示)。
主线程(js引擎线程)中执行宏任务(JS整体代码或回调函数),执行过程中会将对象存储到堆(heap)中,将函数的参数和局部变量加入到栈(stack)中,执行完毕后会释放堆或退出栈。执行完这个宏任务后,会判断微任务队列是否为空,如果不为空,则会将所有的微任务依次取出并执行
。如果在这个过程中触发了任何 Web APIs 将进行第二步操作。
调用 Web API,并在合适的时候将回调函数加入到事件回调队列(event queue)中。比如执行了setTimeout(callback1, 1000)
,会创建一个计时器,并且在另一个线程(浏览器定时触发线程)里面监听计时器是否过期,等到计时器过期后,会将对应回调callback1
加入事件回调队列中。
等到第一步中的微任务执行完毕之后,会判断事件回调队列是否为空。如果不为空,则会取出并执行最先进入队列的回调函数,执行过程如同第一步
。如果为空,则会视情况进行等待或挂起主线程。 一句话总结:先执行一个宏任务,再执行这个宏任务产生的对应微任务,执行完毕后,再执行后面的宏任务,以此往复。
宏任务、微任务
宏任务macro-task
Script整体代码、setTimeout、setInterval、I/O操作、UI rendering
微任务micro-task
new Promise().then中的内容、MutationObserve(前端的回溯) 了解完了宏任务与微任务的分类和js执行宏任务与微任务的顺序,我们再来看开头的那个例子
console.log(1)setTimeout(()=>{console.log(2);},0);constpromise=newPromise((resolve,reject)=>{console.log(3);resolve();});promise.then(()=>{console.log(4);});console.log(5);
第一次宏任务(整体代码):输出1,遇到setTimeout加入到宏任务队列(等待执行),遇到promise正常输出3,遇到.then()加入到微任务队列,输出5,本次宏任务执行完毕,共输出1 3 5 第一次宏任务执行完毕,清空这个宏任务产生的微任务队列,输出4 检查宏任务队列,发现还有一个宏任务setTimeout,执行该任务,输出2 最终结果是1 3 5 4 2
趁热打铁
js 执行顺序:先执行一个宏任务,再执行这个宏任务产生的对应微任务,执行完毕后,再执行后面的宏任务,以此往复。
functionfunc1(){console.log('func1');Promise.resolve().then(()=>{console.log('microtask.promise1');});}functionfunc2(){console.log('func2');Promise.resolve().then(()=>{console.log('microtask.promise2');});}functionmain(){func1();func2();setTimeout(func1,0);setTimeout(func2,0);}main()
输出结果如下:
//第一次宏任务(整体代码)执行func1func2microtask.promise1microtask.promise2//第二次宏任务(setTimeout(func1,0))func1microtask.promise1//第三次宏任务(setTimeout(func2,0))func2microtask.promise2
是不是已经完全懂了,你要相信你是最棒的。