前言
一、浏览器架构
这里以程序员都爱用的Chrome为例,Chrome采用多进程架构,由多个进程组成,进程下包含多个线程。
Chrome中主要有四个主要进程。
简单介绍一下各个进程及主要线程:
1. 浏览器进程(Browser Process)
顶层进程,协调浏览器各进程工作,Tab外的工作由它负责
- UI thread:负责浏览器按钮、地址栏;
- network thread:负责处理网络请求;
- storage thread:控制文件访问;
2. 渲染器进程(Renderer Process)
浏览器内核,负责Tab内的所有工作
- Main: 构建dom树 -> 加载资源 -> js下载与执行 -> 样式计算 -> 构建布局树 -> 绘制 -> 创建层树。(注:Main不是一个线程,而是多个线程的集合,为了方便介绍先聚合一下,后面展开讲)
- Worker thread: Web Worker 运行在这个线程,可能存在多个
- Compositor thread: 合成器,将层合成帧,分成多个磁贴
- Raster thread: 栅格化磁贴后交给GPU
3. 插件控制进程(Plugin Process)
控制一个网页用到的所有插件
4. GPU进程(GPU Process)
处理GPU任务
大致工作流程
二、Main
还记得上一节遗留的线程集合Main吗?现在就展开介绍一下。
聚合的线程主要有以下这几个:
1. GUI渲染线程
负责渲染工作,包括解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
注:GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存等到JS引擎空闲时立即被执行。
2. JS引擎线程
JS引擎线程负责解析Javascript脚本,运行代码(比如Chrome的V8)。
一个Tab页内中无论什么时候都只有一个JS线程在运行JS。
因为GUI渲染线程与JS引擎线程是互斥的,所以当JS执行的时间过长,页面的渲染也会阻塞。
3. 事件触发线程
主要用来控制事件循环
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会被添加到事件线程中。 当对应的事件符合触发条件并被触发时,该线程会把事件添加到队列的队尾,等待JS引擎的处理
注:由于JS单线程的关系,所以这些队列中的事件都得等JS引擎空闲了才会被执行
4. 定时触发器线程
setInterval与setTimeout所在的线程
由于js引擎是单线程的,如果由js来计时会影响计时准确性,因此额外使用一个线程来计时并触发定时
5. 异步http请求线程
XMLHttpRequest连接后会新开一个线程。 将检测到状态变更时,如果设置有回调函数,该线程就产生状态变更事件。
当然,实际做请求工作的还是 浏览器器进程 下 network thread。
三、V8运行环境
通过前面的介绍,我们知道Chrome中的JavaScript在 V8 (Js引擎线程)上运行,并且是 单线程 的,那么V8中是如何运行Js的呢?
先了解一下V8运行环境
堆:记录内存分配,对象被分配在堆中
栈:调用栈,帧栈这类东西存放的地方
队列:待处理消息队列,每一个消息都关联着一个用以处理这个消息的回调函数
来看一个MDN中的例子
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
这一段代码中我们声明了两个函数,在最后一行调用了 console.log,console.log 随即被加入到栈顶,紧跟着调用了 bar , bar 也被加入到栈顶,在 bar 中又调用了 foo,所以foo也被加入到栈顶,现在的栈长这样:
之后开始执行,foo执行完毕返回42后弹出 调用栈,bar执行完毕将42返回并弹出 调用栈,最后是console.log,打印了42后也弹出调用栈,栈被清空。
OK,这就是基本的V8运行机制。
四、Event Loop
众所周知,JavaScript除了是 单线程 的,还是 非阻塞、异步 的,例如发起一个ajax请求,在结果还没返回的时候你依然可以继续执行后续的js,而不需要等待ajax返回结果。
直接上代码
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
这一段代码中,setTimeout就是一个典型的异步操作,按照js代码执行从上到下的原则,很自然推出结果是:1 2 3;然而,实际运行代码时,结果确是:1 3 2。
我的天,这是为啥?
在前面的介绍中,我们知道 setTimeout 并不是由 js引擎线程 负责计时,而是交给 定时触发器线程 负责计时,那怎么让 js引擎线程 到时间了执行setTimeout中的代码呢?
还记得 V8运行环境 中还有个 队列 没提到吗?在这里就派上用场了。
代码执行到 setTimeout 时,交由 定时触发器线程 开始计时,等到预定时间到了,事件触发线程 便会产生 类消息 放入到队列的末尾。
等到 调用栈 “清空”后,或者说 空闲 了,便会去检查队列中是否有消息,有的话便压入栈中,执行,出栈。
所以,setTimeout 回调函数的执行发生在了 console.log(3) 之后。
结论:
- 异步操作通常会绑定一个回调函数(事件监听器、定时器、ajax请求等),等到异步操作执行完毕有结果了,会被放在队列的队尾等待被执行(注:如果没有回调函数,有返回结果了也不会加到队列中)。
- 当执行栈中的同步任务执行完毕后(js引擎空闲了),就开始去读取队列。
- 从队列头部读取一个事件添加到栈中,开始执行,执行完毕后出栈,如果是 窗口事件循环 (这里不展开了,可点击链接了解) 还会执行一些必要的渲染和绘制。
- 再去读取任务队列,下一次循环开始。
截了一张Philip Roberts的演讲《Help, I'm stuck in an event-loop》 中的图帮助理解
注:必须等 执行栈 空闲了才会去读取队列
五、(宏?)任务与微任务
看到这里你们肯定会好奇,为什么这里 “宏” 要加个问号呢? 原因是其实官方英文文档、规范中并没有 “宏任务(macrotask)” 这个概念,也许是为了区分以及更好理解,官方文档 中文译者 加上了 “宏任务” 的概念,这里我就不对“宏任务”下定义了,只说一下自己的理解。
那么首先,任务是啥呢?
按照MDN的意思,任务 就是计划按标准机制执行的 任何 JavaScript,如:初始化、事件回调、调用setTimout或setInterval。
接下来就是争议比较大的 宏任务,有说 “除了微任务以外都是宏任务的”,也有说 “宏任务是异步任务中除了 微任务的其它任务”;这里我们先看一下官方文档中怎么说。
在MDN关于 事件循环 的段落中找到了中文译者加的“宏任务”,英文原文是:
译者翻译:
按照译者的思路,“宏任务” 指的便是 “any pending JavaScript tasks”,即 事件循环中所有等待执行的JavaScript任务;那什么任务才需要等待执行呢?显然是队列中的任务,所以我更倾向于 宏任务 指的是队列中的任务。
有不同见解的欢迎一起讨论,现在网上各种论调都有,尤其是一些公司的面试官,完全按照自己的理解来,愣是拿他们一点办法都没有╮(╯▽╰)╭。
而对于 微任务,官方并没有给出具体定义。
六、任务调度
多了宏任务与微任务以后,事件循环又该怎么运行?
这里就要涉及到两个队列了:任务队列、微任务队列
并不是所有 任务 都会放入 任务队列,需要满足以下至少一个条件:
- 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个
- 触发了一个事件,将其回调函数添加到任务队列时。
- 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。
而微任务队列,顾名思义,就是放置微任务的。
需要注意的是,任务队列(task queues) 是set,而不是queues,因为 event loop 第一步是获取第一个可运行的任务,而不是让第一个任务出队;
微任务队列(microtask queue)不是 任务队列(task queue)。
以下是官方对 事件循环 运行机制的描述:
这里只截取到 窗口事件循环的渲染,之后是谈论不同事件循环会如何处理。
简单概括就是:
- 先判断任务队列是否有可执行的任务,有就继续下一步,没有就跳到第3步。(文档第1步)
- 取队列第一个可执行任务,并执行,继续下一步。(文档2~6)
- 如果微任务队列不为空,依次执行完所有微任务,。(文档第7步,这里我结合 微任务检查点 相关知识展开了一下)
- 如果是 窗口事件循环 就执行一些渲染操作(文档第11步)
- ...
看到这可能就有疑问了,先执行任务队列的任务再执行微任务?怎么跟我代码运行的结果不一样啊?
不要着急,在规范中第一步后面紧跟着一个note:
“微任务队列”不是“任务队列”,因此第一步不会使用它,但是可以使用与微任务任务源关联的任务队列。
这个note就给了浏览器发挥的空间,Chrome这里就选择了先检查微任务队列。
调度情况大致如下:
在每次迭代开始之后加入到 任务队列 中的任务需要在下一次迭代开始之后才会被执行;而如果是加入新的微任务到 微任务队列 中,会在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
七、案例
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
这是头条的一道面试题,感兴趣的可以先不看答案自测一下。
输出结果为:
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeou
*/
我们来分析一下
- 一开始声明了 async1 async2 两个函数,紧接着执行了 console.log('script start') 所以第一个打印的就是 “script start”
- 之后是一个 setTimeout,交由 定时触发器线程 负责计时,这里给到的时间是0,于是等待4毫秒(小于4时会被自动设置为4)后被添加到 任务队列 中等待执行。
- 下一步调用 async1,async1函数内打印 “async1 start”,注意这里遇到了 await,首先调用 async2 打印 “async2” 后交出代码控制权也就是代码不在往下执行,而是继续执行 async1(); 之后的代码(这一部分后面出篇文章聊聊),并将 await async2();后的操作加入 微任务队列。
- async1(); 后是新建了个Promise对象,初始化Promise时打印“promise1” 并直接调用 resolve() 于是将then的回调加入 微任务队列 中。
- 继续往下执行,最后一行打印出“script end”。
- 先来看一下当前状态,调用栈空闲,任务队列中有一个setTimeout的回调,微任务队列中有 console.log('async1 end')、promise.then的回调。
- 调用栈空闲了,于是去执行 微任务队列 中的任务,随即打印输出 “async1 end” 与 “promise2”。
- 微任务队列中所有任务执行完毕后,执行 任务队列 中的任务即setTimeout的回调,输出 “setTimeout”。
后记
附录
常见宏任务
操作 | 浏览器 | Node | I/O | ✅ | ✅ | setTimeout | ✅ | ✅ | setInterval | ✅ | ✅ | setImmediate | ❌ | ✅ | requestAnimationFrame | ✅ | ❌ |
---|
常见微任务
操作 | 浏览器 | Node | process.nextTick | ❌ | ✅ | MutationObserver | ✅ | ❌ | Promise.then catch finally | ✅ | ✅ |
---|
参考:
- html.spec.whatwg.org/multipage/w…
- jakearchibald.com/2015/tasks-…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…
- www.jianshu.com/p/443e8ece3…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!