异步
题外话, 这个模块虽然属于 js ,但是我想用 ts 来写实例看看。
异步和同步
那么都知道 Javascript 语言的执行环境是单线程, 为了解决因为网络请求等待造成的页面无响应
Javascript 语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)
同步:顾名思义,一条条语句从上到下执行
异步:和同步不一样,上一步语句也许在下面才执行
TIP
我很喜欢 js 对于异步编程的处理,简单易用,没 lua coroutine 那么 掉头发,哈哈我水平比较低💩
异步编程
js 实现异步编程可以有四种方法
- 回调函数
- 事件监听
- 发布/订阅
- Promises 对象
- Worker
回调函数
很简单, 假如我需要执行一个很耗时的函数longTimeWork(), 为了避免页面卡在执行这个函数,可以先执行后续 的步骤,把需要等待执行的代码放进回调函数
function longTimeWork(callback: () => void) {
console.log("Start waitting...");
setTimeout(() => {
callback();
}, 1000);
}
function otherThings() {
console.log("World");
}
function afterWaiting() {
console.log("Hello");
}
// need a waitting to start execute afterWaiting function
longTimeWork(afterWaiting);
otherThings();
// 结果
// Start waitting...
// World
// Hello
使用这种方式,可以把同步操作变成异步的,优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数,假如是发请求,如果有多次请求操作,那么就要写很多层嵌套 💩
事件监听
这里用阮一峰的例子
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生
采用的 jQuery 的写法
f1.on("done", f2);
function f1() {
setTimeout(function () {
// f1的任务代码
f1.trigger("done");
}, 1000);
}
上面这行代码的意思是,当 f1 发生 done 事件,就执行 f2。然后,对 f1 进行改写
f1.trigger('done')表示,执行完成后,立即触发 done 事件,从而开始执行 f2。
可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型, 运行流程会变得很不清晰
发布/订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是 Ben Alman 的 Tiny Pub/Sub,这是 jQuery 的一个插件。
首先,f2 向"信号中心"jQuery 订阅"done"信号。
Query.subscribe("done", f2);
function f1() {
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.publish("done")的意思是,f1 执行完成后,向"信号中心"jQuery 发布"done"信号,从而引发 f2 的执行。
此外,f2 完成执行后,也可以取消订阅(unsubscribe)。
jQuery.unsubscribe("done", f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise
Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)
简单来说是 js 官方提供的,封装好的异步方案,用就完了☺️
这里注意一下,即使已经完成了操作(成功或失败),才对 Promise 对象绑定
then函数,它们都会如期执行
console.log("start working");
const fetchPromise: Promise<Response> = fetch("http://bilibili.com");
console.log(fetchPromise);
fetchPromise.then((res: Response) => {
console.log(res);
});
console.log("after sending...");
// start working
// Promise { <pending> }
// after sending...
// Response {
// body: ReadableStream { locked: false },
// bodyUsed: false,
// headers: Headers {
// "cache-control": "no-cache",
// "content-type": "text/html; charset=utf-8",
// date: "Wed, 11 Jan 2023 09:28:41 GMT",
// expires: "Wed, 11 Jan 2023 09:28:40 GMT",
// gear: "3",
// "set-cookie": "b_nut=1673429321; path=/; expires=Thu, 11 Jan 2024 09:28:41 GMT; domain=.bilibili.com",
// support: "nantianmen",
// vary: "Origin,Accept-Encoding",
// "x-cache-time": "0",
// "x-cache-webcdn": "MISS from blzone03",
// "x-origin-time": "no-cache, must-revalidate, max-age=0, no-store",
// "x-save-date": "Wed, 11 Jan 2023 09:28:41 GMT"
// },
// ok: true,
// redirected: true,
// status: 200,
// statusText: "OK",
// url: "https://www.bilibili.com/"
// }
使用fetch API, 把请求bilibili返回的 Promise 对象保存下来,给它添加 then 回调函数,用于请求成功后执行的操作
可以发现我们尝试输出 Promise 的时,有个pending
Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用
fetch()返回 Promise 时的状态,此时请求还在进行中。 - 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的
then()处理函数被调用。 - 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的
catch()处理函数被调用。
链式
Promise 的一个优点是可以使用链式的写法,即一个 Promise 可以绑定多个 then, 或 catch
使用链式的写法,可以避免回调函数的多重嵌套, 每一个 then 的返回值都会传进下一个 then 里。
即使再次进行多次请求也没关系,写法不会改变,依然可以保持链式的结构
catch 用于捕获错误
fetchPromise
.then((res: Response) => {
return res.url;
})
.then((url: string) => {
return fetch(url);
})
.then((res: Response) => {
console.log(res);
})
.catch((err: Error) => {
console.error(err);
});
Promise API
Promise.all()
有时候,我们必须等待多个 promise 全部实现,才能进行一个操作
使用 Promise.all()
- 当且仅当数组中所有的 Promise 都被兑现时,才会通知
then()处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入all()的Promise的顺序相同 - 会被拒绝——如果数组中有任何一个 Promise 被拒绝。此时,
catch()处理函数被调用,并提供被拒绝的 Promise 所抛出的错误
const promise1: Promise<Response> = fetch("https://www.bilibili.com");
const promise2: Promise<Response> = fetch("https://www.acfun.cn");
const promise3: Promise<Response> = fetch("https://www.youtube.com");
Promise.all([promise1, promise2, promise3])
.then((responses: Response[]) => {
for (const response of responses) {
console.log(`${response.url}: ${response.status}`);
}
})
.catch((err: Error) => {
console.log(err);
});
// https://www.bilibili.com/: 200
// https://www.acfun.cn/: 200
// https://www.youtube.com/: 200
Promise.any()
有时,我们只需要等待多个 promise 之中的其一完成,便可执行 then 操作
只要其中任意一个(有可能多个)完成(fulfilled),就执行 then,全部失败(rejected)才执行 catch
const promise1: Promise<Response> = fetch("https://www.bilibili.com");
const promise2: Promise<Response> = fetch("https://www.acfun.cn");
const promise3: Promise<Response> = fetch("https://www.youtube.com");
Promise.any([promise1, promise2, promise3]).then((resp: Response) => {
console.log(`${resp.url} finish the job firstly.`);
});
Promise.race()
只要有任意一个(有可能多个), 完成(fulfilled)或失败(rejected),则执行 then 或执行 catch
async 和 await
有时候,我们发现使用链式 promise 的写法也不够简单,于是官方提供了更简便的语法糖 await/async
async:
简单点,async 用来修饰一个function 说明这个 function 是异步的, 即 执行到它时,会先把函数中不需要等待的部分执行,需要等待的部分则是被分成片段放在"后台"执行了,然后继续执行函数外非等待的部分
这里的后台有点不准确,其实 js 是单线程执行的,只是 cpu 处理的时候需要快速地切换执行的上下文,看起来就像是并行的了 💩
await:
- 只能在用在
async函数中 - 放在一个返回 promise 的执行函数前,表示等待 promise
fulfilled后, 把resolve的值返回
其实
await下面的代码就相当于这个promise的then部分, 用起来是不是更方便了 ☺️
那rejected 的部分怎么办?
- 用
try/catch包裹起来就好了!
async function getWeb(url: string) {
try {
const resp = await fetch(url);
console.log(`${resp.url}: ${resp.status}`);
// ...
} catch (error) {
console.error(error);
}
}
getWeb("https://www.bilibili.com");
// https://www.bilibili.com/: 200
New Promise()
ok,既然我们已经会使用 promise 了,让我们来写一个简单Promise 对象的的例子吧:
功能很简单,就是点击按钮,间隔 1s 后把 <h4> 标签的内容换成牛叫:
<template>
<h4>{{ show }}</h4>
<button @click="handleClick">Click!</button
><button @click="show = 'Hello Cow !'">Reset</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
const show = ref("Hello Cow !");
const callCowPromise = new Promise<T>((resolve: (value: T) => void) => {
setTimeout(() => {
resolve("🐮 Moo~");
}, 1000);
});
function handleClick() {
callCowPromise.then((value: string)=>{
show.value = value
})
show.value = "Wait...";
}
</script>
可以看到,在
handleClick()里先设置了callCowPromise的then函数,再执行show.value = "Wait...", 这部分是同步的噢
展示:
Hello Cow !
vuepress 可以直接渲染出来我们的代码 ☺️
ok, 仅针对 promise 对象说明一下:
- 我们在这里创建了一个 Promise 对象
callCowPromise, 它使用Promise类的构造函数 它返回一个Promise对象 - 构造函数参数为一个回调函数, 因为内容简单,在这里我执行写了回调函数,回调函数的参数
resolve也是个回调函数 - 第一个回调函数中,使用
setTimeout等待一秒,执行传入的回调函数resolve(),内容为牛叫。ok Promise 创建结束 callCowPromise是一个Promise对象,那么可以给它加上then函数,你已经注意到了,then函数中的参数即是resolve传入的牛叫
写一个 promise 对象挺简单的对吧,但其实还可以使用async/await简化一下我们的代码:
const callCow = () =>
new Promise<T>((resolve: (value: T) => void) => {
setTimeout(() => {
resolve("🐮 Moo~");
}, 1000);
});
async function handleClick() {
show.value = "Wait...";
show.value = await callCow();
}
首先 await 只能用在返回promise的函数上, 因此把callCow变成一个返回promise对象的函数, 其他部分不用改。
然后,await 的使用必须在 async 函数内,因此使用async修饰handleClick
使用await 在callCow()前,使得await callCow()返回的值变成了resolve()里传入的牛叫
完成 🎉
TIP
关于async handleClick() 产生的影响, async 会把函数变为异步的, 即进入函数后,先执行 show.value = "Wait..." 遇到需要等待的操作,则把它异步处理,同时执行函数外能马上执行的同步部分,而因为handleClick只是个事件函数,不会影响到上下文的执行顺序,因此没有关系
补充
resolve用来返回正确的结果, 那么错误的原因由reject来返回
const ezfunc = (value: string) =>
new Promise<string>((resolve: (resp: string) => void, reject) => {
if (value === "") reject(new Error("值不能为空!"));
resolve(`<${value}>`);
});
async function main() {
try {
console.log(await ezfunc("Hi ⭐️"));
console.log(await ezfunc(""));
} catch (error) {
console.error(error);
}
}
main();
// <Hi ⭐️>
// Error: 值不能为空!
// at file:///**/hello.ts:3:30
// at new Promise (<anonymous>)
// at ezfunc (file:///**/hello.ts:2:3)
// at main (file:///**/hello.ts:10:23)
workers
毕竟 js 还是单线程的嘛,有些情况下不能很好地发挥多核的性能,因此 workers 给了我们在不同线程中运行某些任务的能力
但是这是要付出代价的。对于多线程代码,你永远不知道你的线程什么时候将会被挂起,其他线程将会得到运行的机会。因此,如果两个线程都可以访问相同的变量,那么变量就有可能在任何时候发生意外的变化,这将导致很难发现的 Bug
为了避免 Web 中的这些问题,你的主代码和你的 worker 代码永远不能直接访问彼此的变量。Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互。这意味着 workers 不能访问 DOM(窗口、文档、页面元素等等)
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),且它所加载的脚本,必须来自网络。
有三类 workers:
- dedicated workers
- shared workers
- service workers
仅介绍第一类
创建 worker
在主线程中
const worker = new Worker("ezworker.ts");
构造函数的参数是脚本文件的地址,脚本必须来自网络。如果下载没有成功(比如 404 错误),Worker 就会默默地失败
通信
要与 worker 互动,只能通过发消息的形式
在主线程中, 使用 postMessage 发送一条消息给子线程
const worker = new Worker("ezworker.ts");
worker.postMessage("Hi my name is oooooooo");
postMessage 其实有两个参数
- message: 普通的消息可以是任何类型, 二进制也可以
- transferable objects: 可转移对象通常用于共享资源,该资源一次仅能安全地暴露在一个 JavaScript 线程中
要接收来自子线程的消息, 使用 onmessage指定监听函数
worker.onmessage = (event: MessageEvent) => {
console.log(`recv: ${event.data}`);
};
关闭 worker
Worker 线程一旦新建成功,就会始终运行,因此为了避免过度浪费资源, 一旦使用完毕,就应该关闭
主线程使用 worker.terminate(); 结束 worker 线程
worker 线程使用 self.close(); 关闭
worker
self.onmessage = (event) => {
self.postMessage(`💩💩💩${event.data}💩💩💩`);
};
例子
懒得写,deno 的 worker 和原版的不太一样 💩