一道题彻底理清 Browser Context 的 Event Loop

文摘   科技   2022-09-23 14:56   中国香港  

  • 基本概念回顾

    • 1、执行和运行

    • 2、浏览器线程

    • 3、执行栈

  • Browser Context 的 Event Loop

    • Browser Context 的 Event Loop 解读

  • 一道题彻底理清 Event loop

    • 先来分析第一段代码

    • inner.click() 发生了什么

  • 结束语

本期作者:领创集团 ADVANCE.AI 前端工程师 胡晓晓 


众所周知,JavaScript 是一门单线程语言,同一时刻只能执行一个任务。同步任务都在主线程上执行,形成一个执行栈。遇到异步任务,如果未拿到结果一直等待的话,就会阻塞后续代码执行,这种结果是我们不想看到的。
那么是否存在这么一种机制:遇到异步代码,JavaScript 单线程运行时不会阻塞?
答案是肯定的:Event Loop 事件循环就是解决异步阻塞的一种机制。
 

基本概念回顾

在进入本文的整体之前,我们先来回顾一些基本的概念。

1、执行和运行

JavaScript 执行和运行是两个概念:
  1. 执行:依赖于环境,如浏览器、node 等等,在不同环境执行机制不尽相同。

  2. 运行:JavaScript 的解析引擎。

2、浏览器线程

浏览器内核是多线程的,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
  1. GUI 渲染线程

    1. 主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。

    2. 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。

    3. 该线程与 JavaScript 引擎线程互斥,当执行 JavaScript 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,JavaScript 引擎才会去执行 GUI 渲染。

  2. JavaScript 引擎线程

    1. 主要负责处理 JavaScript 脚本,执行代码。

    2. 也是主要负责执行准备好待执行的事件,即定时器计数结束或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS 引擎线程的执行。

    3. 该线程与 GUI 渲染线程互斥,当 JavaScript 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。

  3. 定时触发器线程

    1. 负责执行异步定时器一类的线程,如:setTimeout,setInterval。

    2. 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行。

  4. 事件触发线程

    1. 主要负责将准备好的事件交给 JavaScript 引擎线程执行。

    2. 比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行。

  5. 异步 http 请求线程

    1. 负责执行异步请求一类的线程,如:Promise,axios,ajax 等。

    2. 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JavaScript 引擎线程执行。

3、执行栈

JavaScript 执行栈又叫调用栈(LIFO),用来存储代码运行时创建的所有执行上下文。
-->当 JavaScript 引擎第一次遇到脚本 script 标签
    --> 创建一个全局的执行上下文并且压入当前执行栈 
        --> 当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文,压入栈的顶部
        --> 该函数执行结束时,执行上下文从栈中弹出
        --> 控制流程到达当前栈中的下一个上下文
        ... ...

Browser Context 的 Event Loop

众所周知,JavaScript 是一门单线程语言,有同步任务和异步任务之分。
异步任务回调函数是放在任务队列的。
任务队列有两种:宏任务(macro-task)队列和微任务( micro-task)队列。
  • 宏任务队列是一次只执行一个任务

  • 微任务是一次执行当前微任务队列中的所有任务,

常见的macro-task 如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 如: process.nextTick、new Promise().then(回调)、MutationObserver 等。

Browser Context 的 Event Loop 解读

  1. 一开始我们的执行栈为空,全局上下文(script标签)被推入执行栈,代码被执行。

  2. 代码执行过程中,会判断是同步任务还是异步任务,同步任务会被放入执行栈,立刻执行;异步任务会交给对应的异步处理模块去处理,异步处理完成后,会把异步任务的回调放入任务队列,当前任务执行完后出栈;

  3. 如果主线程任务全部执行完成,执行栈为空,接下来我们会检查微任务队列,如果队列不为空,会把微任务队列的回调放入主线程去执行,直到当前微任务队列被清空;

  4. 然后执行宏任务队列,宏任务队列是一次只执行一个任务;

  5. 3和4循环往复,直到两个队列都清空。



一道题彻底理清 Event loop

在 index.html 中有这么一段代码

<div class="outer">  <div class="inner"></div></div>
const outer = document.querySelector('.outer')const inner = document.querySelector('.inner')
new MutationObserver(function() { console.log('mutate')}).observe(outer, { attributes: true})
function onClick () { console.log('click')
setTimeout(() => { console.log('timeout') }, 0)
Promise.resolve().then(() => { console.log('promise') })
outer.setAttribute('data-random', Math.random())}
inner.addEventListener('click', onClick)outer.addEventListener('click', onClick)
当我们点击 inner 时,输出什么?
click
promise
mutate
click
promise
mutate
timeout
timeout
如果 JavaScript 代码中加一行 inner.click(),又会输出什么呢?

答案是:
click
click
promise
mutate
promise
timeout
timeout
 
为什么加了一行代码,输出结果就改变了呢?
上文提到了浏览器内核是多线程的。我们上面这段代码用到了 JavaScript 引擎线程、定时器触发线程以及事件触发线程。

先来分析第一段代码

JavaScript 是单线程,顺序执行代码,所以,当 script 标签里的代码执行完成后,执行栈为空:

如上图所示:执行栈 Call Stack 为空,同时浏览器内核中事件触发线程队列中放入了两个任务。
 
当我们点击 inner 时,会发生什么呢?
  1. 点击 inner 时,事件触发线程会把 inner click callback 放入任务队列;发生冒泡,事件触发线程会把outer click callback 放入任务队列;




2. 检查微任务队列, MicroTask 为空,继续下一步;

3. 从 MactoTask 取出一个宏任务,放入主线程


3.1 执行 onClick
遇到同步代码顺序执行,输出 `click`
执行完出栈

3.2 遇到 setTimeout 放入定时器触发线程,当 setTimeout 的 callback 触发时,会把setTimeout callback 放入 MacroTask 队列
3.3 Promise.then 和 MutationObserver 放入微任务队列;

3.4 当前任务处理完成,onClick 出栈

4. 然后继续下一轮的Event Loop(事件循环);此时检查微任务队列,微任务队列不为空
4.1 取出一个微任务,放入执行栈,执行代码,此时会输出 `promise `,执行完出栈

4.2 继续清空微任务队列,重复上一步步骤,此时会输出 `mutate` 。

5. 取出宏任务队列中的一个执行,步骤同3;


6. 继续下一轮事件循环,清空微任务队列,同步骤4;

7. Macro-tasks 任务队列取出一个任务,放入任务队列
  • 同步任务继续执行


  • 执行完成出栈


8. 又一轮event loop 开始,此时微任务队列为空;
9. 从宏任务队列取出一个任务执行,同步骤7,执行完成后出栈,此时宏任务队列为空。
最终输出结果如上图所示。


inner.click() 发生了什么

与上一段代码不同,执行 inner.click 之后,Call Stack 队列不为空:

1. 执行栈 inner 的click callback先被执行
  1. 首先是 console.log('click'),同步代码立刻执行,输出`click`

  2. 执行到 setTimeout,异步代码,交给定时器触发线程去处理,处理完成后callback放入MacroTasks

  3. 执行到 Promise.then 异步代码,放入微任务队列 MicroTasks ;

  4. 执行到 MutationObserver 异步代码,放入微任务队列 MicroTasks ;

  5.  inner 的click callback代码执行完成,出栈
2.主线程执行栈不为空,继续执行Call Stack的下一个方法, outer 的click callback 方法被执行,同1的步骤,outer 的callback执行中有一点需要注意,微任务队列中最多只能有一个MutationObserver,所以执行完成后结果如下:

3. 此时 inner.click 代码执行完毕,出栈
4.  检查微任务队列,微任务队列不为空
4.1 取出一个微任务,放入执行栈,执行代码,此时会输出 `promise `,执行完出栈
4.2 继续清空微任务队列,重复上一步步骤,一直到清空微任务队列,此时会依次输出 mutaion promise
5. 宏任务队列不为空,取出第一个任务放入执行栈执行,此时输出 timeout
6. 重复4和5 进入下一次事件循环,此时微任务队列为空;
7. 宏任务队列不为空,取出第一个任务放入执行栈,同5
8. 继续重复4和5 ,直到宏任务队列和微任务队列都为空。

结束语

本文旨在帮助大家理解浏览器的 Event Loop ,以图文的形式呈现给大家,所以图会比较多,如有不同见解,欢迎大家一起探讨。


另外:领创集团知乎官方账号已开通!
每周一个技术干货分享!
期待你的关注




关于领创集团

(Advance Intelligence Group)
领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。
领创集团Advance Group
领创集团是亚太地区AI技术驱动的科技集团。
 最新文章