- 原文地址:How to Use Generators in JavaScript
- 原文做者:Seva Zaikov
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:jonjia
- 校对者:vuuihc congFly
Generator 是一种很是强力的语法,但它的使用并不普遍(参见下图 twitter 上的调查!)。为何这样呢?相比于 async/await,它的使用更复杂,调试起来也不太容易(大多数状况又回到了从前),即便咱们能够经过很是简单的方式得到相似体验,可是人们通常会更喜欢 async/await。html
然而,Generator 容许咱们经过 yield
关键字遍历咱们本身的代码!这是一种超级强大的语法,实际上,咱们能够操纵执行过程!从不太明显的取消操做开始,让咱们先从同步操做开始吧。前端
我为文中提到的功能建立了一个代码仓库 —— github.com/Bloomca/obs…android
执行 Generator 函数会返回一个遍历器对象,那意味着经过它咱们能够同步地遍历。为何咱们想这么作?缘由有多是为了实现批处理。想象一下,咱们须要下载 1000 个项目,并在表格中逐行的显示它们(不要问我为何,假设咱们不使用框架)。虽然马上展现它们没有什么很差的,但有时这可能不是最好的解决方案 —— 也许你的 MacBook Pro 能够轻松处理它,但普通人的电脑不能(更别说手机了)。因此,这意味着咱们须要用某种方式延迟执行。ios
请注意,这个例子是关于性能优化,在你遇到这个问题以前,不必这样作 —— 过早优化是万恶之源!git
// 最初的同步实现版本
function renderItems(items) {
for (item of items) {
renderItem(item);
}
}
// 函数将由咱们的执行器遍历执行
// 实际上,咱们能够用相同的同步方式来执行它!
function* renderItems(items) {
// 我使用 for..of 遍历方法来避免新函数的产生
for (item of items) {
yield renderItem(item);
}
}
复制代码
没有什么区别是吧?那么,这里的区别在于,如今咱们能够在不改变源代码的状况下以不一样方式运行这个函数。实际上,正如我以前提到的,没有必要等待,咱们能够同步执行它。因此,来调整下咱们的代码。在每一个 yield
后边加一个 4 ms(JavaScript VM 中的一个心跳) 的延迟怎么样?咱们有 1000 个项目,渲染将须要 4 秒 —— 还不错,假设我想在 2 秒以内渲染完毕,很容易想到的方法是每次渲染 2 个。忽然使用 Promise 的解决方案将变得更加复杂 —— 咱们必需要传递另外一个参数:每次渲染的项目个数。经过咱们的执行器,咱们仍然须要传递这个参数,但好处是对咱们的 renderItems
方法彻底没有影响。github
function runWithBatch(chunk, fn, ...args) {
const gen = fn(...args);
let num = 0;
return new Promise((resolve, promiseReject) => {
callNextStep();
function callNextStep(res) {
let result;
try {
result = gen.next(res);
} catch (e) {
return reject(e);
}
next(result);
}
function next({ done, value }) {
if (done) {
return resolve(value);
}
// every chunk we sleep for a tick
if (num++ % chunk === 0) {
return sleep(4).then(proceed);
} else {
return proceed();
}
function proceed() {
return callNextStep(value);
}
}
});
}
// 第一个参数 —— 每批处理多少个项目
const items = [...];
batchRunner(2, function*() {
for (item of items) {
yield renderItem(item);
}
});
复制代码
正如你所看到的,咱们能够轻松改变每批处理项目的个数,不去考虑执行器,回到正常的同步执行方式 —— 全部这些都不会影响咱们的 renderItems
方法。后端
咱们来考虑下传统的功能 —— 取消。在我 promises cancellation in general (译文:如何取消你的 Promise?) 这篇文章中已经详细谈到了。因此我会使用其中一些代码:promise
function runWithCancel(fn, ...args) {
const gen = fn(...args);
let cancelled, cancel;
const promise = new Promise((resolve, promiseReject) => {
// define cancel function to return it from our fn
// 定义 cancel 方法,并返回它
cancel = () => {
cancelled = true;
reject({ reason: 'cancelled' });
};
onFulfilled();
function onFulfilled(res) {
if (!cancelled) {
let result;
try {
result = gen.next(res);
} catch (e) {
return reject(e);
}
next(result);
return null;
}
}
function onRejected(err) {
var result;
try {
result = gen.throw(err);
} catch (e) {
return reject(e);
}
next(result);
}
function next({ done, value }) {
if (done) {
return resolve(value);
}
// 假设咱们老是接收 Promise,因此不须要检查类型
return value.then(onFulfilled, onRejected);
}
});
return { promise, cancel };
}
复制代码
这里最好的部分是咱们能够取消全部还没来得及执行的请求(也能够给咱们的执行器传递相似 AbortController 的对象参数,因此它甚至能够取消当前的请求!),并且咱们没有修改过本身业务逻辑中的一行的代码。性能优化
另外一个特殊的需求多是暂停/恢复功能。你为何想要这个功能?想象一下,咱们渲染了 1000 行数据,并且速度很是慢,咱们但愿给用户提供暂停/恢复渲染的功能,这样他们就能够中止全部的后台工做读取已经下载的内容了。让咱们开始吧!bash
// 实现渲染的方法仍是同样的
function* renderItems() {
for (item of items) {
yield renderItem(item);
}
}
function runWithPause(genFn, ...args) {
let pausePromiseResolve = null;
let pausePromise;
const gen = genFn(...args);
const promise = new Promise((resolve, reject) => {
onFulfilledWithPromise();
function onFulfilledWithPromise(res) {
if (pausePromise) {
pausePromise.then(() => onFulfilled(res));
} else {
onFulfilled(res);
}
}
function onFulfilled(res) {
let result;
try {
result = gen.next(res);
} catch (e) {
return reject(e);
}
next(result);
return null;
}
function onRejected(err) {
var result;
try {
result = gen.throw(err);
} catch (e) {
return reject(e);
}
next(result);
}
function next({ done, value }) {
if (done) {
return resolve(value);
}
// 假设咱们老是接收 Promise,因此不须要检查类型
return value.then(onFulfilledWithPromise, onRejected);
}
});
return {
pause: () => {
pausePromise = new Promise(resolve => {
pausePromiseResolve = resolve;
});
},
resume: () => {
pausePromiseResolve();
pausePromise = null;
},
promise
};
}
复制代码
调用这个执行器,能够给咱们返回一个具备暂停/恢复功能的对象,全部这些均可以轻松获得,仍是使用咱们以前的业务代码!因此,若是你有不少"沉重"的请求链,须要耗费很长时间,而你想给你的用户提供暂停/恢复功能的话,你能够随意在你的代码中实现这个执行器。
咱们有个神秘的 onRejected
调用,这是咱们这部分谈论的主题。若是咱们使用正常的 async/await 或 Promise 链式写法,咱们将经过 try/catch 语句来进行错误处理,若是不添加大量的逻辑代码就很难进行错误处理。一般状况下,若是咱们须要以某种方式处理错误(好比重试),咱们只是在 Promise 内部进行处理,这将会回调本身,可能再次回到一样的点。并且,这还不是一个通用的解决方案 —— 可悲的是,在这里甚至 Generator 也不能帮助咱们。咱们发现了 Generator 的局限 —— 虽然咱们能够控制执行流程,但不能移动 Generator 函数的主体;因此咱们不能后退一步,从新执行咱们的命令。一个可行的解决方案是使用 command pattern, 它告诉了咱们 yield
结果的数据结构 —— 应该是咱们须要执行此命令须要的全部信息,这样咱们就能够再次执行它了。因此,咱们的方法须要改成:
function* renderItems() {
for (item of items) {
// 咱们须要将全部东西传递出去:
// 方法, 内容, 参数
yield [renderItem, null, item];
}
}
复制代码
正如你所看到的,这使得咱们不清楚发生了什么 —— 因此,也许最好是写一些 wrapWithRetry
方法,它会检查 catch
代码块中的错误类型并再次尝试。可是咱们仍然能够作一些不影响咱们功能的事情。例如,咱们能够增长一个关于忽略错误的策略 —— 在 async/await 中咱们不得不使用 try/catch 包装每一个调用,或者添加空的 .catch(() => {})
部分。有了 Generator,咱们能够写一个执行器,忽略全部的错误。
function runWithIgnore(fn, ...args) {
const gen = fn(...args);
return new Promise((resolve, promiseReject) => {
onFulfilled();
function onFulfilled(res) {
proceed({ data: res });
}
// 这些是 yield 返回的错误
// 咱们想忽略它们
// 因此咱们像往常同样作,但不去传递出错误
function onRejected(error) {
proceed({ error });
}
function proceed(data) {
let result;
try {
result = gen.next(data);
} catch (e) {
// 这些错误是同步错误(好比 TypeError 等)
return reject(e);
}
// 为了区分错误和正常的结果
// 咱们用它来执行
next(result);
}
function next({ done, value }) {
if (done) {
return resolve(value);
}
// 假设咱们老是接收 Promise,因此不须要检查类型
return value.then(onFulfilled, onRejected);
}
});
}
复制代码
Async/await 是如今的首选语法(甚至 co 也谈到了它 ),这也是将来。可是,Generator 也在 ECMAScript 标准内,这意味着为了使用它们,除了写几个工具函数,你不须要任何东西。我试图向大家展现一些不那么简单的例子,这些实例的价值取决于你的见解。请记住,没有那么多人熟悉 Generator,而且若是在整个代码库中只有一个地方使用它们,那么使用 Promise 可能会更容易一些 —— 可是另外一方面,经过 Generator 某些问题能够被优雅和简洁的处理。
明智地选择 —— 能力越大,责任越重(蜘蛛侠 2,2004)!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。