JavaScript中的线程

js为什么是单线程

JS单线程:同一个时间点只能执行一个任务(任务分类:宏任务和微任务);

JavaScript最初设计的执行环境是在浏览器端,需要操作DOM的变动。这一设计决定了JS要使用单线程。如果是多线程,一个线程正在操作DOM,另一个线程却正在删除DOM,就会造成冲突,浏览器无法决定该怎么执行。

如果执行同步问题使用多线程,执行任务造成非常繁琐。
⚠️注意:HTML5标准规定允许Javascript脚本创建多个线程,但是子线程完全受主线程操控,且不得操作DOM。

单线程的阻塞问题

单线程会出现阻塞问题,只有前一个任务完成之后才能执行下一个任务。这样会导致页面出现卡死状态,直接影响用户体验,出现了同步任务和异步任务的解决方案。

  • 同步任务:在主线程排队的任务,只能前一个执行完毕,才执行下一个任务
  • 异步任务:不进入主线程,而是进入“任务队列”的任务,只有“任务队列”通知主线程,该任务才会进入主线程执行。

    JS如何实现异步编程

    JavaScript异步任务执行,运行机制如下:
  1. 所有同步任务都在主线程执行,形成一个执行栈;
  2. 主线程之外,还存在一个“任务队列”,碰到了异步任务,就把该异步任务放置在“任务队列”中。
  3. 执行栈中的所有同步任务执行完毕,系统就会读取“任务队列”。“任务队列”里面对应的异步任务结束等待状态,进入执行栈,开始执行;
  4. 主线程会重复执行1,2,3步骤。

主线程读取任务队列的中事件,按照“先进先出”的数据结构,排在后面的事件,优先被主线程读取。执行栈只要为空,任务队列的第一个事件进入到主线程。如果存在定时任务,主线程需要定期检查定时器执行时间,到了时间才能进入主线程执行。

EventLoop事件循环机制

在深入事件循环机制之前,需要弄懂一下几个概念:

  • 执行上下文(Execution context)
  • 执行栈(Execution stack
  • 微任务(micro-task
  • 宏任务(macro-task

    执行上下文

    执行上下文是对代码执行环境的一层抽象。js的执行上下文分三种:全局执行上下文,函数执行上下文,Eval执行上下文。

  • 全局执行上下文:默认的基础上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局对象(浏览器环境Window,node环境global);设置this的值等于这个全局对象。一个程序中只能有一个全局执行上下文。

  • 函数执行上下文:每当一个函数被调用时,都会给该函数创建一个新的上下文。每个函数都有它独有的执行上下文,切记-函数执行上下文只是在函数被调用时创建。函数执行上下文可以有任意多个。
  • Eval函数执行上下文:执行在eval函数内部的代码,有属于它自己的执行上下文。

    执行栈

    执行栈是栈类型的数据结构,具有先进后出的特性。栈的特性使得代码在执行的时候,遇到执行上下文就将其依次压入执行栈。
    执行顺序,先执行栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕就会出栈,继续执行下一个位于栈顶的执行上下文。 ```javascript function foo() { console.log(“a”); bar(); console.log(“b”); }

function bar() { console.log(“c”); }

foo();

  1. - 初始化执行栈为空;
  2. - foo函数执行并进入执行栈,输出a,遇到函数bar
  3. - 执行bar再进入执行栈,开始执行bar函数,输出c
  4. - bar函数执行完毕出栈,继续执行此时位于栈顶的函数foo,输出b
  5. - foo执行完毕出栈,所有执行栈内任务执行完毕;
  6. <a name="XBg68"></a>
  7. ### 事件循环机制
  8. ![微信截图_20210123213128.png](https://cdn.nlark.com/yuque/0/2021/png/737887/1615123807916-69175416-4626-4f49-adda-ed23a7e1c6e8.png#align=left&display=inline&height=429&margin=%5Bobject%20Object%5D&name=%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20210123213128.png&originHeight=429&originWidth=757&size=172907&status=done&style=none&width=757)<br />![eventLoop.png](https://cdn.nlark.com/yuque/0/2021/png/737887/1615123828473-d20a74b9-10df-4d5c-9931-6a7641a4e361.png#align=left&display=inline&height=519&margin=%5Bobject%20Object%5D&name=eventLoop.png&originHeight=519&originWidth=835&size=193608&status=done&style=none&width=835)
  9. - 同步和异步任务分别进入不同场所,同步任务进入call stack执行栈,异步进入task queue队列。
  10. - 当主执行栈的代码执行完毕,call stack的任务为空时,会先执行Microtask queue(微任务后边详细介绍)队列中的任务,执行结束后会执行task queue中的事件,执行对应的回调函数。
  11. - 如果循环形成js的事件循环机制(Event Loop
  12. <a name="xc360"></a>
  13. ## 宏任务(Macro)和微任务(Micro)
  14. ```javascript
  15. console.log("script start");
  16. setTimeout(function () {
  17. console.log("setTimeout");
  18. }, 500);
  19. Promise.resolve()
  20. .then(function () {
  21. console.log("promise1");
  22. })
  23. .then(function () {
  24. console.log("promise2");
  25. });
  26. console.log("script end");

输出结果:

  1. script start
  2. script end
  3. promise1
  4. promise2
  5. setTimeout

JS 中分两种任务类型:macrotask(宏任务) 和 microtask(微任务),在 ECMAScript 中,microtask 称为 jobsmacrotask 可称为 task;

宏任务(macrotask)

每次执行栈执行的任务就是宏任务(包括每次从任务队列中获取的事件回调并放到执行栈中执行);

  • 每个宏任务会从头到尾将这个任务执行完毕。
  • 浏览器为了能个让js内部宏任务和DOM任务有序执行,会在一个宏任务执行完毕后,在下一个宏任务执行开始前,对页面进行重新渲染reflow(宏任务 => 渲染 => 宏任务 => …)

how-browsers-work-36-638.jpg
3963958-432f5165ba423f57.png

微任务(microtask)

当前宏任务执行完毕立刻执行的任务。

  1. 当前宏任务结束后,下一个宏任务执行之前,在渲染之前执行。
  2. 响应速度比setTimeout(setTimeout是宏任务)快,无需等待渲染;
  3. 一个宏任务执行完毕后,就会将它执行期间产生的所有微任务都执行完毕。

    什么样的场景会形成宏任务和微任务?

  • macrotask
    • 主代码块
    • setTimeout
    • setInterval
    • … …
  • microtask
    • process.nextTick
    • promise.then
    • catch
    • finally
    • … …

在node环境下,process.nextTick的优先级高于Promise,可以理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才执行微任务中的Promise部分。

运行机制

以下任务执行顺序都是靠函数调用栈来模拟

  1. 事件循环机制从script标签内开始,整个script标签作为一个宏任务处理。
  2. 在代码执行的过程中,遇到宏任务,如setTimeout,就会将当前任务分发到对应的执行队列中
  3. 执行过程中遇到微任务,如promise,在创建Promise实例对象时,代码同步执行,如果到了then操作,该任务就会被分发到微任务队列中
  4. script标签内的代码执行完毕,同时执行过程中所涉及到的宏任务和微任务被分配到相应的队列。
  5. 第一个宏任务执行完毕,检查微任务队列执行所有存在的微任务;
  6. 微任务执行完毕,第一轮消息循环执行完毕,页面进行一次渲染;
  7. 然后开始第二轮消息队列循环,从宏任务队列中取出任务执行;
  8. 如果两个任务队列没有任务可执行,此时所有的任务执行完毕。

    宏-微 任务执行顺序

    用代码演示宏任务和微任务的概念。
    题目一 ```javascript console.log(“1”);

setTimeout(() => { console.log(“2”); }, 1000);

new Promise((resolve, reject) => { console.log(“3”); resolve(); console.log(“4”); }).then(() => { console.log(“5”); });

console.log(“6”);

  1. 输出结果为:
  2. ```javascript
  3. 1
  4. 3
  5. 4
  6. 6
  7. 5
  8. 2
  • 初始化状态,执行栈为空;
  • 首先执行 <script> 标签内的同步代码,此时全局的代码进入执行栈中,同步顺序执行代码,输出 1;
  • 执行过程中遇到异步代码 setTimeout(宏任务),将其分配到宏任务异步队列中;
  • 同步代码继续执行,遇到一个 promise 异步代码(微任务),但是构造函数中的代码为同步代码,依次输出3、4,则 then 之后的任务加入到微任务队列中去;
  • 最后执行同步代码,输出 6;
  • 因为 script 内的代码作为宏任务处理,所以此次循环进行到处理微任务队列中的所有异步任务,直达微任务队列中的所有任务执行完成为止,微任务队列中只有一个微任务,所以输出 5;
  • 此时页面要进行一次页面渲染,渲染完成之后,进行下一次循环;
  • 在宏任务队列中取出一个宏任务,也就是之前的 setTimeout,最后输出 2;
  • 此时任务队列为空,执行栈中为空,整个程序执行完毕;

    主线程上添加宏任务与微任务

    执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 ```javascript console.log(“start”);

setTimeout(() => { console.log(“setTimeout”); // 将回调代码放入另一个宏任务队列 }, 0);

new Promise((resolve, reject) => { for (let i = 0; i < 5; i++) { console.log(i); } resolve(); }).then(() => { console.log(“Promise实例成功回调执行”); // 将回调代码放入微任务队列 });

console.log(“end”);

  1. 输出结果为:
  2. ```javascript
  3. start
  4. 0
  5. 1
  6. 2
  7. 3
  8. 4
  9. 5
  10. end
  11. Promise实例成功回调执行
  12. setTimeout

微任务中创建微任务

执行顺序:主线程 => 主线程上创建的微任务 => 微任务上创建的微任务 => 主线程上创建的宏任务

  1. setTimeout(_ => console.log(4)); // 宏任务
  2. new Promise(resolve => {
  3. resolve();
  4. console.log(1); // 同步
  5. }).then(_ => {
  6. console.log(3); // 微任务
  7. Promise.resolve()
  8. .then(_ => {
  9. console.log("before timeout"); // 微任务中创建微任务,在宏任务之前执行
  10. })
  11. .then(_ => {
  12. Promise.resolve().then(_ => {
  13. console.log("also before timeout");
  14. });
  15. });
  16. });
  17. console.log(2);

输出结果为:

  1. 1
  2. 2
  3. 3
  4. before timeout
  5. also before timeout
  6. 4

同时有两个微任务执行:

  1. setTimeout(_ => console.log(5)); // 宏任务
  2. new Promise(resolve => {
  3. resolve();
  4. console.log(1); // 同步
  5. })
  6. .then(_ => {
  7. console.log(3); // 微任务
  8. Promise.resolve()
  9. .then(_ => {
  10. console.log("before timeout"); // 微任务中创建微任务,在宏任务之前执行
  11. })
  12. .then(_ => {
  13. Promise.resolve().then(_ => {
  14. console.log("also before timeout");
  15. });
  16. });
  17. })
  18. .then(_ => {
  19. console.log(4); // 微任务
  20. });
  21. console.log(2);

输出结果为:

  1. 1
  2. 2
  3. 3
  4. before timeout
  5. 4
  6. also before timeout
  7. 5
  1. setTimeout(_ => console.log(5)); // 宏任务
  2. new Promise(resolve => {
  3. resolve();
  4. console.log(1); // 同步
  5. })
  6. .then(_ => {
  7. console.log(3); // 微任务
  8. Promise.resolve()
  9. .then(_ => {
  10. console.log("before timeout1"); // 微任务中创建微任务,在宏任务之前执行
  11. })
  12. .then(_ => {
  13. Promise.resolve().then(_ => {
  14. console.log("also before timeout1");
  15. });
  16. });
  17. })
  18. .then(_ => {
  19. console.log(4); // 微任务
  20. Promise.resolve()
  21. .then(_ => {
  22. console.log("before timeout2"); // 微任务中创建微任务,在宏任务之前执行
  23. })
  24. .then(_ => {
  25. Promise.resolve().then(_ => {
  26. console.log("also before timeout2");
  27. });
  28. });
  29. });
  30. console.log(2);

输出结果为:

  1. 1
  2. 2
  3. 3
  4. before timeout1
  5. 4
  6. before timeout2
  7. also before timeout1
  8. also before timeout2

宏任务中创建微任务

执行顺序:主线程 => 主线程上创建的微任务 => 主线程上的宏任务 => 宏任务队列中创建的微任务

  1. setTimeout(() => {
  2. console.log("timer_1");
  3. setTimeout(() => {
  4. console.log("timer_3");
  5. }, 500);
  6. new Promise(resolve => {
  7. resolve();
  8. console.log("new promise");
  9. }).then(() => {
  10. console.log("promise then");
  11. });
  12. }, 500);
  13. setTimeout(() => {
  14. console.log("timer_2");
  15. }, 500);
  16. console.log("end");

输出结果为:

  1. end
  2. timer_1
  3. new promise
  4. promise then
  5. timer_2
  6. timer_3

微任务中创建的宏任务

执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 => 微任务中创建的宏任务
异步宏任务队列只有一个,当在微任务中创建一个宏任务之后,他会被追加到异步宏任务队列上(跟主线程创建的异步宏任务队列是同一个队列)

  1. new Promise(resolve => {
  2. console.log("new Promise(macro task 1)");
  3. resolve();
  4. }).then(() => {
  5. console.log("micro task 1");
  6. setTimeout(() => {
  7. console.log("macro task 3");
  8. }, 500);
  9. });
  10. setTimeout(() => {
  11. console.log("macro task 2");
  12. }, 1000);
  13. console.log("Task queue");

输出结果为:

  1. new Promise(macro task 1)
  2. Task queue
  3. micro task 1
  4. macro task 3
  5. macro task 2
  1. new Promise(resolve => {
  2. console.log("new Promise(macro task 1)");
  3. resolve();
  4. }).then(() => {
  5. console.log("micro task 1");
  6. setTimeout(() => {
  7. console.log("macro task 3");
  8. }, 1000);
  9. });
  10. setTimeout(() => {
  11. console.log("macro task 2");
  12. }, 1000);
  13. console.log("Task queue");

输出结果为:

  1. new Promise(macro task 1)
  2. Task queue
  3. micro task 1
  4. macro task 2
  5. macro task 3

【注意】:如果把 setTimeout(() => { // 宏任务2 console.log('macro task 2'); }, 1000) 改为立即执行setTimeout(() => { // 宏任务2 console.log('macro task 2'); }, 500) 那么它会在 macro task 3 之前执行,因为定时器是过多少毫秒之后才会加到事件队列里

  1. console.log("======== main task start ========");
  2. new Promise(resolve => {
  3. console.log("create micro task 1");
  4. resolve();
  5. }).then(() => {
  6. console.log("micro task 1 callback");
  7. setTimeout(() => {
  8. console.log("macro task 3 callback");
  9. }, 0);
  10. });
  11. console.log("create macro task 2");
  12. setTimeout(() => {
  13. console.log("macro task 2 callback");
  14. new Promise(resolve => {
  15. console.log("create micro task 3");
  16. resolve();
  17. }).then(() => {
  18. console.log("micro task 3 callback");
  19. });
  20. console.log("create macro task 4");
  21. setTimeout(() => {
  22. console.log("macro task 4 callback");
  23. }, 0);
  24. }, 0);
  25. new Promise(resolve => {
  26. console.log("create micro task 2");
  27. resolve();
  28. }).then(() => {
  29. console.log("micro task 2 callback");
  30. });
  31. console.log("======== main task end ========");

输出结果为:

  1. ======== main task start ========
  2. create micro task 1
  3. create macro task 2
  4. create micro task 2
  5. ======== main task end ========
  6. micro task 1 callback
  7. micro task 2 callback
  8. macro task 2 callback
  9. create micro task 3
  10. create macro task 4
  11. micro task 3 callback
  12. macro task 3 callback
  13. macro task 4 callback

包含 async/await

demo1

  1. async function async1() {
  2. console.log("async1 start");
  3. await async2();
  4. console.log("async1 end"); // await 语句后面的加入到微任务队列中
  5. }
  6. async function async2() {
  7. console.log("async2");
  8. }
  9. async1();
  10. new Promise(resolve => {
  11. console.log("create micro task");
  12. resolve();
  13. }).then(() => {
  14. console.log("micro task callback");
  15. });
  16. console.log("script start");

输出结果为:

  1. async1 start
  2. async2
  3. create micro task
  4. script start
  5. async1 end
  6. micro task callback

demo2

  1. async function job1() {
  2. console.log("a");
  3. await job2();
  4. console.log("b"); // 添加到微任务队列
  5. }
  6. async function job2() {
  7. console.log("c");
  8. }
  9. setTimeout(function () {
  10. new Promise(function (resolve) {
  11. console.log("d");
  12. resolve();
  13. }).then(function () {
  14. console.log("e");
  15. });
  16. console.log("f");
  17. });
  18. job1();
  19. new Promise(function (resolve) {
  20. resolve();
  21. }).then(function () {
  22. console.log("g");
  23. });
  24. console.log("h");

输出结果为:

  1. a
  2. c
  3. h
  4. b
  5. g
  6. d
  7. f
  8. e

demo3

  1. async function async1() {
  2. console.log("async1 start");
  3. await async2();
  4. console.log("async1 end");
  5. }
  6. async function async2() {
  7. console.log("async2");
  8. }
  9. console.log("script start");
  10. setTimeout(function () {
  11. console.log("setTimeout");
  12. }, 0);
  13. async1();
  14. new Promise(function (resolve) {
  15. console.log("promise1");
  16. resolve();
  17. }).then(function () {
  18. console.log("promise2");
  19. });
  20. console.log("script end");

输出结果为:

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

demo4

  1. async function t1() {
  2. console.log(1);
  3. console.log(2);
  4. new Promise(function (resolve) {
  5. console.log("promise3");
  6. resolve();
  7. }).then(function () {
  8. console.log("promise4"); // 微任务1
  9. });
  10. await new Promise(function (resolve) {
  11. console.log("b");
  12. resolve();
  13. }).then(function () {
  14. console.log("t1p"); // 微任务2
  15. });
  16. // await 语句后的加入微任务队列中1
  17. // 微任务5
  18. console.log(3);
  19. console.log(4);
  20. new Promise(function (resolve) {
  21. console.log("promise5");
  22. resolve();
  23. }).then(function () {
  24. console.log("promise6");
  25. });
  26. }
  27. setTimeout(function () {
  28. console.log("setTimeout");
  29. }, 0);
  30. async function t2() {
  31. console.log(5);
  32. console.log(6);
  33. await Promise.resolve().then(() => console.log("t2p")); // 微任务4
  34. // await 语句后的加入微任务队列中2
  35. // 微任务6
  36. console.log(7);
  37. console.log(8);
  38. }
  39. t1();
  40. new Promise(function (resolve) {
  41. console.log("promise1");
  42. resolve();
  43. }).then(function () {
  44. console.log("promise2"); // 微任务3
  45. });
  46. t2();
  47. console.log("end");

输出结果为:

  1. 1
  2. 2
  3. promise3
  4. b
  5. promise1
  6. 5
  7. 6
  8. end
  9. promise4
  10. t1p
  11. promise2
  12. t2p
  13. 3
  14. 4
  15. promise5
  16. 7
  17. 8
  18. promise6
  19. setTimeout

await 之后的代码必须等 await 语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完 await 语句,才把 await 语句后面的全部代码加入到微任务行列,所以,在遇到 await promise 时,必须等 await promise 函数执行完毕才能对 await 语句后面的全部代码加入到微任务中。
所以,在等待 await Promise.then 微任务时:

  • 运行其他同步代码;
  • 等到同步代码运行完,开始运行 await promise.then 微任务;
  • await promise.then 微任务完成后,把 await 语句后面的全部代码加入到微任务行列;

await 语句是同步的,await 语句后面全部代码才是异步的微任务。

demo5

  1. async function b() {
  2. console.log("b");
  3. await c();
  4. // 加入微任务队列2
  5. console.log("b1");
  6. }
  7. async function c() {
  8. console.log("c");
  9. await new Promise(function (resolve, reject) {
  10. console.log("promise1");
  11. resolve();
  12. }).then(() => {
  13. console.log("promise1-1"); // 微任务2
  14. });
  15. // 加入微任务队列1
  16. setTimeout(() => {
  17. console.log("settimeout1");
  18. });
  19. console.log("c1");
  20. }
  21. new Promise(function (resolve, reject) {
  22. console.log("promise2");
  23. resolve();
  24. console.log("promise2-1");
  25. reject();
  26. })
  27. .then(() => {
  28. console.log("promise2-2"); // 微任务1
  29. setTimeout(() => {
  30. console.log("settimeout2");
  31. new Promise(function (resolve, reject) {
  32. resolve();
  33. }).then(function () {
  34. console.log("settimeout2-promise");
  35. });
  36. });
  37. })
  38. .catch(() => {
  39. console.log("promise-reject");
  40. });
  41. b();
  42. console.log("200");

输出结果为:

  1. promise2
  2. promise2-1
  3. b
  4. c
  5. promise1
  6. 200
  7. promise2-2
  8. promise1-1
  9. c1
  10. b1
  11. settimeout2
  12. settimeout2-promise
  13. settimeout1

执行步骤:

  • 从上自下执行,new Promise 函数立即执行 => 打印 :promise2
  • resolve()promise2-2 放入任务队列中
  • settimeout2 放入任务队列
  • 继续向下执行 => 打印 :promise2-1
  • 执行 b() 函数 => 打印 :b
  • 执行 await c() 函数 => 打印:c
  • 进入 await c() 函数中 new Promise 函数 => 打印 :promise1
  • resolve()promise1-1 放入任务队列中
  • settimeout1 放入任务队列
  • 继续执行、无立即执行 => 打印 :200
  • 暂无执行任务,去任务中执行第一个进入任务的待执行动作 => 打印:promise2-2
  • 继续执行任务中序列中待执行动作 => 打印:promise1-1
  • 任务中暂无执行动作,继续执行 c() 函数中的待执行代码 => 打印:c1
  • c() 执行完毕,继续执行 b() 函数中待执行代码 => 打印:b1
  • 至此,立即执行以任务执行完毕,执行任务队列中第一个进入的待执行任务 => 打印 :settimeout2
  • 任务一中,执行 new Promise 函数 resolve()settimeout2-promise 放入任务中,立即执行任务 => 打印:settimeout2-promise
  • 继续执行任务中待执行动作 => 打印:settimeout1

总结:

  • 微任务队列优先于宏任务队列执行;
  • 微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端;
  • 微任务队列中创建的微任务会被添加到微任务队列的尾端;
  • 只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行;
  • 只有运行完 await 语句,才把 await 语句后面的全部代码加入到微任务行列;
  • 在遇到 await promise 时,必须等 await promise 函数执行完毕才能对 await 语句后面的全部代码加入到微任务中;
    • 在等待 await Promise.then 微任务时:
      • 运行其他同步代码;
      • 等到同步代码运行完,开始运行 await promise.then 微任务;
      • await promise.then 微任务完成后,把 await 语句后面的全部代码加入到微任务行列;