实现一个运动路径动画流程

移动页面上元素 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(() => {
//parseInt一定要传第二个参数:进制。不然第一个参数是0x时,会转成16进制
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`
}
//递归,直到resolve为止
innerWalk()
}
}, 20)
}
innerWalk()
})
// 这里是promise的链式调用,then内的函数返回一个promise就可以链式调用
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
//promise封装部分省略

function *taskGenerator() {
yield walk('left', 20)
yield walk('top', 50)
yield walk('left', 30)
}
const gen = taskGenerator()

我们定义了一个 taskGenerator 生成器函数,并实例化出 gen,手动执行:

1
gen.next()

将会向左偏移 20 像素。
再次手动执行:

1
gen.next()

将会向上偏移 50 像素。
再次手动执行:

1
gen.next()

将会向左偏移 30 像素。
整个过程掌控感十足,唯一的不便之处就是需要我们反复手动执行 gen.next()。

async/await 方案

基于以上基础,改造成 async/await 方案也并不困难。

1
2
3
4
5
6
//封装promise部分省略
const task = async function () {
await walk('left', 20)
await walk('top', 50)
await walk('left', 30)
}

只需要直接执行 task() 即可。
async/await 就是 generator 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。