异步编程快速入门
前言¶
异步编程是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);
})();
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);
})();
第一部分完成啦(~ ̄▽ ̄)~,你已经知道什么是异步编程了接着看下一部分吧
2. 异步编程该如何使用¶
在Javascript中,异步编程主要有以下几种方法:
用于设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段(具体使用请点击链接查看MDN)
计时器可以使用清除 下面是一个简单示例:
其中,“2”是在“1”和“3”输出\(1s\)后才输出的
可见,可以实现延期执行代码而不暂停后面代码的执行
console.log('服务器崩溃');
console.log('倒数3');
setTimeout(() => {
console.log('2');
setTimeout(() => {
console.log('1');
setTimeout(() => {
console.log('骗你的');
}, 1000);
}, 1000);
}, 1000);
那有没有什么办法呢?有两种方法,一种是使用,另一种是使用
我们先讲。会设定一个定时器,用于重复调用一个函数或执行一个代码片段,在每次调用之间具有固定的时间间隔
用于清除创建的定时器
参考示例
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],当输出完时,使用清除计时器
再来讲讲该怎么写
(我知道你可能看不懂写法,那先不用管,后面会讲的)
这两段代码都可以在浏览器和Node.js中运行
若在Box3环境中运行,则不需要定义sleep函数,因为Box3环境自带
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. 基本使用¶
的构造函数需要填入一个回调函数,这个回调函数会立即开始执行
会提供这个回调函数两个参数:resolveFunc和rejectFunc。若调用resolveFunc,这个就会兑现;若调用rejectFunc,这个就会拒绝;若这个回调函数发生错误,这个也会拒绝。这两个参数可以是任意的名称
resolveFunc和rejectFunc都可以传入参数。resolveFunc的参数将会在兑现后作为then的回调函数参数onFulfilled的参数;rejectFunc的参数和这个回调函数发生的错误将会在兑现后作为then的回调函数参数onRejected和catch的回调函数参数onRejected的参数
无论是已兑现还是已拒绝,最后都会调用finally的回调函数
- 我知道上面又臭又长的文档你已经看的头晕了,让我们来整理一下
-
- 会提供这个回调函数两个参数:resolveFunc和rejectFunc。若调用resolveFunc,这个就会兑现;若调用rejectFunc,这个就会拒绝;若这个回调函数发生错误,这个也会拒绝。这两个参数可以是任意的名称
-
我们也写一个简单的示例
运行这段代码后,应有\(50\%\)的几率输出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\%\)的几率输出拒绝
-
- resolveFunc和rejectFunc都可以传入参数。resolveFunc的参数将会在兑现后作为then的回调函数参数onFulfilled的参数;rejectFunc的参数和这个回调函数发生的错误将会在兑现后作为then的回调函数参数onRejected和catch的回调函数参数onRejected的参数
-
我们还是写一个简单的示例
运行这段代码后,应有\(50\%\)的几率输出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); });
兑现 大于0.5
,\(25\%\)的几率输出拒绝 拒绝
,\(25\%\)的几率输出拒绝 抛出
-
- 无论是已兑现还是已拒绝,最后都会调用finally的回调函数
-
我们依然写一个简单的示例
运行这段代码后,应有\(50\%\)的几率输出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') });
兑现 大于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
(语法错误)吧 除使用await之外,还可以使用链式使用,见下文
6. 链式使用¶
你也许在前面的图中看到了,的then、catch、finally三个方法都会返回,这就是链式调用的核心
假设有一个变量a: ,a.then()也是一个,a.then().then()也是一个,就这么无限循环套娃下去
若其中有一个拒绝/抛出错误,那么Javascript就会在这个链中找到第一个catch并传递错误信息
每个then的回调函数参数的返回值为下一个then的回调函数参数 下面是一个示例:
7. 扩展信息¶
关于,可以查阅MDN获取更多信息毕竟这只是个快速入门QAQ