使用场景

在游戏开发过程中,会使用到一些异步方法。典型的比如UI动画。针对这些异步方法有3种处理方案:callback(俗称回调),promise,await。本文以 Cocos Creator 和 Typescript 为例子,介绍和分析3种处理方案。

在此之前,我们先假定这样一个场景:有 nodeA/nodeB/nodeC 三个节点,分别执行 actionA/actionB/actionC 三个不同的UI动画。要求顺序执行 ABC。(伪)代码如下所示:

  1. // Test.ts
  2. // 注意:这里的node和action均未赋值,所以不能直接运行(即:这是一份伪代码)
  3. export class Test extends cc.Component {
  4. nodeA: cc.Node;
  5. nodeB: cc.Node;
  6. nodeC: cc.Node;
  7. actionA: cc.ActionInterval;
  8. actionB: cc.ActionInterval;
  9. actionC: cc.ActionInterval;
  10. run_callback() {
  11. // TODO
  12. }
  13. run_promise() {
  14. // TODO
  15. }
  16. run_await() {
  17. // TODO
  18. }
  19. }

Callback:回调地狱在等着你

实现原理

先上代码部分:

// Test.ts

...

    run_callback() {
        this.nodeA.runAction(cc.sequence(
            this.actionA,
            cc.callFunc(() => {
                this.nodeB.runAction(cc.sequence(
                    this.actionB,
                    cc.callFunc(() => {
                        this.nodeC.runAction(this.actionC)
                    })
                ))
            })
        ))
    }

...

可以从代码中看到,通过 cc.sequence 方法来组织顺序,通过 cc.callFunc 来写入回调函数,来实现 ABC 三个节点的顺序执行动画。

优点

是比较古老的实现方法,唯一的优点大概是兼容性比较好。

缺点

  1. 不清晰,无法准确的看出执行顺序。
  2. 在复杂顺序时,往往会陷入到“回调地狱”这一坑中(可以看到一连串以})结尾的代码)。
  3. 无法判定所有顺序都已经执行完毕。

    Promise,一种基础的实现

    实现原理

    先上代码部分: ```typescript // Test.ts

run_promise() {
    let promiseA = () => new Promise(res => {
        this.nodeA.runAction(cc.sequence(this.actionA, cc.callFunc(res)))
    })
    let promiseB = () => new Promise(res => {
        this.nodeB.runAction(cc.sequence(this.actionB, cc.callFunc(res)))
    })
    let promiseC = () => new Promise(res => {
        this.nodeC.runAction(cc.sequence(this.actionC, cc.callFunc(res)))
    })
    promiseA().then(() => {
        promiseB().then(() => {
            promiseC()
        })
    })
}

使用 ES6 的 Promise 对象,将 actionX 封装成为一个 Promise,再顺序调用 3 个 Promise。
<a name="bEpgL"></a>
## 优点

1. 顺便较为明确,而且在复杂顺序下顺序也比较明确。
1. 可以将整个函数返回成一个 Promise,表示所有顺序都已经执行完毕,供其他函数调用。
1. 还可以混合使用 Promise.all/Promise.race 等高阶方法。
<a name="KLlnO"></a>
## 缺点

1. Promise 创建即执行,因此需要使用箭头函数封装 1 次。
1. 也有“回调地狱”的可能性。
<a name="f1281531"></a>
# Await:优雅使用 Promise 的语法糖
<a name="N5LU2"></a>
## 实现原理
代码部分:
```typescript
// Test.ts

...

    async run_await() {
        // TODO
        let promiseA = () => new Promise(res => {
            this.nodeA.runAction(cc.sequence(this.actionA, cc.callFunc(res)))
        })
        let promiseB = () => new Promise(res => {
            this.nodeB.runAction(cc.sequence(this.actionB, cc.callFunc(res)))
        })
        let promiseC = () => new Promise(res => {
            this.nodeC.runAction(cc.sequence(this.actionC, cc.callFunc(res)))
        })
        await promiseA()
        await promiseB()
        await promiseC()
    }

...

await 关键字需要与 async 关键字一起使用,其本质是一个 Promise 的语法糖,await 会等待后面的 Promise 执行完毕并返回执行结果。
参考链接:

  1. 顺序十分清晰,特别是在复杂顺序下。
  2. 可以获取 Promise 的异步返回值。
  3. 更多的高阶操作(骚操作)。

    缺点

  4. 对 Promise 中 reject 状态的处理依赖 try-catch 语句。(这一条可以理解成缺点也可以理解成优点)

  5. 在 Cocos Creator 中,如果使用 Javascript 作为脚本语言,则不支持。(只能在 Typescript 脚本中使用)其他方法,提供一些有趣的思路

    其他方法

    方法1:定时器方法 scheduleOnce

    scheduleOnce 是 cc.Component 提供的定时器方法,需要提前获取到 actionA/actionB/actionC 的完成时间timeA/timeB/timeC,代码如下所示: ```typescript // Test.ts

run_schedule() {
    let timeA: number; // 省略时间获取过程
    let timeB: number;
    let timeC: number;
    this.nodeA.runAction(this.actionA)
    this.scheduleOnce(() => { this.nodeB.runAction(this.actionB) }, timeA)
    this.scheduleOnce(() => { this.nodeC.runAction(this.actionC) }, timeA + timeB)
}

… ``` 这种方法其实是思路上的转变,即将“顺序执行”转化为“按照时间延迟执行”,是一种比较取巧的解法。缺点是,在 Cocos Creator 中,定时器是实现依赖游戏帧,在游戏帧数变动较大时,可能会导致一些顺序上的异常。

方法2:标志位flag

给actionA设置一个标志位,然后每隔一段时间检测一次标志位,检测成功则执行actionB。
这种方法太蠢了,不要使用。

总结

使用 async/await,这是未来的方向!