1. 浏览器进程和线程
浏览器是多进程的,每当你打开一个 Tab ,都会有一个进程被系统创建出来。 这就是为什么你
会在任务管理器中看见多个 chrome.exe 的原因。
每一个 tab 都是一个浏览器『渲染进程』。 当然,除了一个( 或多个 )渲染进程,浏览器还有其它
的进程,例如 GPU 进程等。
渲染进程是浏览器最核心最重要的进程,而渲染进程下又有多个线程,其中与 JS 关系最大的
就是「JS 引擎线程」。
我们常说的「浏览器中的 JS 是单线程执行的」指的就是浏览器 ( 的一个 Tab 中 ) 有且仅有一
个线程执行你所编写的 JS 代码。
1. 浏览器
2. ├── 其它进程
3. ├── ...
4. ├── ...
5. ├── 渲染进程 1 // 一个 tab 一个进程
6. │ ├── JS 引擎线程
7. │ ├── HTTP 请求线程
8. │ ├── 定时触发线程
9. │ ├── 事件触发线程
10. │ └── GUI 线程
11. ├── 渲染进程 2 // 一个 tab 一个进程
12. │ ├── JS 引擎线程
13. │ ├── HTTP 请求线程
14. │ ├── 定时触发线程
15. │ ├── 事件触发线程
16. │ └── GUI 线程
17. ├── ...
18. └── 渲染进程 N // 一个 tab 一个进程
19. ├── JS 引擎线程
20. ├── HTTP 请求线程
21. ├── 定时触发线程
22. ├── 事件触发线程
23. └── GUI 线程
JS 引擎线程也被称作『主线程』。
2. 事件循环和执行栈
JS 引擎线程在解析一段 JS 代码时,会将代码放在某个地方,这个地方就被称作『执行栈』,
然后依次执行里面的函数 ( 代码 ) 。
JS 引擎线程 ( 即,主线程 ) 会执行 JS 代码,但并非所有的 JS 代码都是由 JS 引擎线程 ( 即,主线程 ) 执行!
当在依次执行这些代码过程中,如果遇到了异步代码 ( 例如,发起 ajax 请求、设置定时任务等
) 时,JS 引擎线程就会将这些代码交给其它线程执行 ( 而不是自己亲自、立刻执行它们 ) 。当 JS引擎线程把异步任务代码交给别人执行之后,执行栈中的执行流程就继续向下执行 ( 而不是在
异步任务代码处干等、白耗时间 ) 。
当执行栈中的同步代码执行完之后 ( 异步代码都交出去了 ) ,JS 引擎线程会从「一个队列」中
去取出已完成的异步任务的回调,将这个 ( 这些 ) 回调函数放到执行栈中继续执行。如果回调
中又有异步代码,这些异步代码还是会被 JS 引擎线程「交出去」。
例如前面例子中:
「发起 ajax 请求」代码会被交给「HTTP 请求线程」执行;
「设置定时任务」代码会被交给「定时触发线程」执行。
==这些异步任务代码,都不是由 JS 引擎线程执行的!==
被交给「其它线程」执行的异步任务在被「其它线程」执行完成后,它们 ( 异步任务 ) 的回调
函数会被放入『任务队列 ( 即,上面所说的「一个队列」 ) 』,而 JS 引擎线程在执行完同步代码之后,会从「任务队列」中取出这些已完成的异步任务的回调继续执行。
上述工作,JS 引擎线程是『周期性、循环』执行的!这个周期性的循环也被称作事件循环,
每一次循环其实就是一个时间周期,也被称之为一次 tick 。
3. 宏任务和微任务
上面所说的「任务队列」不止一个。根据任务种类的不同可以分为「微任务队列」和「宏任务
队列」。
在事件循环的过程中,执行栈在同步代码执行完成之后,优先会去检查「微任务栈」中是否有
任务 ( 代码 ) 需要执行,如果没有,再去「宏任务队列」中查看有没有需要执行的任务。如此
往复。口诀:同微宏。
微任务会比宏任务先执行,并且微任务只有一个队列。宏任务队列可能会有多个。
前面举例的 ajax 请求任务和定时器任务都是宏任务,另外,鼠标点击、键盘按下等事件也属于宏任务。
常见的微任务有 promise.then()、promise.catch()、new MutationObserver()、process.nextTick()等。
补充:从某种意义上讲,微任务不能算作严格意义上的异步任务。因为在底层实现上,V8 引
擎并不会将属于微任务的异步任务交给其它线程执行,仍然是由 JS 引擎线程执行:将它的回
调直接在执行完同步代码之后立刻直接执行。逻辑感官上,微任务好似改变了执行顺序的同步
代码。
宏任务特征:有明确的异步任务需要执行和回调,需要其它线程支持;
微任务特征:没有明确地任务需要执行,只有回调,不需要其它线程支持。
javascript
1. console.log('同步代码1');
2. setTimeout(() => {
3. console.log('setTimeout');
4. }, 0); // 这里的 0 ,实际上是默认 4 毫秒,它不是真正的 0
5. new Promise((resolve) => {
6. console.log('同步代码2');
7. resolve();
8. }).then(() => {
9. console.log('promise.then');
10. });
11. console.log('同步代码3');
- setTimeout 和 promise.then 都是异步执行的,将在所以的同步代码块之后执行;
- promise.then 是微任务,而 setTimeout 是宏任务,所以,promise.then 先执行;
- new promise 是同步执行的
4. 案例
案例一:在主线程上添加宏任务
javascript
1. // 执行结果:1 3 2
2.
3. console.log(1);
4.
5. setTimeout(function () {
6. console.log(2);
7. }, 0);
8.
9. console.log(3);
上述代码 3 条语句:
- 一个打印
- 一个定时器
- 一个打印
先执行第一个打印,输出 1 ,然后由于 setTimeout 是异步操作,因此它将被「其它线程」执
行,而它的回调 console.log(2) 会被放入宏队列。接着执行第二个打印,输出 3 ,最后主
线程再取出宏队列中的代码,执行 console.log(2) ,输出 2 。
所以最终的执行结果是 1 3 2 。
案例二:在主线程上添加微任务
javascript
1. // 最终执行结果:1 2 4 3
2.
3. console.log(1);
4. new Promise(function(resolve,reject) {
5. console.log('2');
6. resolve();
7. }).then(function() {
8. console.log(3);
9. })
10. console.log(4);
上述代码 4 条语句:
- 一个打印
- 一个 new promise
- 一个 promise.then
- 一个打印
先执行第一个打印,输出 1 ,然后执行 new Promise ,由于 new Promise 是同步代码,它的参数函数代码会立即执行,所以会执行 console.log(2) 输出 2 。而 promise.then 作为Promise 的回调,会被放入微队列。紧接着执行 console.log(4) 输出 4 ,最后主线程再取出微队列中的 console.log(3) 执行,输出 3 。
所以最终的执行结果是 1 2 4 3。
案例三:宏任务中创建微任务
javascript
1. // 最终结果:1 5 11 6 2 3 4 7 8 10 9
2. console.log('1');
3.
4. setTimeout(function () {
5. console.log('2');
6. new Promise(function (resolve) {
7. console.log('3');
8. resolve();
9. }).then(function () {
10. console.log('4')
11. })
12. }, 0);
13.
14. new Promise(function (resolve) {
15. console.log('5');
16. resolve();
17. }).then(function () {
18. console.log('6')
19. });
20.
21. setTimeout(function () {
22. console.log('7');
23. new Promise(function (resolve) {
24. console.log('8');
25. resolve();
26. }).then(function () {
27. console.log('9')
28. })
29. console.log('10')
30. }, 0);
31.
32. console.log('11');
- 首先会执行第一个打印,输出 1 ;
- 然后第 1 个 setTimeout 定时任务会被「其它线程」执行,其回调( new Promise() {}
)会被放入宏队列,而主线程继续向下;
- 接着执行第 1 个 new Promise 其代码会立即执行,因此会输出 5 。而它的 .then() 方法
的回调( console.log(6) )将会被放入微队列;
- 主线程继续向下,遇到第 2 个 setTimeout 定时任务,它又会被「其它线程」执行,其回
调( new Promise() {} )会被放入宏队列,而主线程继续向下;
- 主线程执行 console.log(11) 输出 11 。截止目前为止,输出的是 1 5 11 。然后,主线
程的代码全部执行完,主线程开始从微队列和宏队列中取代码执行。
- 现在微队列中有且仅有的代码是输出 5 的 new Promise 的回调: console.log(6) 。它
被主线程取出,执行。
- 微队列当前被清空了,主线程从宏队列中取代码执行。首先取出的是第 1 个定时任务的回
调,先输出 2 ,然后执行 new Promise 输出 3 ,然后 promise.then 的回调
console.log(4) 被放入微队列。
- 当前主线程代码又执行结束,然后微队列中有了一条语句,被取出执行,输出 4 。
- 主线程代码队列和微队列全空,主线从又从宏队列中取代码,执行第 2 个的回调,打印 7
和 8 ,将 promise.then 的回调 console.log(9) 扔进微队列后,继续打印 10 。
- 最后,主线程从微队列中取出 console.log(9) 执行,输出 9 。
案例四:微任务中创建宏任务
javascript
1. // 1 5 2 3 4
2. new Promise((resolve) => {
3. console.log("1")
4. resolve()
5. }).then(() => {
6. console.log("2")
7. setTimeout(() => {
8. console.log("3")
9. }, 0)
10. })
11.
12. setTimeout(() => {
13. console.log("4")
14. },1000);
15.
16. console.log("5")
- 首先 new Promise 执行,执行 console.log(1) 输出 1 ,而后 promise.then 被仍进微
队列。
- 紧接着下面的定时任务代码执行,其回调,会在 4 秒后被「其它线程」扔进宏队列。主线
程继续向下。
- 主线程执行 console.log(5) 输出 5 ,至此,主线程代码队列中全部执行完,主线程开始
从为队列中拿代码执行。
- 主线程取出 promise.then 的回调输出 2 ,然后遇到定时器,由于上面定时器间隔时间更
短,因此上面定时器的回调 console.log(3) 会比下面的定时器的回调 console.log(5)
先进入宏队列,因此,主线程先取到、执行的是输出 3 ,后执行的是输出 4 。