前置背景

  • 默认js是(主线程)单线程的
  • 栈和队列
    • 栈是后进先出
    • 队列是先进先出
    • shift() 删除第一个元素; pop() 删除最后一个元素
    • unshift() 开头添加一个元素; push() 末尾添加一个元素
  • 进程和线程的关系
    • 进程是计算机分配任务和调度的任务的基本单位,比如一个页面就是一个进程
    • js中一个进程里只有一个主线程
    • js线程和ui线程是互斥的,主线程中不能同时进行渲染ui和js
    • 如何提高js的加载速度?defer(异步加载js 并且按照顺序执行)/ async(不能保证执行顺序)
    • preload(会先加载资源),prefetch(会把路由拆分)默认不会马上加载,等待浏览器空闲时候,偷偷加载

      为什么js是单线程的

      因为并行操作同一个dom会导致混乱

浏览器事件循环

举个栗子

  1. setTimeout(() => {
  2. console.log(1)
  3. Promise.resolve().then(data => {
  4. console.log(2)
  5. })
  6. }, 0);//这边虽然是0,但是实际上做不到,大概有4ms
  7. Promise.resolve().then(data => {
  8. console.log(3);
  9. setTimeout(() => {
  10. console.log(4);
  11. }, 0)
  12. })
  13. console.log('start');

在上述代码中,js引擎会先执行主栈代码,而setTimeout和then属于异步代码,会暂时放到异步队列中,等待主栈代码执行完成后,再去执行。
毫无疑问先输出start。
在执行主栈代码的过程,遇到[1]setTimeout,[1]setTimeout是宏任务,所以等待时间到了之后,将回调函数放入宏任务队列中。
然后遇到[8]then方法,[8]then方法是微任务,因为这边直接调用了Promise.resolve() 所有马上将回调函数放入了微任务队列。
主栈代码执行完成之后,会先去清空微任务队列,所以执行[8]then回调,所以输出3。然后又遇到了一个[10]setTimeout,等待时间到之后,会将[10]setTimeout中的回调函数同样放到宏任务队列中。
清空完微任务队列后,会去执行宏任务队列中的宏任务,但只会执行一个,之后又会去清空微任务队列。所以这边先执行第一次放进队列的[1]setTimeout,所以输出1。执行过程中又遇到了第二个[3]then微任务,所以将之放入到微任务队列中。
执行完这个[1]setTimeout宏任务的之后,去清空微任务队列,所以执行第二个的微任务,所以输出2。
最后再去执行宏任务队列中的任务,这是只剩下一个[10]setTimeout,所以输出4。
所以答案是 start,3,1,2,4。([]表示行号)

宏任务和微任务

  • 宏任务是宿主环境提供的,比如浏览器
  • 微任务是语言本身提供的,比如promise.then()
  • 常见的宏任务:setImmediate(只有ie支持)、setTimeout、requestAnimationFrame、MessageChannel、ajax
  • 常见的微任务:then、queueMicroMicrotask、MutationObserver,process.nextTick(node 环境)

宏任务和微任务都是在主栈中执行,默认先执行主栈中的代码,执行后清空微任务,之后微任务执行完毕,去第一个宏任务到主栈中执行(如果有微任务再次清空微任务),再去取宏任务形成时间环。
浏览器事件循环.png

Node事件循环

node的事件循环和浏览器的事件循环在node11版本发布后,基本上就一致了,仅有的差异是,node事件循环每个宏任务都有一个单独的回调函数队列,并且按顺序执行。如下:
node事件环.jpeg

  • timers: setTimeout、setInterval
  • pending callbacks: 系统内部调用,不受我们控制
  • idle, prepare: 只在系统内部使用
  • poll: 存放异步 i/o 操作队列,readFile、writeFile等
  • check: 存放setImmediate队列
  • close callbacks: 系统内部调用

举个栗子

  1. let fs = require('fs');
  2. fs.readFile('./note.md','utf8',(err,data)=>{
  3. setTimeout(()=>{
  4. console.log('呵呵呵');
  5. }, 0);
  6. setImmediate(()=>{
  7. console.log('setImmediate');
  8. }, 1000)
  9. })

输出:setImmediate、呵呵呵。
因为 readFile处于poll阶段,下一阶段是check阶段,所以先执行setImmediate,然后因为没有微任务,所以再从头执行setTimeout。

本轮循环和次轮循环

异步任务可以分为两种:

  • 追加在本轮循环的异步任务。
  • 追加在次轮循环的异步任务。

此处的循环就是事件循环,在这边需要理解的是本轮循环一定早于次轮循环。
Node 规定,process.nextTick 和 Promise 的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行他们。而 setTimeout、setInterval、setImmediate 的回调函数,追加在次轮循环。

举个微任务栗子

  1. process.nextTick(()=>{
  2. console.log('nextTick2')
  3. process.nextTick(()=>{
  4. console.log('nextTick3')
  5. process.nextTick(()=>{
  6. console.log('nextTick4')
  7. })
  8. })
  9. })
  10. setTimeout(()=>{
  11. console.log('setTimeout2')
  12. },0)
  13. //输出
  14. nextTick2
  15. nextTick3
  16. nextTick4
  17. setTimeout2

在这个例子中,微任务被追加在此次循环。

举个大栗子

  1. setTimeout(()=>{
  2. console.log('setTimeout1')
  3. process.nextTick(()=>{
  4. console.log('nextTick1')
  5. })
  6. })
  7. process.nextTick(()=>{
  8. console.log('nextTick2')
  9. setTimeout(()=>{
  10. console.log('setTimeout2')
  11. })
  12. })
  13. let fs = require('fs');
  14. fs.readFile('./note.md','utf8',(err,data)=>{
  15. fs.readFile('./note.md','utf8',(err,data)=>{
  16. console.log(12313)
  17. })
  18. setTimeout(()=>{
  19. console.log('呵呵呵');
  20. }, 0);
  21. setImmediate(()=>{
  22. console.log('setImmediate');
  23. }, 1000)
  24. })
  25. //结果
  26. nextTick2
  27. setTimeout1
  28. nextTick1
  29. setTimeout2
  30. setImmediate
  31. 12313
  32. 呵呵呵
  33. //或者
  34. nextTick2
  35. setTimeout1
  36. nextTick1
  37. setTimeout2
  38. setImmediate
  39. 呵呵呵
  40. 12313

输出:nextTick2、setTimeout1、nextTick1、setTimeout2、setImmediate、12313、呵呵呵。
主栈执行:第 7 行入微任务队列。第 1 行入 timer 队列。第 14 行入 poll 队列。
第一轮循环:清空本轮循环微任务队列。输出 nextTick2。第9 行 入 timer 队列,执行一个宏任务(第一行) timer,输出 setTimeout1,第4行入次轮微任务队列。
第二轮循环:清空本轮循环微任务队列执行输出 nextTick1,timer 中宏任务(第9行)执行,输出 setTimeout2。
poll 队列宏任务(第4行)执行, 第18行入 timer队列,第15 行入 poll 队列,第22行入 check 队列,由于当前处于poll 阶段,所以接下去先执行 check阶段,输出 setImmediate,然后是 timer 阶段,但是因为setTimeout 实际上做不到 0毫秒延迟,结果有两种 。(所以先输出 poll 阶段,即 12313,然后是timer阶段输出 呵呵呵)或者(先输出 呵呵呵 再输出 12313)。