深入理解javascript事件循环机制

本文首发于个人博客 https://maclaren0920.github.io

为什么要写这篇文章

在目前的技术社区上已经有大量介绍javascript事件循环的文章,并且也有一些写的非常不错的。但是他们大多都是在基于js层面来分析事件循环机制,很少有基于浏览器的运行流程来分析的,这就造就了很多对事件循环不甚了解的同学在实际开发中并不懂得怎样去正确的使用事件循环的特性,所以就有了现在这篇文章,<font color='red'>本文将从javascript在浏览器中的运行流程来分析事件循环的运行机制</font>。

javascript事件循环

我们都知道javascript是一门单线程的语言,所有的代码都是按顺序执行的,前面的代码没有执行完后面的会一直等待中,这就会造成程序的阻塞。

先看下以下代码:

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

Promise.resolve().then(() => {
  console.log(2);
});

console.log(3);

按照代码执行顺序应该是打印: 1、2、3,
然而实际却是 3、2、1,
这完全不符合代码按顺序执行的逻辑呀!

基于以上代码执行的顺序,我们有必要理解以下概念:

  • js代码分同步任务和异步任务
  • js实现异步的方式是基于事件循环模型

javascript是一门单线程的语言,按照直觉代码就是一行一行执行的,这就是同步任务,没错,以上你认为不符合直觉的代码执行顺序,这其中就掺杂了异步任务。

我们先来理解下同步任务和异步任务:

  • 同步任务
    当我们早上早高峰去做地铁时,人比较多,人们是按顺序一个一个排队进入地铁,如果前面的人没有往前走,后面的人就必须一直等着。
  • 异步任务
    当前面排队的比较多,队伍走的比较慢时,你闲着也是闲着反正又走不动,于是你拿出手机打开微信给朋友发了条消息,然后打开微信公众号看了会文章。

理解以上概念我们再来回头看下之前的代码,很显然setTimeout和Promise是属于异步任务的行列。那么同样是异步任务为什么2在1之前打印呢?

微任务和宏任务

除了同步任务和异步任务的区分之外,异步任务还有更精确的区分:

  • 微任务(micro-task) job
  • 宏任务(macro-task) task

同步任务和异步任务都是由js引擎来调度管理的,在这其中维护了一组任务队列(Event Queue);当执行到setTimeout时会将回调放入到<font color='red'>宏任务队列</font>,当执行到Promise then方法时会将会回调放入到<font color='red'>微任务队列</font>,当同步任务执行完成之后,就会去任务队列中的读取异步任务拿出来放到主线程中依次执行,首先会将微任务队列清空,然后再读取宏任务队列。

到这里你应该应该清楚上述代码的执行顺序的原因,但是这只是基于代码层面的,实际开发中往往更加复杂,异步任务就只有微任务和宏任务吗?

思考下一下代码:

document.body.style.background = 'black';

Promise.resolve().then(() => {
 document.body.style.background = 'red';
}).then(() => {
  document.body.style.background = 'green';
});


requestAnimationFrame(() => {
  document.body.style.background = 'orange';
});

setTimeout(() => {
  document.body.style.background = 'blue';
}, 200);

以上代码依次将body背景色改色,在变成橙色的一瞬间,最终变成了蓝色,你可以将其copy到控制台执行试试看效果。

结合上述讲解最终变成蓝色是没问题的,但是为什么会先变成橙色再变成蓝色呢,在这之前的黑色、红色和绿色呢?

从浏览器渲染顺序看异步执行机制

以上代码的运行结果在这里需要结合浏览器的渲染顺序来理解它。

我们来分析一下:
首先将body背景色变成黑色,然后遇到Promise then方法,依次将body背景色改成红色、绿色,我们之前提到Promise then方法属于微任务,该任务会在宏任务执行之前被全部清空,然后是执行requestAnimationFrame方法将body背景色改成橙色,最后是setTimeout宏任务将背景色改成蓝色。

这是代码的执行顺序,为什么之前的设置的背景色没有生效呢,只有requestAnimationFrame和setTimeout设置的生效了呢,很显然之前设置的被覆盖掉了。

结合同步任务和异步任务的讲解,我们知道同步代码先执行,首先设置背景色为黑色,然后清空微任务队列依次设置背景为红色、绿色,然后执行了requestAnimationFrame设置背景色为橙色,最后setTimeout将背景色变成蓝色。requestAnimationFrame是在setTimeout之前执行的,最后才算执行setTimeout,很显然requestAnimationFrame的执行时机比setTimeout更靠前,但是为什么会有先变成橙色再变成蓝色闪现的效果呢?

原因是在requestAnimationFrame和setTimeout执行顺序之间还穿插了GUI渲染操作,也就是我们经常说的浏览器绘制,当requestAnimationFrame执行完之后浏览器进行GUI渲染重新绘制页面,然后再执行setTimeout方法将背景色改成蓝色。

requestAnimationFrame

上述提到异步任务中分微任务和宏任务,那么requestAnimationFrame是什么东西呢,它是属于微任务还是宏任务呢?为什么它在setTimeout之前执行呢?我们可以在MDN看到关于requestAnimationFrame的描述:
window.requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

意思是说requestAnimationFrame是在浏览器绘制页面之前最后修改DOM元素的时机,文档中并未提到微任务和宏任务,这说明它并不属于这两者之间,它是独立于任务队列的,是由浏览器渲染进程来调度的,因为它独立于同步任务和异步任务,不存在同步异步阻塞的情况,所以一般实现动画效果使用它来实现比setTimeout更合适。

结合以上代码示例和讲解我们可以总结出javascript事件循环的执行顺序:

如上图所示,我将它分为两步,首先执行第一个宏任务,也就是script代码块,将script代码块中的同步任务放入主线程中执行,同步任务执行完成之后取出微任务中队列中的所有任务依次执行,然后执行requestAnimationFrame中的回调,其次是GUI渲染,最后执行setTimeout。第一步在脚本加载完成之后执行,其次不断循环第二步,这就是javascript事件循环的具体流程。

总结

结合上述讲解,最后结尾我们来总结一下:

  1. javascript是单线程语言,同步任务同步执行,异步任务执行异步执行
  2. 异步任务分微任务和宏任务
  3. requestAnimationFrame是独立于任务队列的,它是浏览重新绘制页面之前操作DOM的最后时机

参考

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
https://juejin.cn/post/6844903512845860872#heading-3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容