实现一个运动路径动画流程
移动页面上元素 target(document.querySelectorAll(‘#man’)[0])
先从原点出发,向左移动 20px,之后再向上移动 50px,最后再次向左移动 30px,请把运动动画实现出来。
我们将移动的过程封装成一个 walk 函数,该函数要接受以下三个参数。
- direction:字符串,表示移动方向,这里简化为“left”、“top”两种枚举
- distance:整型,可正或可负
- callback:动作执行后回调
direction 表示移动方向,distance 表示移动距离。通过 distance 的正负值,我们可以实现四个方向的移动。
回调方案
因为每一个任务都是相互联系的:当前任务结束之后,将会马上进入下一个流程,如何将这些流程串联起来呢?我们采用最简单的 callback 实现,明确指示下一个任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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) }) })
|
很明显这是个完全面向过程的实现,有几次位移任务就会潜逃几层,是名副其实的回调地狱
Promise方案
来用promise方案解决一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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
很明显promise的链式调用舒服的多
generator 方案
ES Next 中生成器其实并不是天生为解决异步而生的,但是它又天生非常适合解决异步问题。用 generator 方案解决异步任务也同样优秀:
1 2 3 4 5 6 7 8
|
function *taskGenerator() { yield walk('left', 20) yield walk('top', 50) yield walk('left', 30) } const gen = taskGenerator()
|
我们定义了一个 taskGenerator 生成器函数,并实例化出 gen,手动执行:
将会向左偏移 20 像素。
再次手动执行:
将会向上偏移 50 像素。
再次手动执行:
将会向左偏移 30 像素。
整个过程掌控感十足,唯一的不便之处就是需要我们反复手动执行 gen.next()。
async/await 方案
基于以上基础,改造成 async/await 方案也并不困难。
1 2 3 4 5 6
| const task = async function () { await walk('left', 20) await walk('top', 50) await walk('left', 30) }
|
只需要直接执行 task() 即可。
async/await 就是 generator 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。