资讯专栏INFORMATION COLUMN

JavaScript执行机制、事件循环

rose / 3471人阅读

摘要:曾经的理解首先,是单线程语言,也就意味着同一个时间只能做一件事,那么为什么不是多线程呢这样还能提高效率啊假定同时有两个线程,一个线程在某个节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢所以,设计者把

Event Loop曾经的理解

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

为什么JavaScript不是多线程呢?这样还能提高效率啊

假定JS同时有两个线程,一个线程在某个DOM节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢?

所以,设计者把JS设计成单线程应该就很好理解了,为了避免类似上述操作的复杂性,这一特征将来也不会变。

但是单线程有一个问题:一旦这个线程被阻塞就无法继续工作了,这肯定是不行的

由于异步编程可以实现“非阻塞”的调用效果,引入异步编程自然就是顺理成章的事情了,那么

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里通常把任务分为“同步任务”和“异步任务”,它们有以下的执行顺序:

判断任务是同步的还是异步的,如果是同步任务就进入主线程执行栈中,如果是异步任务就进入Event Table并注册函数,当满足触发条件后,进入Event Queue

只有等到主线程的同步任务执行完后,才会去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执行机制:

取且仅取一个宏任务来执行(第一个宏任务就是script任务)。执行过程中判断是同步还是异步任务,如果是同步任务就进入主线程执行栈中,如果是异步任务就进入异步处理模块,这些异步处理模块的任务当满足触发条件后,进入任务队列,进入任务队列后,按照宏任务和微任务进行划分,划分完毕后,执行下一步。

如果微任务队列不为空,则依次取出微任务来执行,直到微任务队列为空(即当前loop所有微任务执行完),执行下一步。

进入下一轮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[阮一峰]

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92709.html

相关文章

  • JavaScript运行机制事件循环

    摘要:主线程不断重复上面的三步,此过程也就是常说的事件循环。所以主线程代码执行时间过长,会阻塞事件循环的执行。参考资料这一次,彻底弄懂执行机制任务队列的顺序机制事件循环搞懂异步事件轮询与中的事件循环 1. 说明 读过本文章后,您能知道: JavaScript代码在浏览器中的执行机制和事件循环 面试中经常遇到的代码输出顺序问题 首先通过一段代码来验证你是否了解代码输出顺序,如果你不知道输出...

    Ververica 评论0 收藏0
  • JavaScript 运行机制详解(理解同步、异步和事件循环)

    摘要:从异步过程的角度看,函数就是异步过程的发起函数,事件监听函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。 1.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。JavaScrip...

    loonggg 评论0 收藏0
  • Javascript系列之javascript机制

    摘要:异步任务必须指定回调函数,当异步任务从任务队列回到执行栈,回调函数就会执行。事件循环主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为。事件循环事件循环是指主线程重复从消息队列中取消息执行的过程。 参考链接:这一次,彻底弄懂 JavaScript 执行机制https://zhuanlan.zhihu.com/p/...从浏览器多进程到JS单线程,JS运行机制...

    13651657101 评论0 收藏0
  • JS浏览器事件循环机制

    摘要:事件循环机制事件循环机制分为浏览器和事件循环机制,两者的实现技术不一样,浏览器是中定义的规范,是由库实现。整个事件循环完成之后,会去检测微任务的任务队列中是否存在任务,存在就执行。 文章来自我的 github 博客,包括技术输出和学习笔记,欢迎star。 先来明白些概念性内容。 进程、线程 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。 线...

    zebrayoung 评论0 收藏0
  • 浏览器中的事件循环机制

    摘要:单线程的话,如果我们做一些的操作比如说这是一个耗时的操所那么在这将近一秒内,线程就会被阻塞,无法继续执行下面的任务。事件循环的主要机制就是任务队列机制一个事件循环有一个或者多个任务队列。 浏览器中的事件循环机制 网上一搜事件循环, 很多文章标题的前面会加上 JavaScript, 但是我觉得事件循环机制跟 JavaScript 没什么关系, JavaScript 只是一门解释型语言, ...

    zzbo 评论0 收藏0
  • 这一次,彻底弄懂 JavaScript 执行机制

    摘要:事件完成,回调函数进入。主线程从读取回调函数并执行。终于执行完了,终于从进入了主线程执行。遇到,立即执行。宏任务微任务第三轮事件循环宏任务执行结束,执行两个微任务和。事件循环事件循环是实现异步的一种方法,也是的执行机制。 本文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我。不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作...

    dreambei 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<