Promise基础
1、 通过执行函数控制期约状态
Copy new Promise(() => setTimeout(console.log, 0, 'executor'));
setTimeout(console.log, 0, 'promise initialized');
// executor
// promise initialized
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p);
// 在console.log打印期约实例的时候,还不会执行超时回调(即resolve()) // Promise <pending>
2、为避免期约卡在待定状态,可以添加一个定时退出功能。比如,可以通过 setTimeout 设置一个 7 10 秒钟后无论如何都会拒绝期约的回调:
Copy let p = new Promise((resolve, reject) => { setTimeout(reject,10000); //10秒后调用reject() // 执行函数的逻辑
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log,11000,p); //11秒后再检查状态 // (After 10 seconds) Uncaught error
// (After 11 seconds) Promise <rejected>
3、 Promise.resolve() 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve()静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:
Copy let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法,如下所示:
Copy let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true
这个幂等性会保留传入期约的状态:
Copy
let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending> setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
4、 Promise.reject() 与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。下面的两个期约实例实际上是 一样的:
Copy let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
这个拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒 绝处理程序:
Copy let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:
Copy setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>
5、 同步/异步执行的二元性 Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式。下面的例子完美地展示 了这一点,其中包含了两种模式下抛出错误的情形:
Copy try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch(e) {
console.log(e);
}
// Uncaught (in promise) Error: bar
第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。乍一看这可能 有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由 的错误。这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式 的媒介。 在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法。
Promise实例方法
Promise.prototype.then()
如前所述,两个处理程序参数都是可选的。而且,传给 then()的任何非函数类型的参数都会被静 默忽略。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined。这 样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。
Copy function onResolved(id) {
setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
// 非函数处理程序会被静默忽略,不推荐
p1.then('gobbeltygook');
// 不传 onResolved 处理程序的规范写法
p2.then(null, () => onRejected('p2'));
// p2 rejected(3 秒后)
// Promise.prototype.then()方法返回一个新的期约实例:
let p1 = new Promise(() => {});
let p2 = p1.then();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined。
Copy let p1 = Promise.resolve('foo');
// 若调用then()时不传处理程序,则原样向后传 let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
// 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>:
setTimeout(console.log, 0, p4); // Promise <resolved>:
setTimeout(console.log, 0, p5); // Promise <resolved>:
Copy 如果有显式的返回值,则 Promise.resolve()会包装这个值: ...
// 这些都一样
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));
let p1 = Promise.reject('foo'); 13
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar
undefined
undefined
undefined
// Promise.resolve()保留返回的期约
let p8 = p1.then(() => new Promise(() => {})); let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>:
抛出异常会返回拒绝的期约:
Copy ...
let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected> baz
注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:
Copy ...
let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux
Promise.reject()替代之前例子中的 Promise.resolve()是一样的道理
Promise.prototype.catch()
Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。
下面的代码展示了这两种同样的情况:
Copy let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
Promise.prototype.catch()返回一个新的期约实例:
Copy let p1 = new Promise(() => {});
let p2 = p1.catch();
setTimeout(console.log, 0, p1);
setTimeout(console.log, 0, p2);
setTimeout(console.log, 0, p1 === p2); // false
拒绝期约与拒绝错误处理
Copy let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo
Copy Uncaught (in promise) Error: foo
at Promise (test.html:5)
at new Promise (<anonymous>)
at test.html:5
Uncaught (in promise) Error: foo
at Promise (test.html:6) 6 at new Promise (<anonymous>)
at test.html:6
Uncaught (in promise) Error: foo
at test.html:8
Uncaught (in promise) Error: foo
at Promise.resolve.then (test.html:7)
这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过 throw()关键字抛出错误时, JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令: 所有错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息展示了错误发生的路径。注意 错误的顺序:Promise.resolve().then()的错误最后才出现,这是因为它需要在运行时消息队列中 添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约。
Copy throw Error('foo'); console.log('bar'); // 这一行不会执行
// Uncaught Error: foo
但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时 继续执行同步指令:
Copy
Promise.reject(Error('foo')); console.log('bar');
// bar
// Uncaught (in promise) Error: foo
如本章前面的 Promise.reject()示例所示,异步错误只能通过异步的 onRejected 处理程序 捕获:
Copy // 正确
Promise.reject(Error('foo')).catch((e) => {});
// 不正确
try {
Promise.reject(Error('foo'));
} catch(e) {}
串行期约合成
Copy 用的合成函数可以这样实现:
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
let addTen = compose(addTwo, addThree, addFive); addTen(8).then(console.log); // 18
期约取消
Copy <body>
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
setTimeout(console.log, 0, "delay cancelled");
resolve();
});
})
}
}
const startButton = document.querySelector('#start');
const cancelButton = document.querySelector('#cancel');
function cancellableDelayedResolve(delay) {
setTimeout(console.log, 0, "set delay");
return new Promise((resolve, reject) => {
const id = setTimeout((() => {
setTimeout(console.log, 0, "delayed resolve");
resolve();
}), delay);
const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback));
cancelToken.promise.then(() => clearTimeout(id));
});
}
startButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
期约进度通知
Copy class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});
p.notify((x) => setTimeout(console.log, 0, 'progress:', x));
p.then(() => setTimeout(console.log, 0, 'completed'));
// (约 1 秒后)80% remaining
// (约 2 秒后)60% remaining
// (约 3 秒后)40% remaining
// (约 4 秒后)20% remaining
// (约 5 秒后)completed
// notify()函数会返回期约,所以可以连缀调用,连续添加处理程序。多个处理程序会针对收到的
// 每条消息分别执行一遍,如下所示:
p.notify((x) => setTimeout(console.log, 0, 'a:', x))
.notify((x) => setTimeout(console.log, 0, 'b:', x));
p.then(() => setTimeout(console.log, 0, 'completed'));
// (约 1 秒后) a: 80% remaining
// (约 1 秒后) b: 80% remaining
// (约 2 秒后) a: 60% remaining
// (约 2 秒后) b: 60% remaining
// (约 3 秒后) a: 40% remaining
// (约 3 秒后) b: 40% remaining
// (约 4 秒后) a: 20% remaining
// (约 4 秒后) b: 20% remaining
// (约 5 秒后) completed
// 总体来看,这还是一个比较粗糙的实现,但应该可以演示出如何使用通知报告进度了
Copy async function foo() {
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar() {
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 1
// 2
// 3
// 4
// 5
// 8
// 9
// 6
// 7
异步函数策略
实现 sleep()
Copy async function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function foo() {
const t0 = Date.now();
await sleep(1500); // 暂停约 1500 毫秒
console.log(Date.now() - t0);
}
foo();
// 1502
利用平行执行
Copy async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
await randomDelay(0);
await randomDelay(1);
await randomDelay(2);
await randomDelay(3);
await randomDelay(4);
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed
用一个 for 循环重写,就是:
Copy async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
for (let i = 0; i < 5; ++i) {
await randomDelay(i);
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed
就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序, 7 但总执行时间会变长。 如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:
Copy async function foo() {
const t0 = Date.now();
const p0 = randomDelay(0);
const p1 = randomDelay(1);
const p2 = randomDelay(2);
const p3 = randomDelay(3);
const p4 = randomDelay(4);
await p0;
await p1;
await p2;
await p3;
await p4;
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 2 finished
// 0 finished
// 1 finished
// 3 finished
// 4 finished
// 1002ms elapsed
用数组和 for 循环再包装一下就是:
Copy
async function foo() {
const t0 = Date.now();
const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
for (const p of promises) {
await p;
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// 877ms elapsed
注意,虽然期约没有按照顺序执行,但 await 按顺序收到了每个期约的值:
Copy async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve(id);
}, delay));
}
async function foo() {
const t0 = Date.now();
const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
for (const p of promises) { // 与Promise.all(promises)类似
console.log(`awaited ${await p}`);
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 1 finished
// 2 finished
// 4 finished
// 3 finished
// 0 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed
平行控制并发数
Copy
async function asyncPool(poolLimit, array, iteratorFn) {
const results = [];
const pending = [];
for (const item of array) {
const p = iteratorFn(item).then((result) => {
const index = pending.indexOf(p);
if (index !== -1) {
pending.splice(index, 1);
}
return result;
});
results.push(p);
if (poolLimit <= array.length) {
const e = p.then(() => pending.length);
console.log(e)
pending.push(e);
if (pending.length >= poolLimit) {
await Promise.race(pending);
}
}
}
return Promise.all(results);
}
async function randomDelay(id) {
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
const tasks = Array(5).fill(null).map((_, i) => i);
await asyncPool(3, tasks, randomDelay);
console.log(`${Date.now() - t0}ms elapsed`);
}
// asyncPool ES9版本 最新版
async function* asyncPool(concurrency, iterable, iteratorFn) {
const executing = new Set();
async function consume() {
const [promise, value] = await Promise.race(executing);
executing.delete(promise);
return value;
}
for (const item of iterable) {
// Wrap iteratorFn() in an async fn to ensure we get a promise.
// Then expose such promise, so it's possible to later reference and
// remove it from the executing pool.
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value]
);
executing.add(promise);
if (executing.size >= concurrency) {
yield await consume();
}
}
while (executing.size) {
yield await consume();
}
}
// asyncPool ES7版本 1.0版本
async function asyncPool(poolLimit, iterable, iteratorFn) {
const ret = [];
const executing = new Set();
for (const item of iterable) {
const p = Promise.resolve().then(() => iteratorFn(item, iterable));
ret.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
if (executing.size >= poolLimit) {
await Promise.race(executing);
}
}
return Promise.all(ret);
}
foo();
串行执行期约
这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约, 如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:
Copy async function addTwo(x) {return x + 2;}
async function addThree(x) {return x + 3;}
async function addFive(x) {return x + 5;}
async function addTen(x) {
for (const fn of [addTwo, addThree, addFive]) {
x = await fn(x);
}
return x;
}
addTen(9).then(console.log); // 19
栈追踪与内存管理
期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。看看下面的例子, 它展示了拒绝期约的栈追踪信息:
Copy function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, 'bar');
}
function foo() {
new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar // setTimeout
// setTimeout (async)
// fooPromiseExecutor
// foo
根据对期约的不同理解程度,以上栈追踪信息可能会让某些读者不解。栈追踪信息应该相当直接地 表现 JavaScript 引擎当前栈内存中函数调用之间的嵌套关系。在超时处理程序执行时和拒绝期约时,我 们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。可是,我们知道这 些函数已经返回了,因此栈追踪信息中不应该看到它们。 答案很简单,这是因为 JavaScript 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时, 调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息 会占用内存,从而带来一些计算和存储成本。 如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:
Copy function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, 'bar');
}
async function foo() {
await new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
// foo
// async function (async)
// foo
这样一改,栈追踪信息就准确地反映了当前的调用栈。fooPromiseExecutor()已经返回,所以 它不在错误信息中。但 foo()此时被挂起了,并没有退出。JavaScript 运行时可以简单地在嵌套函数中 存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上存储在内存中,可用于在出 错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以 优先考虑的。
使用 ReadableStream 主体
ReadableStream 暴露了 getReader()方法,用于产生 ReadableStream- DefaultReader,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 Uint8Array。
Copy fetch('https://fetch.spec.whatwg.org/')
.then((response) => response.body)
.then((body) => {
let reader = body.getReader();
function processNextChunk({ value, done }) {
if (done) {
return;
}
console.log(value);
return reader.read()
.then(processNextChunk);
}
return reader.read()
.then(processNextChunk);
});
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...
异步函数非常适合这样的 fetch()操作。可以通过使用 async/await 将上面的递归调用打平:
Copy fetch('https://fetch.spec.whatwg.org/')
.then((response) => response.body)
.then(async function (body) {
let reader = body.getReader();
while (true) {
if (done) {
break;
}
console.log(value);
}
});
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...
另外,read()方法也可以真接封装到 Iterable 接口中。因此就可以在 for-await-of 循环中方 便地实现这种转换:
Copy fetch('https://fetch.spec.whatwg.org/')
.then((response) => response.body)
.then(async function (body) {
let reader = body.getReader();
let asyncIterable = {
[Symbol.asyncIterator]() {
return {
next() {
return reader.read();
}
};
}
};
for await (chunk of asyncIterable) {
console.log(chunk);
}
});
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...
通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码。而且,这个实现通过支持只读 取部分流也变得更稳健。如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续 操作:
Copy async function* streamGenerator(stream) {
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
yield value;
}
} finally {
reader.releaseLock();
}
}
fetch('https://fetch.spec.whatwg.org/')
.then((response) => response.body)
.then(async function (body) {
for await (chunk of streamGenerator(body)) {
console.log(chunk);
}
});
异步迭代
创建并使用异步迭代器
要理解异步迭代器,最简单的办法是用它跟同步迭代器进行比较。下面代码中创建了一个简单的 Emitter 类,该类包含一个同步生成器函数,该函数会产生一个同步迭代器,同步迭代器输出 0~4:
Copy class Emitter {
constructor(max) {
this.max = max;
this.syncIdx = 0;
}
*[Symbol.iterator]() {
while (this.syncIdx <= this.max) {
yield this.syncIdx++;
}
}
}
const emitter = new Emitter(5);
function syncCount() {
const syncCounter = emitter[Symbol.iterator]();
for (const x of syncCounter) {
console.log(x);
}
}
syncCount();
// 0
// 1
// 2
// 3
// 4
这个例子之所以可以运行起来,主要是因为迭代器可以立即产生下一个值。假如你不想在确定下一 个产生的值时阻塞主线程执行,也可以定义异步迭代器函数,让它产生期约包装的值。 为此,要使用迭代器和生成器的异步版本。ECMAScript 2018 为此定义了 Symbol.asyncIterator, 以便定义和调用输出期约的生成器函数。同时,这一版规范还为异步迭代器增加了 for-await-of 循环, 用于使用异步迭代器。 相应地,前面的例子可以扩展为同时支持同步和异步迭代:
Copy class Emitter {
constructor(max) {
this.max = max; this.syncIdx = 0; this.asyncIdx = 0;
}
*[Symbol.iterator]() {
while (this.syncIdx < this.max) {
yield this.syncIdx++;
}
}
async *[Symbol.asyncIterator]() {
// *[Symbol.asyncIterator]() {
while (this.asyncIdx < this.max) {
// yield new Promise((resolve) => resolve(this.asyncIdx++));
yield this.asyncIdx++
}
}
}
const emitter = new Emitter(5);
function syncCount() {
const syncCounter = emitter[Symbol.iterator]();
for (const x of syncCounter) {
console.log(x);
}
}
async function asyncCount() {
const asyncCounter = emitter[Symbol.asyncIterator]();
for await (const x of asyncCounter) {
console.log(x);
}
}
syncCount();
// 0
// 1
// 2
// 3
// 4
asyncCount(); // 这里是串行异步迭代
// 0
// 1
// 2
// 3
// 4
为了加深理解,可以把前面例子中的同步生成器传给 for-await-of 循环:
Copy const emitter = new Emitter(5);
async function asyncIteratorSyncCount() {
const syncCounter = emitter[Symbol.iterator]();
for await (const x of syncCounter) {
console.log(x);
}
}
asyncIteratorSyncCount(); // 这里是同步迭代
// 0
// 1
// 2
// 3
// 4
虽然这里迭代的是同步生成器产生的原始值,但 for-await-of 循环仍像它们被包装在期约中一 样处理它们。这说明 for-await-of 循环可以流畅地处理同步和异步可迭代对象。但是常规 for 循环 就不能处理异步迭代器了:
Copy function syncIteratorAsyncCount() {
const asyncCounter = emitter[Symbol.asyncIterator]();
for (const x of asyncCounter) {
console.log(x);
}
}
syncIteratorAsyncCount();
// TypeError: asyncCounter is not iterable
关于异步迭代器,要理解的非常重要的一个概念是Symbol.asyncIterator符号不会改变生成器 函数的行为或者消费生成器的方式。注意在前面的例子中,生成器函数加上了 async 修饰符成为异步 函数,又加上了星号成为生成器函数。Symbol.asyncIterator 在这里只起一个提示的作用,告诉将 来消费这个迭代器的外部结构如 for-await-of 循环,这个迭代器会返回期约对象的序列。
处理异步迭代器的reject()
因为异步迭代器使用期约来包装返回值,所以必须考虑某个期约被拒绝的情况。由于异步迭代会按 顺序完成,而在循环中跳过被拒绝的期间是不合理的。因此,被拒绝的期约会强制退出迭代器:
Copy async *[Symbol.asyncIterator]() {
while (this.asyncIdx < this.max) {
if(this.asyncIdx < 3) {
yield this.asyncIdx;
} else {
throw 'Exited loop'
}
}
}
asyncCount();
// 0
// 1
// 2
// Uncaught (in promise) Exited loop
使用next()手动异步迭代
for-await-of 循环提供了两个有用的特性:一是利用异步迭代器队列保证按顺序执行,二是隐藏 异步迭代器的期约。不过,使用这个循环会隐藏很多底层行为。 因为异步迭代器仍遵守迭代器协议,所以可以使用 next()逐个遍历异步可迭代对象。如前所述, next()返回的值会包含一个期约,该期约可解决为{ value, done }这样的迭代结果。这意味着必须 使用期约 API 获取方法,同时也意味着可以不使用异步迭代器队列。
Copy const emitter = new Emitter(5);
const asyncCounter = emitter[Symbol.asyncIterator]();
console.log(asyncCounter.next());
// Promise<{value, done}>
参考资料