JS中的异步
本文总结于: 侯策《前端开发核心知识进阶》
setTimeout
JavaScript 中所有任务分为同步任务和异步任务。
同步任务是指:当前主线程将要消化执行的任务,这些任务一起形成执行栈(execution context stack)
异步任务是指:不进入主线程,而是进入任务队列(task queue),即不会马上进行的任务。
当同步任务全都被消化,主线程空闲时,即上面提到的执行栈 execution context stack 为空时,将会执行任务队列中的任务,即异步任务。
这样的机制保证了:虽然 JavaScript 是单线程的,但是对于一些耗时的任务,我们可以将其丢入任务队列当中,这样一来,也就不会阻碍其他同步代码的执行。等到异步任务完成之后,再去进行相关逻辑的操作。
const t1 = new Date()
setTimeout(() => {
const t3 = new Date()
console.log('setTimeout block')
console.log('t3 - t1 =', t3 - t1)
}, 100)
let t2 = new Date()
while (t2 - t1 < 200) {
t2 = new Date()
}
console.log('end here')
// end here
// setTimeout block
// t3 - t1 = 200
即便 setTimeout 定时器的定时为 100 毫秒,但是同步任务中 while 循环将执行 200 毫秒,计时到时后仍然会先执行主线程中的同步任务,只有当同步任务全部执行完毕,end here 输出,才会开始执行任务队列当中的任务。此时 t3 和 t1 的时间差为 200 毫秒,而不是定时器设定的 100 毫秒。
最小延迟
setTimeout(() => {
console.log('here 100')
}, 100)
setTimeout(() => {
console.log('here 2')
}, 0)
//here 2
//here 100
另一种情况
setTimeout(() => {
console.log('here 1')
}, 1)
setTimeout(() => {
console.log('here 2')
}, 0)
在 Chrome 中运行结果相反,事实上针对这两个 setTimeout,谁先进入任务队列,谁先执行并不会严格按照 1 毫秒和 0 毫秒的区分。
表面上看,1 毫秒和 0 毫秒的延迟完全是等价的。这就有点类似“最小延迟时间”这个概念。直观上看,最小延迟时间是 1 毫秒,在 1 毫秒以内的定时,都以最小延迟时间处理。此时,在代码顺序上谁靠前,谁就先会在主线程空闲时优先被执行。
MDN 上给出的最小延时概念是 4 毫秒,可以参考 最小延迟时间,另外,setTimeout 也有“最大延时”的概念。这都依赖于规范的制定和浏览器引擎的实现。
宏任务(macrotask)与微任务(microtask)
宏任务和微任务虽然都是异步任务,都在任务队列中,但是他们也是在两个不同的队列中。
宏任务包括:
- setTimeout
- setInterval
- I/O
- 事件
- postMessage
- setImmediate (Node.js,浏览器端该 API 已经废弃)
- requestAnimationFrame
- UI 渲染
微任务包括:
- Promise.then
- MutationObserver
- process.nextTick (Node.js)
例子:
console.log('start here')
const foo = () => (new Promise((resolve, reject) => {
console.log('first promise constructor')
let promise1 = new Promise((resolve, reject) => {
console.log('second promise constructor')
setTimeout(() => {
console.log('setTimeout here')
resolve()
}, 0)
resolve('promise1')
})
resolve('promise0')
promise1.then(arg => {
console.log(arg)
})
}))
foo().then(arg => {
console.log(arg)
})
console.log('end here')
-
首先输出同步内容:start here,执行 foo 函数,同步输出 first promise constructor,
-
继续执行 foo 函数,遇见 promise1,执行 promise1 构造函数,同步输出 second promise constructor,以及 end here。同时按照顺序:setTimeout 回调进入任务队列(宏任务),promise1 的完成处理函数(第 18 行)进入任务队列(微任务),第一个(匿名) promise 的完成处理函数(第 23 行)进入任务队列(微任务)
-
虽然 setTimeout 回调率先进入任务队列,但是优先执行微任务,按照微任务顺序,先输出 promise1(promise1 结果),再输出 promise0(第一个匿名 promise 结果)
-
此时所有微任务都处理完毕,执行宏任务,输出 setTimeout 回调内容 setTimeout here
案例
实现功能
移动页面上元素 target(document.querySelectorAll(’#man’)[0])
先从原点出发,向左移动 20px,之后再向上移动 50px,最后再次向左移动 30px,请把运动动画实现出来。
回调方案导致的回调地狱
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance, callback) => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束,执行下一个回调
callback && callback()
}
else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
}
else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
walk(direction, distance, callback)
}
}, 20)
}
walk('left', 20, () => {
walk('top', 50, () => {
walk('left', 30, Function.prototype)
})
})
其中walk的第三个参数为回调函数,可以看到这样的回调嵌套很不优雅,有几次位移任务,就会嵌套几层,是名副其实的回调地狱。
Promise 方案
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束
resolve()
}
else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
}
else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
walk('left', 20)
.then(() => walk('top', 50))
.then(() => walk('left', 30))
- walk 函数不再嵌套调用,不再执行 callback,而是函数整体返回一个 promise,以利于后续任务的控制和执行
- 设置 innerWalk 进行每一像素的递归调用
- 在当前任务结束时(shouldFinish 为 true),resolve 当前 promise
generator 方案
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束
resolve()
}
else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
}
else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
function *taskGenerator() {
yield walk('left', 20)
yield walk('top', 50)
yield walk('left', 30)
}
const gen = taskGenerator()
gen.next()
//向左偏移 20 像素
gen.next()
//向上偏移 50 像素
gen.next()
//向左偏移 30 像素
async/await 方案
- async 声明的函数,其返回值必定是 promise 对象,如果没有显式返回 promise 对象,也会用 Promise.resolve() 对结果进行包装,保证返回值为 promise 类型
- await 会先执行其右侧表达逻辑(从右向左执行),并让出主线程,跳出 async 函数,而去继续执行 async 函数外的同步代码
- 如果 await 右侧表达逻辑是个 promise,让出主线程,继续执行 async 函数外的同步代码,等待同步任务结束后,且该 promise 被 resolve 时,继续执行 await 后面的逻辑
- 如果 await 右侧表达逻辑不是 promise 类型,那么仍然异步处理,将其理解包装为 promise, async 函数之外的同步代码执行完毕之后,会回到 async 函数内部,继续执行 await 之后的逻辑
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束
resolve()
}
else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
}
else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
const task = async function () {
await walk('left', 20)
await walk('top', 50)
await walk('left', 30)} task()
通过对比 generator 和 async/await 这两种方式,读者应该准确认识到,async/await 就是 generator 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。