JavaScript 异步方案整理
相关章节
图解JavaScript——代码实现(Object.create()、flat()等十四种代码原理实现不香吗?)
图解JavaScript——基础篇
图解 JavaScript——进阶篇
图解23种设计模式(TypeScript版)
参考链接
Promise/A+
Promise的源码实现
ES6 入门教程
六种异步方案
本节主要阐述六种异步方案:回调函数、事件监听、发布/订阅、Promise、Generator 和 Async。其中重点是发布/订阅、Promise、Async 的原理实现。
回调函数
异步编程最基本的方法,把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
- 优点:简单、容易理解和实现
- 缺点:多次调用会使代码结构混乱,形成回调地狱
1
2
3
4
5
6function sleep(time, callback) {
setTimeout(() => {
// 一些逻辑代码
callback();
}, time);
}
事件监听
异步任务的执行不取决于代码的执行顺序,而取决于某个事件是否发生。
- 优点:易于理解,此外对于每个事件可以指定多个回调函数,而且可以“去耦合”,有利于实现模块化
- 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰
1
2
3dom.addEventListener('click', () => {
console.log('dom 被点击后出发!!!');
});
发布/订阅
发布/订阅模式是在观察着的基础上,在目标和观察者之间增加一个调度中心。订阅者(观察者)吧自己想要订阅的时间注册到调度中心,当该事件触发的时候,发布者(目标)发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
Promise
Promise 是异步编程的一种解决方案,是为解决回调函数回调地狱问题而提出的,它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套改为链式调用。
- 优点:将回调函数的嵌套改为链式调用;使用 then 方法之后,异步任务两端执行看的更加清楚
- 缺点:Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得不清楚
1
2
3
4
5
6
7
8
9
10
11
12
13const promise = new Promise((resolve, reject) => {
if (/* 如果异步成功 */) {
resolve(value);
} else {
reject(error);
}
});
promise.then((value) => {
// ...success
}, (reason) => {
// ...failure
});
Generator
Generator 函数时 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。其最大的特点是可以空时函数的执行。
- 优点:异步操作表示很简洁,此外可以控制函数的执行。
- 缺点:流程管理不方便,不能实现自动化的流程管理
1
2
3
4
5
6
7
8
9
10
11function* genF() {
yield 'come on!';
yield 'Front End Engineer';
return 'goood';
}
const gF = genF();
gF.next(); // {value: 'come!', done: false}
gF.next(); // {value: 'Front End Engineer', done: false}
gF.next(); // {value: 'good', done: true}
gF.next(); // {value: undefined, done: true}
Async
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。简言之,该函数就是 Generator 函数的语法糖。
- 优点:内置执行器,可以自动执行;语义相比 Generator 更加清晰;返回值是 Promise,比 Generator 函数的返回值是 It二胺投入对象操作更加方便。
- 缺点:增加学习成本
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
33async function asyncFun() {
await func1();
await func2();
return '666';
}
function func1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('888');
}, 100);
}).then((value) => {
console.log(value);
});
}
function func2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('777');
}, 100);
}).then((value) => {
console.log(value);
});
}
asyncFun().then((value) => {
console.log(value);
});
// 888
// 777
// 666
Promise 原理实现
不管是实际开发还是面试过程中,都会遇到 Promise 的实现原理,下面我们根据 PromiseA+规范来进行实现,然后对其相关的静态方法(Promise.resolve(), Promise.reject(), Promise.all(), Promise.race())和实例方法 (Promise.prototype.catch(), Promise.prototype.finally())进行实现。
思考一下
首先用一幅图来展示我考虑实现这个函数的思路
根据 Promise/A+ 规范实现 Promise
人家有相关标准,我们就要遵守,笔记遵纪守法才是好公民,现在只能硬着头皮把这个标准过一遍。
- Promise 状态:Promise 状态转换只能从 pending 转换至 fulfilled or rejected。转换成功则有其成功的 value 值,失败则有失败的 reason 值。
- 状态分类:pending(进行中)、fulfilled(已成功)、rejected(已失败)
- 状态转换:
- pending 转换至 fulfilled or rejected
- fulfilled 不能转换状态(必须有一个 value 值)
- rejected 不能转变状态(必须有一个 reject 的 reason)
- 必须提供then 方法接受来访问最终结果(promise.then(onFulfilled, onRejected))
- onFulfilled 和 onRejected 是可选参数
- 如果 onFulfilled 不是一个函数,它必须被忽略
- 如果 onRejected 不是一个函数,它必须被忽略
- 如果 onFulfilled 是一个函数
- 在状态变为 fulfilled 时被调用,且 promise 的值作为其第一个参数
- 状态不是 fulfilled 不能被调用
- 最多调用一次
- 如果 onRejected 是一个函数
- 在状态变为 rejected 被调用,且 promise 的值作为其第一个参数
- 状态不是 rejected 不能被调用
- 最多调用一次
- 在执行上下文栈只包含平台代码之前,onFulfilled 和 onRejected 不能被调用。由于 promise 被考虑为“平台代码”,所以在自身处理程序被调用时可能已经包含一个任务调度队列。
- onFulfilled 和 onRejected 必须被作为函数调用
- then 可以被调用多次
- 如果 promise 变为 fulfilled 状态,则所有的onFulfilled 按照 then 的原始调用顺序执行
- 如果 promise 变为 rejected 状态,则所有的 onRejected 按照then 的原始调用顺序执行
- then 必须返回一个 promise
promise2 = promise1.then(onFulfilled, onRejected);
- onFulfilled 或 onRejected 返回的结果为 x,调用
[[resolve]](promise2, x)
- onFulfilled 或 onRejected 抛出一个异常 e, promise 必须以 e 的理由失败
- 如果 onFulfilled 不是一个函数且状态为 fulfilled,则 promise2 也应该是 fulfilled 状态且传递的 value 为 promise1 的 value
- 如果 onRejected 不是一个函数且状态为 rejected,则 promise2 也应该是 rejected 状态且传递的 reason 为 promise1 的 reason
- onFulfilled 或 onRejected 返回的结果为 x,调用
- onFulfilled 和 onRejected 是可选参数
- The PromiseResolution Procedure - 为了运行
[[Resolve]](promise, x)
执行以下步骤- 如果 promise 和 x 引用同一个对象,会报 TypeError 错误
- 如果 x 是一个 promise
- 如果 x 是 pending 状态,则会保持 pending 状态直到 fulfilled 状态或 rejected 状态
- 如果 x 是 fulfilled 状态,promise 也是 fulfilled 且传给改 value 值
- 如果 x 是 rejected 状态,promise 也是 rejected 且传给该 reason 值
- 如果 x 是一个对象或函数
- 让 then 变成 x.then
- 如果检索属性 x.then 抛出异常 e,则以 e 为原因拒绝 promise
- 如果 then 是一个函数,用 x 作为 this 调用它。第一个参数是 resolvePromise,第二个参数是 rejectPromise
- 如果 resolvePromise 用一个值 y 调用,运行
[[Resolve]](promise, y)
- 如果 rejectPromise 用一个原因 r 调用,用 r 拒绝 promise
- 如果 resolvePromise 和 rejectPromise 都被调用,或者对同一个参数进行多次调用,那么第一次调用优先,以后的调用都会被忽略
- 如果调用 then 抛出异常
- 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略
- 否则以 e 作为理由拒绝 promise
- 如果 then 不是一个函数,变为 fulfilled 状态并传值为 x
- 如果 x 不是一个对象获函数,状态变为 fulfilled 并传值 x
下面就是基于 Promise/A+ 规范实现的代码,已经经过 promise-aplus-tests 库进行了验证
1 | const PENDING = 'pending'; |
其它方法
按照 Promise/A+ 规范实现了 Promise 的核心内容,但是其只实现了 Promise.prototype.then() 方法,那其它方法呢?下面我们就唠一唠其它方法,包括静态方法(Promise.resolve()、Promise.reject()、Promise.all()、Promise.race()) 和实例方法(Promise.prototype.catch()、Promise.prototype.finally())。
- 其他方法
- 静态方法
- Promise.resolve()
- Promise.reject()
- Promise.all()
- Promise.race()
- 实例方法
- Promise.prototype.catch()
- Promise.prototype.finally()
- 静态方法
Promise.resolve()
将现有对象转换为 Promise 对象
参数说明:
- 参数是一个 Promise 实例(如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动的返回这个实例)
- 参数是一个 thenable 对象(具有 then 方法的对象),Promise.resolve 方法会将这个对象转化为 Promise 对象,然后就立即执行 thenable对象的 then 方法
- 参数不是具有 then 方法的对象,或根本就不是对象,Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved
- 不带有任何参数,直接返回一个 resolved 状态的 Promise 对象
1 | class Promise { |
Promise.reject()
返回一个新的 Promise 实例,该实例的状态为 rejected。
注意:Promise.reject() 方法的参数,会原封不动的作为 reject 的理由,变成后续方法的参数
1 | class Promise { |
Promise.all()
用于将多个 Promise 实例,包装秤一个新的 Promise 实例const p = Promise.all([p1, p2, p3])
接收数组(或者具备 Iterator)作为参数,p1, p2, p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
只有所有状态都变为 fulfilled,p 的状态才会是 fulfilled,p1, p2, p3 的返回值组成一个数组,传递给 p 的回调函数;否则是 rejected 状态,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
1 | class Promise { |
Promise.race()
const p = Promise.race([p1, p2, p3])
只要 p1、p2、p3 之中有一个实例率先改变状态,状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给回调函数。
1 | class Promise { |
Promise.catch()
是 .then(null, rejection) 或 .then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。
Promise 对象的错误具有 “冒泡”性质,会一直向后传递,知道被捕获为止。也就是说,错误总会被下一个 catch 语句捕获。
1 | class Promise { |
Promise.finally()
用于指定不管 Promise 对象最后状态如何,都会执行的操作
本质是 then 方法的特例
1 | class Promise { |
Async 原理实现
在开发过程中常用的另一种异步方案莫过于 Async,通过 async 函数的引入使得异步操作变得更加方便。实质上,async 是 Generator 的语法糖,最大的亮点是 async 内置执行器,调用后即可自动执行,不像 Generator 需要调用 next() 方法才能执行。
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。简言之,该函数就是 Generator 函数的语法糖。
优点:内置执行器,可以自动执行;语义相比 Generator 更加清晰;返回值是 Promise,比 Generator 函数的返回值是 Iterator 对象操作更加方便。
缺点:增加学习成本。
async 函数对 Generator 函数的改进:
- 内置执行器。Generator 函数的执行必须依靠执行器
- 更好的语义。async 和 await 比起 星号(*)和 yield 语义更加清楚了
- 适用度更广。co 模块约定,yield 命令后面只能是 Thunk 函数或者 Promise 对象,而 async 函数的 await 命令后面,可以说 Promise 对象和原始类型的值
- 返回值是 Promise。async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便,可以用 then 指示下一步操作
这是 Async 的实现原理,即将 Generator 函数作为参数放入 run 函数中,最终实现自动执行并返回 Promise 对象。
1 | function run(genF) { |
发布/订阅实现
发布/订阅模式在观察着模式的基础上,在目标和观察者之间增加了一个调度中心。订阅者(观察者)把自己想要订阅的事件注册到调度中心,当该事件出发的时候,发布者(目标)发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
1 | // 发布订阅(TypeScript 版) |