JavaScript执行机制、事件循环

Event Loop曾经的理解

首先,JS是单线程语言,也就意味着同一个时间只能作一件事,那么html

  • 为何JavaScript不是多线程呢?这样还能提升效率啊
假定JS同时有两个线程,一个线程在某个DOM节点上编辑了内容,而另外一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪一个操做呢?

因此,设计者把JS设计成单线程应该就很好理解了,为了不相似上述操做的复杂性,这一特征未来也不会变。编程

可是单线程有一个问题:一旦这个线程被阻塞就没法继续工做了,这确定是不行的segmentfault

因为异步编程能够实现“非阻塞”的调用效果,引入异步编程天然就是瓜熟蒂落的事情了,那么promise

  • JS单线程如何实现异步的呢?

今天的主咖登场——事件循环(Event Loop),JS异步是经过的事件循环实现的,理解了Event Loop机制,就理解
了JS的执行机制。浏览器

先来段代码:多线程

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

for(let i = 3; i < 10000; i++){
    console.log(i)
}
执行结果:1 3 4 5 6 7 ... 9997 9998 9999 2

setTimeout里的函数并无当即执行,咱们都知道这部分叫异步处理模块,延迟了一段时间,知足必定条件后才执行并发

仔细想一想,咱们在JS里一般把任务分为“同步任务”和“异步任务”,它们有如下的执行顺序:异步

  1. 判断任务是同步的仍是异步的,若是是同步任务就进入主线程执行栈中,若是是异步任务就进入Event Table并注册函数,当知足触发条件后,进入Event Queue
  2. 只有等到主线程的同步任务执行完后,才会去Event Queue中查找是否有可执行的异步任务,若有,则进入主线程执行

以上两步循环执行,就是所谓的Event Loop,因此上述代码里:异步编程

console.log(1) 是同步任务,进入主线程,当即执行
setTimeout 是异步任务,进入Event Table,0ms后(实际时间可能有出入,见注文)进入Event Queue,等待进入主线程
for 是同步任务,进入主线程,当即执行
全部主线程任务执行完后,setTimeout从Event Queue进入主线程执行
*注:HTML5规范规定最小延迟时间不能小于4ms,即x若是小于4,会被当作4来处理。 不过不一样浏览器的实现不同,好比,Chrome能够设置1ms,IE11/Edge是4ms

这就是我以前对Event Loop的理解,可是自从看了这篇文章深刻理解JS引擎的执行机制颠覆了我对Event Loop认识三观,看下面的代码函数

Event Loop如今的理解

console.log("start")
setTimeout(()=>{
    console.log("setTimeout")
}, 0)
new Promise((resolve)=>{
    console.log("promise")
    resolve()
}).then(()=>{
    console.log("then")
})
console.log("end")

尝试按照咱们上面的JS执行机制去分析:

console.log("start")是同步任务,进入主线程,当即执行 setTimeout是异步任务,进入Event
Table,知足触发条件后进入Event Queue
new Promise是同步任务,进入主线程,当即执行
.then是异步任务,进入Event Table,知足触发条件后进入Event Queue,排在Event Queue队尾 console.log("end")是同步任务,进入主线程,当即执行

因此执行结果是:start > promise > end > setTimeout > then

But可是,亲自跑了代码结果倒是:start > promise > end > then > setTimeout

对比结果发现,难道Event Queue里面的顺序不是队列的先进先出的顺序吗?仍是这块执行时有什么改变,事实就是,前面按照同步和异步任务划分的方式并不许确,那么怎么划分才是准确的呢,先看图(转自谷雨JavaScript 异步、栈、事件循环、任务队列):

图片描述

咣咣咣~敲黑板,知识点,知识点,知识点:

Js 中,有两类任务队列: 宏任务队列(macro tasks)和 微任务队列(micro tasks)

宏任务队列能够有多个,微任务队列只有一个。那么什么任务,会分到哪一个队列呢?

宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
微任务:process.nextTick, Promise的then或catch, Object.observer, MutationObserver.

那么结合上面的流程图和最初理解的执行机制,总结了一下更为准确的JS执行机制:

  1. 取且仅取一个宏任务来执行(第一个宏任务就是script任务)。执行过程当中判断是同步仍是异步任务,若是是同步任务就进入主线程执行栈中,若是是异步任务就进入异步处理模块,这些异步处理模块的任务当知足触发条件后,进入任务队列,进入任务队列后,按照宏任务和微任务进行划分,划分完毕后,执行下一步。
  2. 若是微任务队列不为空,则依次取出微任务来执行,直到微任务队列为空(即当前loop全部微任务执行完),执行下一步。
  3. 进入下一轮loop或更新UI渲染。

Event Loop就是循环执行上面三步,接下来使用上面的结论分析个例子帮助理解

  • 微任务里嵌套宏任务
console.log('第一轮');

setTimeout(() => {                    //为了便于叙述时区分,标记为 setTimeout1
    console.log('第二轮');
    Promise.resolve().then(() => {    //为了便于叙述时区分,标记为 then1
        console.log('A');
    })
}, 0);

setTimeout(() => {                    //为了便于叙述时区分,标记为 setTimeout2
    console.log('第三轮');
    console.log('B');
}, 0);

new Promise((resolve)=>{              //为了便于叙述时区分,标记为 Promise1
    console.log("C")
    resolve()
}).then(() => {                       //为了便于叙述时区分,标记为 then2
    Promise.resolve().then(() => {    //为了便于叙述时区分,标记为 then3
        console.log("D")
        setTimeout(() => {            //为了便于叙述时区分,标记为 setTimeout3
            console.log('第四轮');
            console.log('E');
        }, 0);
    });
});

执行结果:第一轮 > C > D > 第二轮 > A > 第三轮 > B > 第四轮 > E

分析:

loop1:
第一步:首先执行全局宏任务,里面同步任务有下面两个,都当即进入主线程执行完后出栈

1.console.log('第一轮')
2.Promise1

输出 “第一轮” > “C”

异步任务有三个,分别进入相应的任务队列:

1.setTimeout1,该任务按照划分标准是 宏任务

setTimeout(() => {
    console.log('第二轮');
    Promise.resolve().then(() => {
        console.log('A');
    })
}, 0);
2.setTimeout2,该任务按照划分标准是 宏任务

setTimeout(() => {
    console.log('第三轮');
    console.log('B');
}, 0);
3.then2,该任务按照划分标准是 微任务

.then(() => {
    Promise.resolve().then(() => {
        console.log("D")
        setTimeout(() => {
            console.log('第四轮');
            console.log('E');
        }, 0);
    });
});
因此此时宏任务队列为: setTimeout1,setTimeout2
微任务队列为: then2

第二步:loop1 微任务队列不为空,then2出队列并执行,而后这个微任务里的 then3继续进入微任务队列 ,setTimeout3进入宏任务队列队尾

那么此时微任务队列为: then3
宏任务队列为: setTimeout1,setTimeout2,setTimeout3

可是此时第二步并无完,由于微任务队列只要不为空,就一直执行当前loop的微任务,因此从微任务队列取出 then3 执行输出 “D”

此时微任务队列为:
宏任务队列为: setTimeout1,setTimeout2,setTimeout3

到目前为止,当前loop的微任务对列为空,进入下一个loop,输出状况是“第一轮” > “C” > “D”

loop2:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout1执行输出“第二轮”,并then1进入微任务队列

此时微任务队列为: then1
宏任务队列为: setTimeout2,setTimeout3

第二步:loop2 微任务队列不为空,则从微任务队列取出then1执行,输出“A”

此时微任务队列为:
宏任务队列为: setTimeout2,setTimeout3

到目前为止,当前loop的微任务对列为空,进入下一个loop,输出状况是“第一轮” > “C” > “D” > “第二轮” > “A”

loop3:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout2执行输出“第三轮” > “B”

此时微任务队列为:
宏任务队列为: setTimeout3

第二步:因为loop3 微任务队列为空,则直接进入下一轮loop,输出状况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B”

loop4:
第一步:在宏任务队列队首里取出一个任务执行,即setTimeout3执行输出“第四轮” > “E”

此时微任务队列为:
宏任务队列为:

第二步:因为loop4 微任务队列为空,宏任务队列也为空,则这次Event Loop结束,最终输出状况是“第一轮” > “C” > “D” > “第二轮” > “A” > “第三轮” > “B” > “第四轮” > “E”

上面的整个过程就是更为准确的Event Loop,下面还有个不一样的例子供读者自行尝试

  • 宏任务里嵌套微任务
console.log('第一轮');

setTimeout(() => {
    console.log('第二轮');
    Promise.resolve().then(() => {
        console.log('A');
    })
}, 0);

setTimeout(() => {
    console.log('第三轮');
    console.log('B');
}, 0);

new Promise((resolve) => {
    console.log("C")
    resolve()
}).then(() => {                        //注意,这个函数改动啦
    setTimeout(() => {
        console.log('第四轮');
        console.log('E');
        Promise.resolve().then(() => {
            console.log("D")
        });
    }, 0);
});

执行结果:第一轮 > C > 第二轮 > A > 第三轮 > B > 第四轮 > E > D

Links:

深刻理解JS引擎的执行机制
JavaScript 异步、栈、事件循环、任务队列
JavaScript 运行机制详解:深刻理解Event Loop
JavaScript:并发模型与Event Loop
JavaScript 运行机制详解:再谈Event Loop[阮一峰]

相关文章
相关标签/搜索