JavaScript 异步
为什么 JavaScript 是单线程
JavaScript 的主要用途是与用户互动,以及操作DOM。
假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
事件循环(Event Loop)和任务队列(Task Queue)
- JavaScript 的异步机制由事件循环和任务队列构成。
- JavaScript 本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成。
- JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。
- 异步操作会由浏览器执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列中。
- 当主线程的执行栈清空之后会读取任务队列中的回调函数并执行。主线程会一致重复这个操作从而进入一个无限的循环,这就是事件循环。
宏任务(Macrotask)和微任务(Microtask)
我们一般的异步任务会分为宏任务和微任务两种:
- 宏任务: 整体代码script, setTimeout, setInterval, setImmediate, I/O, UI rendering
- 微任务: process.nextTick, Promises, MutationObserver
注意:
- 每一次事件循环中,宏任务只会提取一个执行,而微任务会一直提取,直到微任务队列清空。
- 一般情况下,宏任务我们会直接称为任务队列,只有微任务才会特别指明。
- 如果某个微任务又推入了一个任务进入微任务队列,那么在主线程完成该任务之后,仍然会继续运行微任务直到任务队列耗尽。
- 事件循环每次只会入栈一个宏任务,主线程执行完该任务后又会先检查微任务队列并完成里面的所有任务后再执行宏任务。
总结
- 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
- 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务
- 每个回调之后且 js 执行栈中为空
- 每个宏任务结束后
- 异步执行:
- 1、主线程执行程序,遇到异步任务时候。由浏览器或者操作系统去执行异步任务。主程序继续向下执行
- 2、异步程序执行完成之后将对应的回调函数推入任务队列。根据不同类型分别推入到宏任务队列和微任务队列
- 3、主线程执行栈清空之后,从任务队列中读取一个宏任务和所有的微任务
- 4、主线程先执行微任务,这个过程中如果推入了微任务则会一直执行微任务,直到微任务队列为空。再执行宏任务
- 5、主线程重复步骤 3
setTimeout(function() {
console.log('1')
})
const aaa = new Promise(function(resolve) {
console.log('2')
resolve()
}).then(function() {
console.log('3')
// 不停的推入 微任务
aaa.then(() => {
console.log('4')
aaa.then(() => {
console.log('5')
new Promise(function(resolve) {
console.log('6')
resolve()
}).then(function() {
console.log('7')
})
console.log('8')
})
})
})
console.log('9')
// 2 9 3 4 5 6 8 7 1