likes
comments
collection
share

js的事件循环机制

作者站长头像
站长
· 阅读数 10

前言

这篇博客,我想换个方式写写。

这是我《js核心基础》系列的一篇文章。

目前已经完成:

概论

js的执行是单线程的,但是随着计算机的发展,前端工程越来越庞杂。无脑地按顺序执行代码带来的用户体验非常差劲,伴随着前端的复杂化,js也在不断地优化自身。随着一代代版本迭代,事件循环机制越发完善。

本文将以一个初入js世界修仙宗门的修行小伙:张小凡的升职历程来描述这个演变过程。

一,初到异界,拜入V8宗

js设计之初,就是单线程的。

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?因为后者先执行的话,前者已经找不到节点了。

而在这辽阔的js世界之内,有个庞然大物雄踞其中:V8宗,所有的修行者梦寐以求的修行圣地。

我们的主角:张小凡,这一天来到这里,成为了一个低级杂役。

二,传法布道,初知阻塞

初入宗门,布道长老就给张小凡普及了基本的大道法则:

js是单线程,当我们执行js代码的时候,默认是按照顺序一行行执行。

如果有异步代码,它们的执行需要一定的时间,如果还是单单使用主线程,就会因为这些耗时长的操作而堵住,程序无法接着执行,这就是平时所说的阻塞

为了让后续代码接着执行,达到非阻塞的目的,就有了异步非阻塞的概念,在js中,异步操作的处理都是异步非阻塞

console.log("1111")
setTimeout(()=>{
    console.log("2222")
},1000)
console.log("3333")

如上代码的打印顺序是:

1111
3333
2222

遇到了异步代码,需要耗费一定的时间去处理。这时js并不会堵住,而是会接着执行后续的代码,也就是异步非阻塞。

实际上,在js中,只有同步阻塞异步非阻塞两种。

二,小试牛刀,初露锋芒

拜入宗门,张小凡不可避免地领取了任务:给宗门劈柴烧水。

我们把一个异步事件理解为烧开水。

而张小凡,境界低微,一次只能处理一件事情。

然而主角毕竟是主角,他会思考会复盘。

夜深人静,躺在床上,思忖起今日布道长老的话:

有一堆事情abc要做,还要烧一壶开水,但是烧开水需要一定的时间,为了把等待的这个时间利用起来,而不是我守在热水壶旁边啥也干不了,只能等水烧开。(同步阻塞)

于是我可以在热水壶上安装一个哨子,当水烧开就会发出声音,提醒我水烧开了。这烧水的期间,我就可以去做其他事情,只要听到哨声,把电关了就行。(异步非阻塞)

想至此,张小凡觉也不睡了,连夜赶工做了一大堆哨子。

从此,V8宗内的开水烧得又快又好,仙子们洗得又白又嫩。

张小凡的烧水业务得到了全宗上下的广泛好评。

然好景不长,没过几天,就因为哨声太吵被眼红者举报了!

三,返本溯源,崭露头角

张小凡早已不是初入江湖的毛头小子。

一个方案是方法论的体现,一套可行的方法论,可以演变出不同的可行方案。

那就可以返本溯源,将这套方案抽象一下:

照着这个思路,于是在主线程的执行栈之外,又有名为事件队列的一个队列,里面就存放着异步操作的结果(水壶哨声响了的事件,需要你去关电)。

当上面分配任务下来,主线程中的所有代码依次顺序执行,遇到同步则直接执行,遇到异步,则在Event Table中注册异步(类比于烧水事件中的把水壶的电插上)。这些操作无需花费多少时间的。

等到异步操作的结果出来了,就把回调函数注册到事件队列中去(类比于水壶的哨声响了,需要人去关电这一事件)。

另外js的事件执行有一个轮询的机制:等到主线程空了,就去事件队列中查找要执行的事情放入主线程,然后同步的执行,异步的注册,而注册的异步结果出来了放入事件队列中。主线程空了再去,空了再去……(这就是Event Loop事件循环)

setTimeout(function(){
    console.log('定时器第一个')
},100);
setTimeout(function(){
    console.log('定时器第二个')
},0);
console.log('代码执行结束')

如下示意图:

js的事件循环机制

对于张小凡而言,他也达成了自己升职的目的:当上了小组长,宗门给他新派了两个杂役,一个叫eventTable事件注册表,另一个叫事件队列。

eventTable事件注册表做的就是遇到异步函数注册异步事件(给水壶插上哨子)。

等时间表中的异步事件完成后,再把它按照完成顺序交接给事件队列。

事件队列比较有眼色,会察言观色,看张小凡空闲的时候,就给他汇报已完成的任务。

烧好的水壶持续汇总到张小凡这里(主线程处理器),他要做的仅仅是拔掉电源、上报宗门。于是功劳美美到手。

四,宏微任务,事件循环

正当张小凡沉浸在自己修为高深冠绝寰宇威压九天十地的美好幻想中。

执法长老找上门来:“紫霞仙子闭关多年,明日化神,需要沐浴更衣,需要烧上些开水兑以灵水备用。”

张小凡立马从老爷椅上弹起,点头哈腰连连称是。

咱处在宗门内,就得多为领导考虑嘛,领导关注的事情优先完成!必要时可以插队!

成不成另说,起码态度得表现出来不是?

于是张小凡将异步任务划分为宏任务和微任务(可以插队的任务)。

在js世界里,常见的微任务有:

Promises.(then catch finally),process.nextTick, MutationObserver

常见的宏任务有:

整体代码script,setTimeoutsetInterval ,setImmediate,I/O,UI renderingnew 

也就是说,遇到异步函操作,还需要判断是宏任务还是微任务,宏任务的话,就把异步操作的结果加入宏任务队列,微任务的话,就加入到微任务队列。

于是,异步用到的队列,就由原来的一个事件队列,变成了宏队列和微队列两个,而主线程空了的话,会先去微队列中查找(若在这个过程中,微队列的事件又产生的新的微任务加入队尾,也会在本次循环中进行处理,简而言是就是把每轮循环把微队列搞空),然后再去宏队列中查找(而每次宏任务只执行一个,就又轮到微任务队列)

这样一来,微任务就能插队啦。

张小凡越想越激动:“天不生我张小凡,js世界万古如长夜。”

接下来就是人员安排,注册异步事件的小伙之前表现不错,给他涨个几灵石俸禄。

事件队列呢,够机灵,就让他当个小组长,做轮询,依次从宏任务队列和微任务队列取功劳,啊不,取完成的事项给我汇报就好。

再给他俩物色个媳妇,督促买个山脚洞府。人嘛,一旦有了软肋,就更好拿捏些。

紧接着,张小凡就去掌门那边一阵哭穷,死乞白赖总算又要来两个杂役:一个穷苦出身的就当宏任务队列;一个是师兄凡间的子侄,就让他当微任务队列,离领导近点。

于是人员架构的示意图如下:

js的事件循环机制

这样一来,每一轮事件循环中,只要微任务队列没有被清空,都能一直插队,因为直到微任务队列清空后,才会去宏任务队列取一个宏任务进入主线程。

六,神功练成,威压寰宇

new Promise(function(resolve){
    console.log('1');
    resolve();
}).then(function(){
    console.log('2')
});
setTimeout(function(){
    console.log('3')
    new Promise(function(resolve){
        console.log('4');
        resolve();
    }).then(function(){
        console.log('下一轮事件循环:5')
    });
},0);
new Promise(function(resolve){
    console.log('6');
    resolve();
}).then(function(){
    console.log('插队的任务:7')
});
console.log('8');

代码分析:

执行Js代码由上至下顺序。
1new Promise属于主线程任务,直接打印1Promis下的then方法属于微任务,把then分到微任务 Event Queue中。
2,遇到setTimeout,把setTimeout分发到宏任务Event Queue中。
3new Promise属于主线程任务,直接打印6Promis下的then方法属于微任务,把then分到微任务 Event Queue中。
4console.log('8');属于主线程任务,直接执行打印8
5,主线程中任务执行完后,就要执行微任务Event Queue中代码,实行先进先出,所以依次打印27
6,微任务队列空了,就开始去宏任务队列中去完成的回调。依次打印34
7,执行完一个宏任务了,开始下一轮事件循环。从微任务队列中取,于是打印5
最终结果:1682,插队的任务:734,下一轮事件循环:5

从此,V8宗烧水业务蒸蒸日上,威名远扬。

张小凡又瘫在了老爷椅上,晃晃悠悠~