Promise

Promise基础

1、 通过执行函数控制期约状态

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 秒钟后无论如何都会拒绝期约的回调:

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()静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve, reject) => resolve()); 
let p2 = Promise.resolve();

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法,如下所示:

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

这个幂等性会保留传入期约的状态:


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 捕获,而只能通过拒绝处理程序捕获)。下面的两个期约实例实际上是 一样的:

let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();

这个拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒 绝处理程序:

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()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>

5、 同步/异步执行的二元性 Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式。下面的例子完美地展示 了这一点,其中包含了两种模式下抛出错误的情形:

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。这 样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。

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。

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>:
如果有显式的返回值,则 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>:

抛出异常会返回拒绝的期约:

...
let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10);  // Promise <rejected> baz

注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:

...
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)。

下面的代码展示了这两种同样的情况:

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()返回一个新的期约实例:

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

拒绝期约与拒绝错误处理

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
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()的错误最后才出现,这是因为它需要在运行时消息队列中 添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约。

 throw Error('foo'); console.log('bar'); // 这一行不会执行
// Uncaught Error: foo

但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时 继续执行同步指令:


 Promise.reject(Error('foo')); console.log('bar');
// bar
// Uncaught (in promise) Error: foo

如本章前面的 Promise.reject()示例所示,异步错误只能通过异步的 onRejected 处理程序 捕获:

// 正确
Promise.reject(Error('foo')).catch((e) => {});
// 不正确
try {
Promise.reject(Error('foo'));
} catch(e) {}

串行期约合成

用的合成函数可以这样实现:
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

期约取消

<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>

期约进度通知

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
// 总体来看,这还是一个比较粗糙的实现,但应该可以演示出如何使用通知报告进度了
  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()

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

利用平行执行

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 循环重写,就是:

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 但总执行时间会变长。 如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:

  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 循环再包装一下就是:


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 按顺序收到了每个期约的值:

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

平行控制并发数



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 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约, 如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:

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

栈追踪与内存管理

期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。看看下面的例子, 它展示了拒绝期约的栈追踪信息:

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 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时, 调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息 会占用内存,从而带来一些计算和存储成本。 如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

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。

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 将上面的递归调用打平:

  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 循环中方 便地实现这种转换:

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 }
// ...

通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码。而且,这个实现通过支持只读 取部分流也变得更稳健。如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续 操作:

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:

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 循环, 用于使用异步迭代器。 相应地,前面的例子可以扩展为同时支持同步和异步迭代:

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 循环:

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 循环 就不能处理异步迭代器了:

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()

因为异步迭代器使用期约来包装返回值,所以必须考虑某个期约被拒绝的情况。由于异步迭代会按 顺序完成,而在循环中跳过被拒绝的期间是不合理的。因此,被拒绝的期约会强制退出迭代器:

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 获取方法,同时也意味着可以不使用异步迭代器队列。

const emitter = new Emitter(5);
const asyncCounter = emitter[Symbol.asyncIterator]();
console.log(asyncCounter.next());
// Promise<{value, done}>

参考资料

Last updated