浏览器EventLoop攻略 (保姆级教学,深入原理剖析)

997 阅读9分钟




1 浏览器多线程

我们先说浏览器,它包含多个线程,负责编译代码、UI渲染、网络处理、读取文件等行为

2 JS 主线程

JS 引擎

如果 JavaScript 是图纸,那 JS 引擎就是实现图纸的机器了

我来看看JS引擎的结构

  • 执行栈:JS 代码执行的地方,当引擎遇到函数调用之类的可执行代码块时,就会把它们推入执行栈

  • 内存堆:内存分配发生的地方,遇到变量声明和函数声明时,会把它们实体储存在里面

主线程

JS单线程,指的就是JS引擎中,解析执行JS的执行栈是唯一的,所有JS代码都只能在这个执行栈里按顺序执行。

JS引擎这个单栈解析代码的线程,我们称之为主线程

3 JS 运行环境

JS运行

我原以为的耿直 boy

JS 单线程意味着所有任务都需要排队,前一个任务结束,才会执行下一个任务。如果前一个任务耗时很长,后一个任务

就不得不一直等待 ...

其实很狡猾

如果是因为计算量太大,浏览器CPU忙不过来就算了,可是大多时候CPU都很闲。如果遇到类似I/O操作(读取文件很慢),

就不得不等到执行完了才能继续往下执行。

这时候JavaScript 设计者大大意识到,这时候主线程完全不管I/O操作,反正CPU压力点都不大,完全可以把它放一边

挂起,让CPU专门用个线程去执行,继续执行后面的任务。等它执行完成,先把它返回的回调存起来,等闲了再把挂起的

任务继续执行。

于是大大把 JS 任务分成了两种:

  • 同步任务:直接在主线程执行的任务,按顺序从前往后执行,如 script

  • 异步任务:耗时复杂的操作,主线程遇到会先挂起,丢给 Web API 找到对应线程去处理,等主线程空闲,再把它执行完的回调拿回主线程执行,如 setTimeout、I/O、Promise ...

JS运行环境

所以 JS 的实际运行环境就有点复杂了,像下面这样子

按处理方式分类 上面作者大大按对不同 JS 任务的处理方式,分成了同步和异步任务。

按调用逻辑分类 然后大大根据 JS 的运行环境调用逻辑,又重新把任务分成两类:

我们再来看JS运行环境的结构

  • JS 引擎:用来执行 JS 代码

  • Web API:包含各种辅助线程用来处理各种异步任务

  • callback queue:回调队列,遵循先进先出原则,按分类分为宏任务、微任务队列,分别用来存放它们的异步任务执行完的回调

  • Event Loop:JS运行环境的大脑,为防止主线程阻塞,判断什么时候去调什么任务去主线程的判断逻辑

4 浏览器 Event Loop

终于说到事件环了,说白了就是协调任务(微任务、宏任务)执行循序的判断逻辑。

事件环全名事件循环,从循环可知我们的判断逻辑不只判断一次,而是一直循环检查,每次循环操作成为一次tick,每次tick的处理是比较复杂的。

关键步骤

  • 执行一个宏任务,执行栈中有就直接执行(没错!说的就是script,程序最开始执行的就是script宏任务),没有就去宏任务队取

  • 执行过程如果遇到微任务,就把它加进微任务队列

  • 渲染完成后,执行微任务队列中所有微任务回调,直至清空

  • 微任务清空后,GUI线程接管控制器渲染页面

  • 下一轮循环 ...

流程图

是否初看的有点懵?那让我们生动形象的理解下!

假如

  • 整个JS - 程序是一个项目
  • 宏任务 - 领导给我们的一次开发迭代任务,给我们布置相关任务和时间,要我们按时完成
  • 微任务 - 每次版本迭代要修复的紧急BUG,要求尽快完成,每次迭代都要清除当前所有BUG
  • 宏任务队列 - 版本迭代的排期
  • 微任务队列 - bug列表(禅道!)

  • 初始版本很紧急,没有排期,要直接开发上线(js是解释型语言,script被直接编译执行),如果开发中碰到一些耗时需求,就先放进排期,初始开发版本完成(script执行结束)

  • 解决掉当前禅道里所有BUG,紧急上线(清空当前微任务,第一次循环结束)

  • 然后从排期取出第一个进行版本迭代(从宏任务队列取出第一个宏任务,执行)

  • 开完发成后同样要清空当前所有BUG,然后上线(宏任务完成后,同样清空当前所有微任务,第二次循环结束)

  • 然后继续下版本迭代...(下一轮循环)

  • 直至排期上再没有任务,项目稳定,结束迭代(直到宏任务队列清空)

这个例子够生动形象了吗?

接下来我们说几个小知识点

  • 执行完一个宏任务,必须清空当前所有的微任务(包括执行此次宏任务时新生成的,微任务队列全部清空),才能执行下一个宏任务

  • 页面渲染在宏任务执行完,清空微任务运行后:宏任务 - 微任务 - 渲染

  • GUI渲染线程和主线程冲突,不能同时执行JS又渲染,渲染时会把控制权交给GUI

  • 优先级我们都是说微任务高于宏任务,因为都是去判断异步任务的优先级:

    script(第一个宏任务直接同步执行) - 微任务 - 宏任务 - 微任务 - ...
    

课后作业开始啦!

先来个简单的

console.log(1)

setTimeout(() => {
  console.log(2)
  Promise.resolve().then(() => {
    console.log(3)
  })
}, 100);

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then(data => {
  console.log(data)
})

setTimeout(() => {
  console.log(6)
}, 50);

console.log(7)









看看你对了吗?

   1  4  7  5  6  2  3  

运行流程:

  • 第一轮:执行script - 打印1 - 第一个setTimeout 入宏队列 - 打印4 - promise 入微队列 - 第二个setTimeout入宏队列 - 打印7 - 宏任务结束开始清空微任务 - 执行promise回调 打印5 - 微任务清空
1  4  7  5  
  • 第二轮: 取出第二个setTimeout宏任务执行 - 打印6 - 微任务为空
6  
  • 第三轮:取出第一个setTimeout宏任务执行 - 打印2 - promise入微队列 - 宏任务结束开始清空微任务 - 执行promise回调 打印3 - 微任务清空
2 3  



再来个升级版

console.log(1)

setTimeout(() => {
  console.log(2)
  Promise.resolve().then(() => {
    console.log(3)
  })
}, 0);

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then(data => {
  console.log(data)
  console.log(6)
}).then(() => {
  console.log(7)

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

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

console.log(10)









这个对了吗?

   1  4  10  5  6  7  2  3  9  8




5 总结

剖析浏览器事件环让我们学习了JS在浏览器的运行流程,对JS的单线程运行知道的更加透彻。

在我自己对浏览器事件环的学习中,遇到了几个不清晰的点,我在网上没查到满意的答案,所以我总结后写出了自己的理解, 如果有误麻烦留言给我说下,谢谢

两个问题

第一个

事件环流程是 宏任务队列取出一个宏任务 - 清空所有微任务 - 循环 ,script 不是从宏任务队列取出,怎么算宏任务,它怎么入栈的?

JS是解释型语言,编译过程是先生成语法树存在内存,然后直接入执行栈边解释边执行,它没进过宏任务队列

一个宏任务我的理解是一次任务,类似我举例说的一次版本迭代,它主要做的是新版版的需求开发,同时包含清空当前所有BUG(清空当前微任务),它们都做完了才是一次版本迭代(一次循环)。

之所以script不在宏任务队列里,是因为异步任务执行完后的回调总要有个地方暂存(任务队列),而script是同步不需要暂存

第二个

我们都说微任务的优先级是高于宏任务的,可解说里是以先宏任务执行完再执行微任务这样的顺序,到底顺序是怎样的?

这个我刚开始看事件环的时候也是满脑疑惑,不是微任务高于宏任务吗?

怎么我一看运行流程是script宏任务先执行,然后微任务,这样循环呢?

这我也查了好多文章都说的摸棱两可,后面我把应用场景放进去再推论,给出我觉得合理的解释。

事件环实际运行是 宏任务 - 微任务 的顺序,JS运行就是这顺序。

其实我们说的 微任务 - 宏任务 的顺序是对异步而言的,因为script是第一个宏任务,同时它也是同步的。

而我们要判断的都是判断异步的执行顺序(scprit同步当然最先执行呐,这直接忽略掉)

所以我们去默认屏蔽掉script,然后从异步执行的顺序去看:script - 微任务 - 宏任务 - 微任务