知识储备:
阮一峰《ECMAScript 6 入门》读书笔记——Promise
阮一峰《ECMAScript 6 入门》读书笔记——async 函数
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 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。