2021年11月更新

浏览器进程

以目前的多进程浏览器Chrome为例

  1. 浏览器进程(Browser进程)
  2. 渲染器进程
  3. GPU进程
  4. 网络进程
  5. 插件进程
  6. 缓存进程

其中渲染进程(Renderer)包含以下线程

  1. 主线程
  2. 合成器线程(Compositor)
  3. 光栅线程(Raster)
  4. 工作线程(Worker)


渲染进程在运行JS代码时,JS的引擎会维护一组代理,每个代理由一组执行上下文、执行上下文堆栈、一个主线程、一组用于处理工作线程的附加线程、一个任务队列和一个微任务队列组成;除了主线程之外,代理的每个组件对于该代理都是唯一的.


事件循环(Event Loop)

概括

每个代理都有一个事件驱循环驱动,用来收集任何用户和其他事件,将任务排入队列以处理每个回调。然后它运行任何挂起的 JavaScript 任务,然后是任何挂起的微任务,然后执行任何需要的渲染和绘制,然后再次循环以检查挂起的任务;

事件循环分为三种类型:窗口事件循环(Window event loop)工人事件循环(Worker event loop)Worklet事件循环(Worklet event loop)

这个过程中大概的步骤为:

  1. 出列并运行任务列队中最早的任务或上一轮入列的任务
  2. 执行所有的微任务
    • 当微任务不为空
    • 出列并运行最早的微任务
  3. 渲染更改
  4. 重复步骤1

任务

一个任务就是由执行诸如从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发之类的标准机制而被调度的任意 JavaScript 代码。这些都在 任务队列(task queue)上被调度

  • script (主代码块)
  • setTimeout
  • setInterval
  • setImmediate -
  • I/O 、UI rendering

微任务

与任务最大的区别在于他们将会在一个任务退出并且执行上下文为空时,一次执行微任务列队里的所有任务

  • process.nextTick(Nodejs)
  • promise
  • Object.observe
  • MutationObserver

小结

需要注意的是在执行来自任务列队中的任务时,在每一次新的事件循环开始迭代的时候都会执行列队里的每个任务,但是在执行过程中新加入到列队里的任务都要在下一次迭代开始之后才会执行,不同的是在这个程中微任务则会在一个任务退出且执行上下文为空的时候立即执行,意思是微任务会在下一个任务开始前且当前的事件循环结束之前执行所有的微任务(类似插队)

<script>
console.log(1)

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

setTimeout(() => {
console.log(3)

Promise.resolve().then(() => {
console.log('new Promise')
})

setTimeout(() => {
console.log('new setTimeout')
})

ocnsole.log(4)
}, 10);
</script>

<script>
console.log(5)

setTimeout(() => {
console.log(6)
})
</script>

上面的代码最终的打印结果是

1
2
5
6
3
4
new Promise
new setTimeout

  1. 事件循环开始时,两个<script>任务存在于任务列队里,开始第一轮的迭代,首先执行第一个任务<script>内的代码,console.log(1)压入调用栈执行并出栈;将Promise.resolve()压入调用栈执行并出栈,同时将Promise的回调函数加入微任务列队;setTimeout()压入调用栈执行并出栈,同时创建一个回调函数的任务(Tasks)加入任务列队用于下一次迭代;第一个<script>任务结束

  2. 此时调用栈为空,开始执行微任务列队中的所有任务,执行console.log(2);直到微任务列队为空;

  3. 开始执行第二个任务<script>内的代码,console.log(5)进入调用栈执行并出栈;setTimeout()压入调用栈执行并出栈,同时创建一个回调函数的任务(Tasks)加入任务列队用于下一个迭代循环执行,第二个任务结束;此时微任务列队为空,第一轮任务结束.

  4. 此时任务列队里有第一轮过程中生成的两个setTimeout()回调任务,第一个setTimeout()回调任务将在10毫秒后执行,第二个setTimeout()回调任务将在0毫秒后执行,所以此时将优先执行第二个setTimeout()回调任务;

  5. 执行第二个setTimeout()回调任务, console.log(6)压入调用栈执行并出栈;结束任务,此时微任务列队为空,开始执行下一个任务

  6. 执行第一个setTimeout()回调任务, console.log(3)压入调用栈执行并出栈,将Promise.resolve()压入调用栈执行并出栈,同时将Promise的回调函数加入微任务列队;之后执行setTimeout()并将回调函数加入到任务列队用于下一个迭代的执行;最后console.log(4)进入调用栈执行并出栈;任务结束

  7. 此时调用栈为空,开始执行微任务列队中的所有任务,执行console.log(new Promise);直到微任务列队为空;第二轮任务结束.

  8. 此时任务列队里有第二轮过程中生成的一个setTimeout()回调任务,console.log('new setTimeout')压入调用栈执行并出栈,结束任务;此时微任务列队为空,任务列队为空;

  9. 所有任务执行完毕


总结

  1. 事件循环开始时,第一轮的任务包含所有的<script>内的代码,一个<script>代表一个任务;
  2. 当前循环内创建的微任务,会在每一个任务结束后清空.当前循环内创建的任务,会在下一个迭代(下一轮)执行;
  3. 同一个循环内的settimeout()的回调任务根据参数时间确定执行顺序
  4. 连续Promise().then的回调微任务执行顺序这个涉及到promise源码的问题,这里不过多讲解;会单独写一篇文章详解

参考资料:

https://developers.google.com/web/updates/2018/09/inside-browser-part3?hl=en
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide