导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

冷门技能

基本认知

Untitled

如上图,在任务管理器的进程一栏里,有道词典和有道云笔记就是进程,而在进程下又有着多个执行不同任务的线程。

进程

渲染进程

⇒ 渲染引擎线程

⇒ JS引擎线程

为什么渲染线程与JS线程互斥

因为不能一边渲染内容,一边JS在修改DOM。

所以

<div>Front of you</div>
<script>while(true) {}</script> <!-- JS一直在执行,不空闲 -->
<div>End of you</div> <!-- 页面一直卡死,不渲染任何东西 -->
<div>Front of you</div>
<script>
  setTimeout(() => {
    document.getElementByTagName('div')[0].innerText = 'Front of me';
  }, 1000);
</script> <!-- JS执行完后,空闲 -->
<div>End of you</div> <!-- 页面渲染 Front of you 和 End of you,1s后改为 Front of me -->

事件

宏任务、微任务

浏览器事件环的运行流程

Untitled

  1. 先在JS引擎线程执行栈中执行同步代码(遇到宏任务,会执行它们,并把宏任务们的回调放进宏任务队列)
  2. 然后清空所有微任务
  3. GUI渲染
  4. 取一个宏任务回调出来执行(先进先出)【其实处理的都是宏任务的回调函数 e.g. ajax回调,setTimeout回调,例如代码执行时,遇到 setTimeout ,其实是立马执行了 setTimeout,但把回调放进了宏任务队列
  5. 再执行同步代码
  6. 清空微任务
  7. ... 循环

简单记顺序就是:

同步代码 → 清空所有微任务 → 渲染 → 取宏任务队列中一个出来执行

⭐️ 案例1 - 带页面渲染

document.body.style.backgroundColor = 'orange';
console.log(1);
setTimeout(()=> { // setTimeout1
  document.body.style.backgroundColor = 'green';
  console.log(2);
}, 100);
Promise.resolve(3).then(num => { // Promise.then1
  document.body.style.backgroundColor = 'purple';
  console.log(num);
});
console.log(4);

分析:

// 宏任务 setTimeout
// 微任务 Promise.then
/**
 * 过程:
 *  设置 body.bgColor = 'orange'; -> 没渲染
 *  打印1
 *  (把 setTimeout 执行,把回调放进 宏任务队列)
 *  同步执行 Promise.resolve(3) 把 then 回调放进 微任务队列
 *  打印4
 *  同步任务执行栈代码执行完毕
 *  先清空微任务队列, 执行 Promise.then
 *    设置 body.bgColor = 'purple'; -> 没渲染
 *    打印 3
 *  GUI渲染, 把 body.bgColor 渲染成了 purple
 *  取宏任务队列中 setTimeout cb ,等到 100ms 以后放进JS引擎执行栈中执行
 *    设置 body.bgColor = 'green' -> 没渲染
 *    打印 2
 *  同步代码执行栈中已经没有任务了
 *  微任务也没任务了
 *  再GUI渲染
 *    把 body.bgColor = 'green' -> 此时渲染了
 * /

案例2

Promise.resolve().then(() => { // then1
  console.log('p1');

  setTimeout(() => { // setTimeout2
    console.log('s2');
  }, 0);
});

setTimeout(() => { // setTimeout1
  console.log('s1');
  Promise.resolve().then(() => { // then2
    console.log('p2');
  });
});

分析:

/**
 * 宏任务: setTimeout1 setTimeout2
 * 微任务: then1 then2
 * 打印: p1 s1 p2 s2
 */

⭐️ 案例3

Promise.resolve().then(() => { // then1
  console.log('p1');

  setTimeout(() => { // setTimeout2
    console.log('s2-1');
  }, 0);
  setTimeout(() => { // setTimeout3
    console.log('s2-2');
  }, 0);
});

setTimeout(() => { // setTimeout1
  console.log('s1');
  Promise.resolve().then(() => { // then2
    console.log('p2-1');
  }).then(() => { // then3
    console.log('p2-2');
  });
});

分析:

1. 执行 then1, 把 then1 放进微任务队列
    // 此刻微任务队列: then1
2. 执行 setTimeout1, 把 setTimeout1 放进 宏任务队列
    // 此刻宏任务队列: setTimeout1
    到此, JS引擎执行栈代码已经全部执行完毕
3. 开始清空微任务队列
    执行 then1
      【打印 p1】
      把 setTimeout2 和 setTimeout3 放进宏任务队列
        // 此刻宏任务队列: setTimeout1、setTimeout2、setTimeout3
      微任务队列全部执行完毕, 移除 then1
        // 此刻微任务队列: 空
4. 开始执行宏任务
    取出宏任务中第一个 setTimeout1 任务执行
      【打印 s1】
      把 then2、then3 放入微任务队列
        // 此刻微任务队列: then2、then3
      宏任务 setTimeout1 执行完毕, 移除 setTimeout1 任务
        // 此刻宏任务队列: setTimeout2、setTimeout3
      进入下次循环
5. 开始清空微任务队列
    执行 then2
      【打印 p2-1】
    执行 then3
      【打印 p2-2】
    微任务队列清空完毕, 清除 then2、then3
      // 此刻微任务队列: 空
6. 开始执行宏任务
    取出宏任务中第一个 setTimeout2 执行
      【打印 s2-1】
    宏任务 setTimeout2 执行完毕, 移除 setTimeout2 任务
        // 此刻宏任务队列: setTimeout3
7. JS引擎执行栈中没有代码执行, 微任务队列也没有任务, 再次从宏任务队列中取任务执行
    取出宏任务中最后一个任务 setTimeout3 执行
      【打印 s2-2】
    宏任务队列也清空了

所以最终打印顺序为:
  p1
  s1
  p2-1
  p2-2
  s2-1
  s2-2

⭐️ 案例4 - setTimeout 延迟不为 0

console.log(1);

setTimeout(() => { // setTimeout1
  console.log(2);
}, 10);

new Promise(function(resolve, reject) {
  console.log(3);
  resolve('');
  console.log(4);
}).then(res => { // then1
  console.log(5);
});
console.log(6);

分析:

1. 【先打印 1】
2. 把 setTimeout1 放进 宏任务队列
  // 此刻宏任务队列: [setTimeout1]
3. new Promise 中 function 立即同步执行(executor是个同步代码)
  【打印3】
  执行resolve,把 then1 放进 微任务队列
  // 此刻微任务队列: [then1]
  【打印4】
  function 执行完毕
4. 执行同步log6
  【打印6】
5. 开始清空微任务队列
  执行 then1
    【打印5】
6. 开始执行宏任务队列中的第一个
  等待10ms, 执行 setTimeout1 cb
    【打印2】

最终打印顺序:
  1
  3
  4
  6
  5
  2

改造:

⭐️ 把 executor 中的 resolve 删掉

console.log(1);

setTimeout(() => { // setTimeout1
  console.log(2);
}, 10);

new Promise(function(resolve, reject) {
  console.log(3);
  // resolve('');
  console.log(4);
}).then(res => { // then1
  console.log(5);
});
console.log(6);

// 分析
resolve 不执行,导致 then1 不会被调用, 所以 then1 不会加入到微任务队列
所以最终打印为
1
3
4
6
2

继续改造,把 resolve 改为 reject:

console.log(1);

setTimeout(() => { // setTimeout1
  console.log(2);
}, 10);

new Promise(function(resolve, reject) {
  console.log(3);
  reject('');
  console.log(4);
}).then(res => { // then1
  console.log(5);
});
console.log(6);

// 分析
then1会被放进微任务队列,但 then 的第二个回调没指定,所以会报错

打印:

Untitled

接着改造,我们把 reject 回调指定一下:

console.log(1);

setTimeout(() => { // setTimeout1
  console.log(2);
}, 10);

new Promise(function(resolve, reject) {
  console.log(3);
  reject('');
  console.log(4);
}).then(res => { // then1
  console.log(5);
}, err => {
  console.log('err', 5);
});
console.log(6);

// 分析
这次指定了 错误捕获,所以打印 'err 5'

Untitled

⭐️ 再次改造

console.log(1);

setTimeout(() => { // setTimeout1
  Promise.resolve().then(() => { // then2
    console.log(2);
  });
}, 10);

new Promise(function(resolve, reject) {
  console.log(3);
  resolve('');
  console.log(4);
}).then(res => { // then1
  setTimeout(() => { // setTimeout2
    console.log(5);
  }, 0);
});
console.log(6);

// 分析
这次要注意一个点,setTimeout1 是延迟 10ms 执行的
所以 setTimeout2 的执行会先于 setTimeout1
导致 then2 后于 setTimeout2 执行

最终打印
  1
  3
  4
  6
  5
  2

⭐️ 案例5 - async/await

let res = function() {
  console.log(1);
  return new Promise((resolve, reject) => {
    console.log(2);
    resolve(4);
  })
}

new Promise(async (resolve, reject) => {
  console.log(3);
  let test = await res();
  console.log(test);
});

console.log(5);

new Promise((resolve, reject) => {
  console.log(6);
});

console.log(7);

分析:

// async 函数 默认返回一个 promise 实例
// await 必须存在于 async 函数中
// await test() 产出一个值,就相当于 yield 产出一个值
// test 如果返回一个 promise 对象,
// 那么 await test() 就会去调用 test() 执行以后的 then(res => {}) 方法, 拿到 res 结果

let res = function() {
  console.log(1);
  return new Promise((resolve, reject) => {
    console.log(2);
    resolve(4);
  })
}

new Promise(async (resolve, reject) => {
  console.log(3);
  let test = await res(); // then1
  console.log(test);
});

console.log(5);

new Promise((resolve, reject) => {
  console.log(6);
});

console.log(7);

1. 定义一个 function 赋值给 res
2. 执行 new Promise 中的 function
  不要看是个 async 就觉得是异步执行,此 function 就是个立即执行的 executor
  只不过 async 表示此 function 最终要返回一个 promise 对象而已
  【打印3】
  await res() 相当于调用 res().then()
  立马执行 res 函数
  【打印1】
  执行 res 中的 new Promise
  【打印2】
  resolve(4) 后把 res().then() 放入微任务队列
  // 此刻微任务队列: [then1]
3.【打印5】
4. 执行 new Promise
  【打印6】
5.【打印7】
6. 执行栈同步代码全部执行完了,开始执行所有微任务
  then() 执行
  【打印4】

最终打印结果:
  3
  1
  2
  5
  6
  7
  4

改造:

let res = function() {
  console.log(1);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      new Promise(resolve => {
        console.log(2);
        setTimeout(() => {
          console.log(3);
        });
      });
    }, 0);
    resolve(5);
  })
}

new Promise(async (resolve, reject) => {
  setTimeout(() => {
    Promise.resolve().then(() => {
      console.log(4);
    });
  }, 0);
  let test = await res();
  console.log(test);
});

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

new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log(7);
  }, 0);
});

console.log(8);

分析:

let res = function() {
  console.log(1);
  return new Promise((resolve, reject) => {
    setTimeout(() => { // setTimeout2
      new Promise(resolve => {
        console.log(2);
        setTimeout(() => { // setTimeout5
          console.log(3);
        });
      });
    }, 0);
    resolve(5);
  })
}

new Promise(async (resolve, reject) => {
  setTimeout(() => { // setTimeout1
    Promise.resolve().then(() => { // then2(4)
      console.log(4);
    });
  }, 0);
  let test = await res(); // then1(5)
  console.log(test);
});

setTimeout(() => { // setTimeout3
  console.log(6);
}, 0);

new Promise((resolve, reject) => {
  setTimeout(() => { // setTimeout4
    console.log(7);
  }, 0);
});

console.log(8);
/**
 * 宏任务: setTimeout1 setTimeout2 setTimeout3 setTimeout4 setTimeout5
 * 微任务: then1(5) then2(4)
 * 打印: 1 8 5 4 2 6 7 3
 */

⭐️ 案例6 - addEventListener

<button id="btn">click</button>
<script>
  const oBtn = document.getElementById('btn');
  oBtn.addEventListener('click', () => {
    console.log(1);

    Promise.resolve('m1').then(str => { // then1
      console.log(str);
    });
  }, false)

  oBtn.addEventListener('click', () => {
    console.log(2);

    Promise.resolve('m2').then(str => { // then2
      console.log(str);
    })
  }, false)

  oBtn.click();
</script>

分析:

addEventListener 中的回调算宏任务, oBtn.click() 之后 会立即按代码顺序调用 两个回调
相当于把这俩回调当做同步在依次执行

类似于下面代码

function handler1() { // cb1
  console.log(1);

  Promise.resolve('m1').then(str => { // then1
    console.log(str);
  });
}

function handler2() { // cb2
  console.log(2);

  Promise.resolve('m2').then(str => { // then2
    console.log(str);
  })
};

handler1();
handler2();

所以

1. 先执行第一个 click 回调
  【打印1】
   把 then1 放进 微任务队列
   // 此刻微任务队列: [then1]
2. 再执行第二个 click 回调
  【打印2】
  把 then2 放进 微任务队列
  // 此刻微任务队列: [then1, then2]
3. 执行栈中同步代码都执行完毕,开始执行所有微任务
  执行 then1
 【打印m1】
  执行 then2
 【打印m2】

// 最终打印:
 1
 2
 m1
 m2

但如果不是js主动触发 click 事件,而是让用户触发 click 事件,情况就不同了

<button id="btn">click</button>
<script>
  const oBtn = document.getElementById('btn');
  oBtn.addEventListener('click', () => {
    console.log(1);

    Promise.resolve('m1').then(str => { // then1
      console.log(str);
    });
  }, false)

  oBtn.addEventListener('click', () => {
    console.log(2);

    Promise.resolve('m2').then(str => { // then2
      console.log(str);
    })
  }, false)
</script>

分析:

此种情况,就要看做是走了两次事件循环,
把click回调当做两个宏任务依次放进宏任务队列,一个个取出来执行

1. 用户点击按钮
2. 触发第一个 click 回调
  【打印1】
   把 then1 添加进微任务队列
   // 此刻微任务队列: [then1]
3. 清空微任务队列
  【打印m1】
  // 此刻微任务队列: []
4. 触发第二个 click 回调
  【打印2】
   把 then2 添加进微任务队列
   // 此刻微任务队列: [then2]
5. 清空微任务队列
  【打印m2】

最终顺序:
  1
  m1
  2
  m2

改造:

const oBtn = document.getElementById('btn');
oBtn.addEventListener('click', () => { // cb1
  setTimeout(() => { // setTimeout1
    console.log('1');
  }, 0);

  Promise.resolve('m1').then(str => { // then1
    console.log(str);
  });
}, false)

oBtn.addEventListener('click', () => { // cb2
  setTimeout(() => { // setTimeout2
    console.log('2');
  }, 0);

  Promise.resolve('m2').then(str => { // then2
    console.log(str);
  })
}, false)

/**
 * 宏任务: cb1 cb2 setTimeout1 setTimeout2
 * 微任务: then1(m1) then2(m2)
 * 执行: m1 m2 1 2
 */

换成 handler1、handler2:

const oBtn = document.getElementById('btn');
const handler1 = function() {
  setTimeout(() => { // setTimeout1
    console.log('1');
  }, 0);

  Promise.resolve('m1').then(str => { // then1
    console.log(str);
  });
}

const handler2 = function() {
  setTimeout(() => { // setTimeout2
    console.log('2');
  }, 0);

  Promise.resolve('m2').then(str => { // then2
    console.log(str);
  })
}

handler1()
handler2();

/**
 * 宏任务: setTimeout1 setTimeout2
 * 微任务: then1 then2
 * 执行: m1 m2 1 2
 */

⭐️ 总结