跳转至

异步编程快速入门

前言

异步编程是Box3中非常重要的部分,涉及到对话框、数据库、HTTP、RTC等等内容,甚至包括最基础的等待。学习异步编程,可以让你更好的理解Box3的API,实现更多的效果

你对异步编程有多了解?

该文档不同内容适用于不同人群,根据自己的情况,点击下面链接跳转到该页面的不同地方

我不会Javascript
我只会Hello world
在上一行的基础上,我只会定义变量和基础运算
在上一行的基础上,我只会if和else
在上一行的基础上,我只会for和while
在上一行的基础上,我只会switch
在上一行的基础上,我不会定义函数
在上上行的基础上,我只会定义函数
在上一行的基础上,我只会定义类
异步编程?从没听说过
我听说过异步编程,但不知道怎么用
我只会用setTimeout setInterval
我会使用基本的async await,但不知道其中原理
我听说过Promise,但不知道和Box3有什么关系
我听说过Promise,但不会用(或者只会async await)
我听说过Promise,但不会链式调用
我会Promise,想要了解关于Promise的更多信息

0. 先去学好Javascript吧

你可以不看这个页面了,去看看Javascript 基础教程Javascript 教程 - 菜鸟教程Javascript 参考 - MDN

1. 什么是异步编程?

我们先来讲下同步编程
同步编程,就是所有事情同步执行(并不代表所有事情会在同一时刻完成,而是所有事情都在同一个线程进行)

stateDiagram-V2
    direction LR
    [*] --> A
    A --> B
    B --> C
    C --> D
    D --> E
    E --> [*]

异步编程和同步编程相对,事情可能不会在同一线程进行

stateDiagram-V2
    direction LR
    [*] --> A
    A --> [*]
    [*] --> B
    B --> [*]
    [*] --> C
    C --> [*]
    [*] --> D
    D --> [*]
    [*] --> E
    E --> [*]
我们来做个实验,定义一个等待\(1s\)的函数sleep,然后调用它
function sleep(){
    return new Promise((resolve) => {
        setTimeout(resolve, 1e3);
    });
}
(async () => {
    console.log(1);
    await sleep();
    console.log(2);
    await sleep();
    console.log(3);
    await sleep();
    console.log(4);
})();
此时输出结果应该是一秒输出一行,那是因为这是同步编程,程序等待sleep运行完之后才会继续 我们稍微修改一下调用:
function sleep(){
    return new Promise((resolve) => {
        setTimeout(resolve, 1e3);
    });
}
(async () => {
    console.log(1);
    sleep();
    console.log(2);
    sleep();
    console.log(3);
    sleep();
    console.log(4);
})();
你会发现所有输出都会瞬间完成,而这只是删掉了一个await(是不是很神奇),具体原因后文再说


第一部分完成啦(~ ̄▽ ̄)~,你已经知道什么是异步编程了接着看下一部分吧

2. 异步编程该如何使用

在Javascript中,异步编程主要有以下几种方法:

  • /
  • /

用于设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段(具体使用请点击链接查看MDN)
计时器可以使用清除 下面是一个简单示例:

Javascript
console.log(1);
setTimeout(() => {
    console.log(2);
}, 1000);
console.log(3);
输出结果
1
3
2

其中,“2”是在“1”和“3”输出\(1s\)后才输出的
可见,可以实现延期执行代码而不暂停后面代码的执行

让我们用制作一个倒计时:

console.log('服务器崩溃');
console.log('倒数3');
setTimeout(() => {
    console.log('2');
    setTimeout(() => {
        console.log('1');
        setTimeout(() => {
            console.log('骗你的');
        }, 1000);
    }, 1000);
}, 1000);
很明显,这段代码显然是非常杂乱
那有没有什么办法呢?有两种方法,一种是使用,另一种是使用

我们先讲会设定一个定时器,用于重复调用一个函数或执行一个代码片段,在每次调用之间具有固定的时间间隔
用于清除创建的定时器

提示

技术上是可以互换使用的,但为了代码的易维护性,请匹配使用

思考

请使用,而不使用,实现上面计时器的效果
什么,你不会用?点击跳转到MDN查看详细说明

参考示例

let lines = ['2', '1', '骗你的'], index = 0;
console.log('服务器崩溃');
console.log('倒数3');
let intervalID = setInterval(() => {
    console.log(lines[index++]);
    if(index >= lines.length)
        clearInterval(intervalID);
}, 1e3);
我们来分析以下这段代码,lines储存了计时器所有要输出的内容,index储存了当前输出到第几行,intervalID储存计时器的id
使用每秒钟输出lines[index],当输出完时,使用清除计时器

再来讲讲该怎么写
(我知道你可能看不懂写法,那先不用管,后面会讲的)

function sleep(){
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
(async () => {
    console.log('服务器崩溃');
    console.log('倒数3');
    await sleep();
    console.log('2');
    await sleep();
    console.log('1');
    await sleep();
    console.log('骗你的');
})();
function sleep(){
    return new Promise((resolve) => {
        setTimeout(resolve, 1000);
    });
}
console.log('服务器崩溃');
console.log('倒数3');
sleep().then(() => {
    console.log('2');
    return sleep();
}).then(() => {
    console.log('1');
    return sleep();
}).then(() => {
    console.log('骗你的');
});

这两段代码都可以在浏览器和Node.js中运行
若在Box3环境中运行,则不需要定义sleep函数,因为Box3环境自带

(async () => {
    console.log('服务器崩溃');
    console.log('倒数3');
    await sleep(1000);
    console.log('2');
    await sleep(1000);
    console.log('1');
    await sleep(1000);
    console.log('骗你的');
})();
console.log('服务器崩溃');
console.log('倒数3');
sleep(1000).then(() => {
    console.log('2');
    return sleep(1000);
}).then((1000) => {
    console.log('1');
    return sleep();
}).then((1000) => {
    console.log('骗你的');
});

Box3中,sleep用于等待特定的时间,单位为\(ms\)
你也许发现了,[await写法]中sleep前面加了await,而[写法]中没有
你可以试试在[await写法]中去除前面的await,而在[写法]中加上(注意空格),看看会发生什么
先不急着讲具体原因,先把下一部分看了吧

3. 什么是

表示异步操作最终的完成(或失败)以及其结果值
总处于以下三种状态之中:

  • 待定(pending),初始状态,既没有被兑现,也没有被拒绝
  • 已兑现(fulfilled),意味着操作成功完成
  • 已拒绝(rejected),意味着操作失败

我们举个例子,某用户向吉吉喵反馈bug,吉吉喵收到后会回复该用户

flowchart LR
    A[某个用户] --反馈bug--> B[吉吉喵]
    B --收到--> A
但众所周知,吉吉喵是个很忙的喵,所以大多数情况下并不能做到秒回
我们将吉吉喵回复的过程看作一个。吉吉喵没有回复的时候,这个就处于待定状态;若吉吉喵认为这确实是个bug,回复“收到”,这个就会兑现,处于已兑现状态;若吉吉喵认为还需要提供更多信息,或者这个bug无法复现,这个就会拒绝,处于已拒绝状态
flowchart LR
    A[某个用户]
    B[吉吉喵] --> C{"检查
    尝试复现
    [待定]"}

    C --> D("这是bug
    [已兑现]")
    C -->E("无法复现
    [已拒绝]")
    D --收到--> A
    D --反馈--> F[搬砖喵]
    E --请提供更多信息--> A

4. 在Box3中的体现

在Box3中用途很多,从最基本的sleep函数,再到.dialog / .dialog方法,再到数据库和数据储存空间等等,都需要使用
你是否发现,这些方法一般前面都要加await,而有的方法,例如.say / .say则不用?
我们来看看这些方法的声明

.get(key: ): <>
.set(key: , value: ): <>

.say(message: ):
.setVoxel(x: , y: , z: , voxel: | , rotation: | ):

可见,前两个方法的返回值是个,而后两个不是

5. 基本使用

的构造函数需要填入一个回调函数,这个回调函数会立即开始执行
会提供这个回调函数两个参数:resolveFuncrejectFunc。若调用resolveFunc,这个就会兑现;若调用rejectFunc,这个就会拒绝;若这个回调函数发生错误,这个也会拒绝。这两个参数可以是任意的名称
resolveFuncrejectFunc都可以传入参数。resolveFunc的参数将会在兑现后作为then的回调函数参数onFulfilled的参数;rejectFunc的参数和这个回调函数发生的错误将会在兑现后作为then的回调函数参数onRejectedcatch的回调函数参数onRejected的参数
无论是已兑现还是已拒绝,最后都会调用finally的回调函数

我知道上面又臭又长的文档你已经看的头晕了,让我们来整理一下
的构造函数需要填入一个回调函数,这个回调函数会立即开始执行

我们写一个简单的示例

new Promise((resolveFunc, rejectFunc) => {
    console.log('1');
    resolveFunc();
});
运行这段代码后,会 立刻 输出以下内容
1

会提供这个回调函数两个参数:resolveFuncrejectFunc。若调用resolveFunc,这个就会兑现;若调用rejectFunc,这个就会拒绝;若这个回调函数发生错误,这个也会拒绝。这两个参数可以是任意的名称

我们也写一个简单的示例

new Promise((a, b) => {
    if(Math.random() >= 0.5)
        a();
    else if(Math.random() >= 0.5)
        b();
    else
        throw "抛出";
}).then(() => {
    console.log('兑现');
}, () => {
    console.log('拒绝');
});
运行这段代码后,应有\(50\%\)的几率输出兑现\(50\%\)的几率输出拒绝

resolveFuncrejectFunc都可以传入参数。resolveFunc的参数将会在兑现后作为then的回调函数参数onFulfilled的参数;rejectFunc的参数和这个回调函数发生的错误将会在兑现后作为then的回调函数参数onRejectedcatch的回调函数参数onRejected的参数

我们还是写一个简单的示例

new Promise((a, b) => {
    if(Math.random() >= 0.5)
        a('大于0.5');
    else if(Math.random() >= 0.5)
        b('拒绝');
    else
        throw "抛出";
}).then((v) => {
    console.log('兑现', v);
}, (reason) => {
    console.log('拒绝', reason);
});
运行这段代码后,应有\(50\%\)的几率输出兑现 大于0.5\(25\%\)的几率输出拒绝 拒绝\(25\%\)的几率输出拒绝 抛出

无论是已兑现还是已拒绝,最后都会调用finally的回调函数

我们依然写一个简单的示例

new Promise((a, b) => {
    if(Math.random() >= 0.5)
        a('大于0.5');
    else if(Math.random() >= 0.5)
        b('拒绝');
    else
        throw "抛出";
}).then((v) => {
    console.log('兑现', v);
}).catch((reason) => {
    console.log('拒绝', reason);
}).finally(() => {
    console.log('但无论无何,这是一个Promise')
});
运行这段代码后,应有\(50\%\)的几率输出兑现 大于0.5\(25\%\)的几率输出拒绝 拒绝\(25\%\)的几率输出拒绝 抛出
并且总是会输出但无论无何,这是一个Promise

这下你应该看懂了吧
我们用一张图来总结一下:

---
title: Promise
---
flowchart LR
    Promise1[Promise]
    Promise2[Promise]
    then1(".then(onFulfilled)
    运行onFulfilled回调")
    catch1(".catch(onRejected)
    .then(..., onRejected)
    运行onRejected回调")
    finally1("finally(onFinally)
    运行onFinally回调")
    then2(".then(onFulfilled)
    运行onFulfilled回调")
    catch2(".catch(onRejected)
    .then(..., onRejected)
    运行onRejected回调")
    finally2("finally(onFinally)
    运行onFinally回调")
    Promise1 --兑现--> then1
    Promise1 --拒绝--> catch1
    Promise1 --> finally1
    then1 --返回--> Promise2
    catch1 --返回--> Promise2
    finally1 --返回--> Promise2
    Promise2 --兑现--> then2
    Promise2 --拒绝--> catch2
    Promise2 --> finally2
    then2 --> ...
    catch2 --> ...
    finally2 --> ...

问题来了,我们刚刚所有的示例,都是自己写的,怎么在Box3中使用呢?
我们以对话框为例:

world.onPlayerJoin(({ entity }) => {
    var dialog = entity.player.dialog({
        type: 'select',
        title: '系统',
        content: `${entity.player.name},你想看看box3-docs的更新日志吗`,
        options: ['让我看看!', '下次一定']
    });
    dialog.then((result) => {
        if(result && result.index === 0) {
            entity.player.dialog({
                type: 'text',
                title: 'box3-docs 更新日志',
                content: "新增Box3World / GameWorld页面\n新增Box3Entity / GameEntity页面\n新增Box3Player / GamePlayer页面\n新增db & Box3Database页面",
                hasArrow: true
            }).then((resolve) => {
                entity.player.dialog({
                    type: 'text',
                    title: 'box3-docs 更新日志',
                    content: "新增Box3Vector3 / GameVector3页面\n新增Box3Bounds3 / GameBounds3页面\n新增Box3RGBColor / GameRGBColor页面\n新增Box3RGBAColor / GameRGBAColor页面",
                    hasArrow: false
                });
            });
        }
    });
});

我们来分析一下,当玩家进入地图时,玩家会打开一个选择对话框,dialog方法会返回一个
我们调用其then方法。那问题来了,我们怎么知道对话框给我们的参数是什么呢?
我们根据API参考可知,dialog使用选择对话框时,其返回值为< / | > & /
“ & / ”可以先不管,可以发现,前面是<...>,这个“...”就是then方法的回调函数的参数,即代码中result的类型,即 / |
然后就是根据result来决定是结束还是继续

这时可能有人说了:我就打开个对话框?这么复杂?
因为这是个,还有更简单的方法,即在Box3中广泛使用的await
只需在对象前面加上“await”即可(注意空格)
await会等待解析完成,并直接返回<...>中“...”的值。这个过程中,会暂停后面代码的运行
那如果拒绝了呢?那么await就会抛出错误
如果不是,那么await不会解析,直接返回输入的东西 那么上面的代码就可以改写成这样子:

world.onPlayerJoin(async ({ entity }) => {
    var result = await entity.player.dialog({
        type: 'select',
        title: '系统',
        content: `${entity.player.name},你想看看box3-docs的更新日志吗`,
        options: ['让我看看!', '下次一定']
    });
    if(result && result.index === 0) {
        await entity.player.dialog({
            type: 'text',
            title: 'box3-docs 更新日志',
            content: "新增Box3World / GameWorld页面\n新增Box3Entity / GameEntity页面\n新增Box3Player / GamePlayer页面\n新增db & Box3Database页面",
            hasArrow: true
        });
        await entity.player.dialog({
            type: 'text',
            title: 'box3-docs 更新日志',
            content: "新增Box3Vector3 / GameVector3页面\n新增Box3Bounds3 / GameBounds3页面\n新增Box3RGBColor / GameRGBColor页面\n新增Box3RGBAColor / GameRGBAColor页面",
            hasArrow: false
        });
    }
});
是不是熟悉多了?
其实还有个奇葩写法,就是这样:
world.onPlayerJoin(async ({ entity }) => {
    var dialog = entity.player.dialog({     // 注意这里没有await
        type: 'select',
        title: '系统',
        content: `${entity.player.name},你想看看box3-docs的更新日志吗`,
        options: ['让我看看!', '下次一定']
    });
    let result = await dialog;              // 注意这里
    if(result && result.index === 0) {
        await entity.player.dialog({
            type: 'text',
            title: 'box3-docs 更新日志',
            content: "新增Box3World / GameWorld页面\n新增Box3Entity / GameEntity页面\n新增Box3Player / GamePlayer页面\n新增db & Box3Database页面",
            hasArrow: true
        });
        await entity.player.dialog({
            type: 'text',
            title: 'box3-docs 更新日志',
            content: "新增Box3Vector3 / GameVector3页面\n新增Box3Bounds3 / GameBounds3页面\n新增Box3RGBColor / GameRGBColor页面\n新增Box3RGBAColor / GameRGBAColor页面",
            hasArrow: false
        });
    }
});
这么写也是完全可行的,没有任何问题
但要注意一点:有await必有async(除非在模块顶层),不然就等着吃SyntaxError(语法错误)吧

提示

async/await的目的在于简化使用基于的API时所需的语法。async/await的行为就好像搭配使用了生成器

冷知识

其实await可以解析一切有then的对象,例如 / ,甚至你自己随手写的含有then的对象
于是你也可以在animate方法前加上await,也是可以生效的

除使用await之外,还可以使用链式使用,见下文

6. 链式使用

你也许在前面的图中看到了,thencatchfinally三个方法都会返回,这就是链式调用的核心
假设有一个变量a: a.then()也是一个a.then().then()也是一个,就这么无限循环下去
若其中有一个拒绝/抛出错误,那么Javascript就会在这个链中找到第一个catch并传递错误信息
每个then的回调函数参数的返回值为下一个then的回调函数参数 下面是一个示例:

Javascript
new Promise((resolve) => {
    resolve(1);
}).then(v => v * 50000)
.then(v => v + 2419 * 3)
.then(v => v * 2)
.then(v => console.log(v))
.then(v => {
    throw "1919810";
})
.then(v => console.log(v))
.catch(reason => console.log(reason))
.catch(reason => console.log(reason));
输出结果
114514
1919810

7. 扩展信息

关于,可以查阅MDN获取更多信息

评论区