Skip to content

宏任务

宿主环境提供的异步方法,都是宏任务。

  • 全局代码(script 脚本)

  • UI 渲染(部分浏览器实现会把UI渲染当成宏任务)

  • DOM 事件监听的回调

  • setTimeout 回调,等待时间到达后才将回调放入宏任务队列。

  • setInterval 回调

  • setImmediate(Node.js 支持,浏览器中仅IE和Edge部分实现,标准不支持)

  • XMLHttpRequest/fetch等回调(onload、onerror等)

  • messageChannel

    javascript
    const channel = new MessageChannel()
    channel.port1.postMessage('msg')
    channel.port2.onmessage = e => {
      console.log(e.data)
    }
    Promise.resolve().then(() => {
      console.log('@')
    })
  • IndexedDB 的回调

  • History API 的某些回调

  • WebSocket 的事件回调(onmessage, onopen, onclose等)

  • FileReader、File API 的事件回调

  • requestAnimationFrame(不是宏任务,也不是微任务,单独的队列,发生在渲染前)

Nodejs 中的宏任务

  • 全局代码(script)
  • setTimeout/setInterval
  • setImmediate
  • I/O 回调(如 fs、net、http等异步回调)
  • process.nextTick(注意这是微任务!)
  • MessageChannel(node也支持)
  • 各种事件(如 'data', 'end', 'error' 等 EventEmitter 回调)

微任务

由语言标准提供。

  • Promise 相关方法(new Promise() 执行器中的代码属于同步任务)

    • p.then()

    • p.catch()

    • p.finally()

    • Promise.resolve.then()

    • Promise.reject.then()

  • await 之后的代码

    javascript
    await foo() // 这一行是同步代码
    bar()
    
    // 等价于
    new Promise((resolve, reject) => {
      foo()
    }).then(() => {
      bar()
    })
  • new MutationObserver(cb),监听标签的变化,标签只要有任何变化,对应回调就会在微任务异步执行

  • queueMicrotask Promise.resolve().then()

    js
    window.queueMicrotask = function (callback) {
      Promise.resolve()
        .then(callback)
        // 定时器是为了把异常抛到宏任务队列,这样就变成了全局未捕获异常,就不会阻塞
        // 后续 Promise 链中的微任务
        .catch(e => setTimeout(() => { throw e }))
    }
  • process.nextTick(Nodejs)

事件循环机制:

JS 代码执行时,浏览器分配一个 JS 引擎线程(主线程),里面有一个 ECStack,用来自上而下执行代码,以函数调用为单位入栈/出栈,执行时会判断是同步代码还是异步代码,如果是同步代码立即执行,如果是异步代码,JS 引擎“注册”这个异步任务,代码不会立即执行,主线程继续往下执行。异步任务会注册到浏览器的 EventLoop 中,EventLoop 会将异步任务分为宏任务和微任务。分别用宏任务队列和微任务队列存储它们。等到所有的同步代码执行完后,主线程空闲下来了,执行一个宏任务(比如整个 script 脚本或 setTimeout 回调),执行过程中遇到微任务(如 Promise.then),会将其加入微任务队列,当前宏任务执行完成后,立即清空所有微任务队列(依次执行所有微任务),微任务全部执行完毕后,浏览器有机会进行一次页面渲染(GUI 渲染),但渲染可能被浏览器优化跳过(合并多次 DOM 更新优化性能)。之后,从宏任务队列中取出下一个宏任务,循环往复。

同步:任务是依次执行的,上一个任务没有完成,下一个任务不能进行处理(js单线程)

异步:上一个任务没有完成,下一个任务也可以继续处理(浏览器多线程)

页面一直都是绿色,不会经历 red => green 的过程,因为 GUI 渲染线程在微任务清空完后才执行。

js
document.body.style.background = 'red'
Promise.resolve().then(() => {
  document.body.style.background = 'green'
})

有两个宏任务,全局 script 和 setTimeout,全局代码执行完后,可能执行 GUI 渲染线程,setTimeout 执行完后一定执行 GUI 渲染线程,因为页面可能由 red => green,也可能一直是 green,GUI 渲染线程未必每次都执行,浏览器可能优化。

js
document.body.style.background = 'red'
setTimeout(() => {
  document.body.style.background = 'green'
})

因为宏任务是一个一个取出的,所以输出顺序为 1 2 3 4

javascript
btn.addEventListener('click', () => {
  console.log('1')
  Promise.resolve().then(() => {
    console.log('2')
  })
})

btn.addEventListener('click', () => {
  console.log('3')
  Promise.resolve().then(() => {
    console.log('4')
  })
})

同步代码,直接在执行栈中将两个回调执行,因此输出顺序是 1 3 2 4

javascript
btn.addEventListener('click', () => {
  console.log('1')
  Promise.resolve().then(() => {
    console.log('2')
  })
})

btn.addEventListener('click', () => {
  console.log('3')
  Promise.resolve().then(() => {
    console.log('4')
  })
})

btn.click() // 同步代码
js
Promise.resolve().then(() => {
  console.log('then1')
  Promise.resolve().then(() => {
    console.log('then1-1')
  }).then(() => {
    console.log('then1-2')
  })
})
.then(() => {
  console.log('then2')
})
.then(() => {
  console.log('then3')
})
.then(() => {
  console.log('then4')
})
.then(() => {
  console.log('then5')
})

// then-1
// then1-1
// then2
// then1-2
// then3
// then4
// then5

// 第一轮微任务队列

// 初始:then1 进入微任务队列
// 执行 then1

// 打印:then1
// 注册:Promise.resolve().then(...)(then1-1)
// 注册:then1的下一个 then(then2)作为微任务
// 第二轮微任务队列

// then1-1(嵌套 promise 的 then)
// then2(主链 then)
// 执行 then1-1

// 打印:then1-1
// 注册:then1-2(then1-1 的 then)
// 执行 then2

// 打印:then2
// 注册:then3(then2 的 then)
// 第三轮微任务队列

// then1-2
// then3
// 执行 then1-2

// 打印:then1-2
// 执行 then3

// 打印:then3
// 注册:then4
// 第四轮微任务队列

// then4
// 执行 then4

// 打印:then4
// 注册:then5
// 第五轮微任务队列

// then5
// 执行 then5

// 打印:then5
js
// https://juejin.cn/post/6949699310732869669
Promise.resolve().then(() => {
  console.log('then1')
  
  Promise.resolve().then(() => {
    console.log('then1-1')
 
    return Promise.resolve()
  }).then(() => {
    console.log('then1-2')
  })
})
.then(() => {
  console.log('then2')
})
.then(() => {
  console.log('then3')
})
.then(() => {
  console.log('then4')
})
.then(() => {
  console.log('then5')
})

// then-1
// then1-1
// then2
// then3
// then4
// then1-2
// then5

// Promise.resolve().then(() => {
//     return Promise.resolve(4);//return了一个Promise
// })
// .then((res) => {
//     console.log(res)
// })
// 可以理解为
// Promise.resolve()
// .then(() => {
//     return 4;
// })
// .then()
// .then()
// .then((res) => {
//     console.log(res)
// })
js
function light () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('red')
      resolve()
    }, 3000)
  }).then(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('green')
        resolve()
      }, 2000)
    })
  }).then(() => {
    return light()
  })
}

为什么要有微任务?只有宏任务可以吗? 宏任务遵循谁能执行了就会先执行的原则执行,只有宏任务的情况下,如果后面有一个优先级很高, 要立即执行的代码,也只能后面执行 所以只有宏任务是不行的,因为可能要处理高优先级的任务,所以引入了微任务的概念

Node中的事件循环和浏览器中的事件循环有什么区别? Node宏任务执行顺序 1.timers定时器:执行已经安排的setTimeout和setInterval的回调 2.pending callback(待执行回调):执行延迟到下一个循环迭代的I/O回调 3.idle,prepare:仅系统内部使用 4.poll:检索新的I/O事件,执行与I/O相关的回调 5.check:执行setImmediate()的回调 6.close callback:WebSocket.on('close', () => {})

微任务和宏任务在Node中的执行顺序(Node 10为界限) Node v10及以前 1.执行完一个阶段中的所有宏任务(上面的1-6) 2.执行nextTick队列的内容 3.执行微任务

Node v10以后: 和浏览器的行为统一

js
// example1
async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end')

  // await async2()可以理解为 
  // new Promise(() => {
  //   async2() // 所以是async2()立即执行的,但是await后面的代码都相当于放到了.then中,所以上面代码意思就是
  // }).then(() => {
  //   console.log('async1 end')
  // })
}

async function async2 () {
  console.log('async2')
}

console.log('script start')

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

async1()

new Promise((resolve) => {
  console.log('Promise1')
  resolve()
}).then(() => {
  console.log('Promise2')
})

console.log('script end')

// script start
// async1 start
// async2
// Promise1
// script end
// async1 end
// Promise2
// setTimeout

// example2

console.log('start')

setTimeout(() => {
  console.log('children2')
  Promise.resolve().then(() => {
    console.log('children3')
  })
}, 0);

new Promise((resolve, reject) => {
  console.log('children4')
  setTimeout(() => {
    console.log('children5')
    resolve('children6')
  }, 0);
}).then((value) => {
  console.log('children7')
  setTimeout(() => {
    console.log(value)
  }, 0);
})

// start
// children4
// children2
// children3
// children5
// children7
// children6

// example3
const p = function () {
  return new Promise((resolve, reject) => {
    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1)
      }, 0);
      resolve(2)
    })
    p1.then((value) => {
      console.log(value)
    })
    console.log(3)
    resolve(4)
  })
}
p().then((value) => {
  console.log(value)
})
console.log('end')

// 3 
// end
// 2
// 4
javascript
console.log(1)
async function async () {
  console.log(2)
  await console.log(3)
  console.log(4)
}
setTimeout(() => {
  console.log(5)
}, 0);
const promise = new Promise((resolve, reject) => {
  console.log(6)
  resolve(7)
})
promise.then(res => {
  console.log(res)
})
async()
console.log(8)

// 1
// 6
// 2
// 3
// 8
// 7
// 4
// 5