一、单线程
JavaScript 是单线程,H5 允许 JavaScript 创建多个线程,但是子线程受主线程控制,并且不能操作 DOM
所有任务分为同步任务和异步任务
- 同步任务:在主线程中按顺序执行,这些任务形成一个执行栈
- 异步任务:不会立即执行,放到任务队列中,当异步任务完成后,会将对应的事件放到任务队列中
二、异步执行机制
2.1 执行栈
存放同步任务,按照任务的顺序处理
2.2 任务队列
异步任务的结果放在任务队列中,等待主线程的处理。本身不会立马执行,而是等待执行条件满足时(如 I/O 操作完成、定时器到期)才会执行
2.3 事件循环
当执行栈中同步任务全部完成后,事件循环会检查任务队列中是否有异步任务的结果
如果有待处理的异步任务,事件循环就会将这些任务逐个取出,放到执行栈中执行
主线程会不断重复这个过程:执行栈中的同步任务,检查任务队列中的异步任务
三、宏任务、微任务
3.1 宏任务
宏任务通常是指在事件循环的每一个循环中执行的任务,主要由 JavaScript 引擎和浏览器控制
常见的宏任务:
- script
- setTimeout
- setInterval
- setImmediate(Node.js 环境)
- I/O 操作
- UI 渲染(例如,浏览器的页面渲染和重绘)
- postMessage(跨线程消息传递)
- requestAnimationFrame(浏览器的动画帧请求)
3.2 微任务
微任务是在当前宏任务完成后、下一个宏任务开始之前执行的任务,微任务主要用于处理异步操作的回调
常见的微任务:
- Promise 的 then/catch/finally(非 new Promise)
- MutationObserver(监听 DOM 变动,浏览器环境)
- process.nextTick(Node.js 环境)
- Object.observe
3.3 执行顺序
1)执行当前宏任务: 事件循环从宏任务队列中取出一个任务并执行,例如 setTimeout 的回调
2)执行所有微任务: 当前宏任务完成后,事件循环会处理所有在微任务队列中的任务,微任务队列会在宏任务队列之前被清空
3)渲染和更新 UI: 在微任务队列执行完后,浏览器会更新渲染,确保 DOM 和 UI 的改变被显示出来。此时,浏览器会执行重绘和布局操作
4)执行下一个宏任务: 事件循环继续到下一个宏任务,并重复上述过程
注意: setTimeOut 并不是直接的把回调函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回调函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么 setTimeOut 不能精准执行的问题了。setTimeOut 执行需要满足两个条件:
- 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行回调函数
- 这个回调函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
四、async、await
async/await 是异步编程的终极解决方案
async 是"异步"的意思,而 await 是等待的意思,await 用于等待一个异步任务执行完成的结果
- async/await 是一种编写异步代码的新方法(以前是采用回调和 promise)
- async/await 是建立在 promise 的基础上,像 promise 一样,也是非阻塞的
- async/await 让异步代码看起来、表现起来更像同步代码
async 修饰的函数就是异步函数,该函数的返回值是 promise 对象。await 只能写在 async 修饰的函数里,await修饰的代码会等待。在函数里,碰到 await 修饰的代码时,await 朝后的代码都会等待
尤其需要注意的是async,await,promise,setTimeout 的执行顺序,具体例子如下:
async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
console.log('promise1');
reslove();
}).then(function () {
console.log('promise2');
})
console.log('script end');
执行顺序:
script start
async1 start
async2
promise1
script end
asnyc1 end
promise2
setTimeOut
执行的流程:
- 整个代码片段(script)作为一个宏任务执行console.log('script start'),输出script start
- 执行setTimeout,是一个异步动作,放入宏任务异步队列中
- 执行async1(),输出async1 start,继续向下执行
- 执行async2(),输出async2,并返回了一个promise对象
await让出了线程,把返回的promise加入了微任务异步队列
所以async1()下面的代码也要等待上面完成后继续执行
- 执行 new Promise,输出promise1,然后将resolve()放入微任务异步队列
- 执行console.log('script end'),输出script end
到此同步的代码就都执行完成了,然后去微任务异步队列里去获取任务
- 接下来执行resolve(async2返回的promise返回的),输出了async1 end
- 然后执行resolve(new Promise的),输出了promise2
- 最后执行setTimeout,输出了setTimeout
在第4步中, await 这里有一个机制, 就是 await 的等待, 不会阻塞外部函数的执行, 而 await 等待的 如果是一个 Promise 则 Promise 里面的代码还是同步执行;如果不是 Promise ,就会使用 Promise.resolve 来进行封装, 这里的 async2 是一个 async 方法, 里面的打印会同步执行, 而 await async2() 后面的代码会放到微任务队列中的第一个位置,等待外部同步代码执行完毕以后再执行