Generator는 매우 강력한 구문이지만 널리 사용되지는 않습니다. 이 글은 주로 JavaScript에서 Generator를 사용하는 방법을 소개합니다. 필요한 친구들이 참고하면 됩니다.
Generator는 매우 강력한 구문이지만 널리 사용되지는 않습니다(아래 트위터 설문조사 참조!). 왜 그럴까요? async/await에 비해 사용이 더 복잡하고 디버깅이 쉽지 않습니다(대부분의 경우는 과거로 거슬러 올라갑니다). 매우 간단한 방법으로 비슷한 경험을 얻을 수 있음에도 불구하고 사람들은 일반적으로 async/await를 선호합니다.
그러나 Generator를 사용하면 Yield 키워드를 통해 자체 코드를 반복할 수 있습니다! 이것은 매우 강력한 구문이며 실제로 실행을 조작할 수 있습니다! 덜 명확한 취소 작업부터 시작하여 동기화 작업부터 시작해 보겠습니다.
기사에 언급된 기능에 대한 코드 저장소를 만들었습니다 - github.com/Bloomca/obs…
Batch(또는 계획)
Generator 기능을 실행하면 traverser 개체가 반환됩니다. 즉, Through it을 의미합니다. 동기적으로 탐색할 수 있습니다. 우리는 왜 이것을 하고 싶은가? 그 이유는 일괄 처리를 구현하기 위한 것일 수 있습니다. 1000개의 항목을 다운로드하고 테이블에 행별로 표시해야 한다고 상상해 보세요(프레임워크를 사용하지 않는다고 가정하고 이유는 묻지 마세요). 즉시 보여주는 것은 아무런 문제가 없지만 때로는 그것이 최선의 해결책이 아닐 수도 있습니다. 아마도 MacBook Pro는 쉽게 처리할 수 있지만 일반 사람의 컴퓨터는 그렇지 못할 수도 있습니다(휴대폰은 말할 것도 없고). 따라서 이는 어떻게든 실행을 지연해야 함을 의미합니다.
이 예는 성능 최적화에 관한 것이므로 이 문제가 발생하기 전에는 이 작업을 수행할 필요가 없습니다. 조기 최적화는 모든 악의 근원입니다.
// 最初的同步实现版本 function renderItems(items) { for (item of items) { renderItem(item); } } // 函数将由我们的执行器遍历执行 // 实际上,我们可以用相同的同步方式来执行它! function* renderItems(items) { // 我使用 for..of 遍历方法来避免新函数的产生 for (item of items) { yield renderItem(item); } }
차이가 없겠죠? 여기서 차이점은 이제 소스 코드를 변경하지 않고도 이 함수를 다르게 실행할 수 있다는 것입니다. 실제로 앞서 언급했듯이 기다릴 필요가 없으며 동기식으로 수행할 수 있습니다. 이제 코드를 수정해 보겠습니다. 각 산출 후 4ms(JavaScript VM에서는 하트비트 1개)의 지연을 추가하는 것은 어떻습니까? 1000개의 항목이 있고 렌더링에는 4초가 걸립니다. 나쁘지 않습니다. 2초 안에 렌더링한다고 가정하면, 생각하기 쉬운 방법은 한 번에 2개를 렌더링하는 것입니다. 갑자기 Promise를 사용하는 솔루션이 더욱 복잡해졌습니다. 매번 렌더링할 항목 수라는 또 다른 매개변수를 전달해야 합니다. 실행기를 통해 이 매개변수를 전달해야 하지만, renderItems 메서드에 전혀 영향을 미치지 않는다는 이점이 있습니다.
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 메서드에 영향을 주지 않습니다.
Cancel
전통적인 기능인 취소를 고려해 보겠습니다. 나는 이에 대해 내 기사인 약속 취소 일반(번역: 약속을 취소하는 방법)에서 자세히 설명했습니다. 그래서 다음 코드 중 일부를 사용하겠습니다.
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행의 데이터를 렌더링하고 있는데 매우 느리다고 가정해 보겠습니다. 사용자에게 렌더링을 일시 중지/재개하여 모든 백그라운드 작업을 중지하고 다운로드된 내용을 읽을 수 있는 기능을 제공하고 싶습니다. 시작해 봅시다!
// 实现渲染的方法还是一样的 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
Async/await 是现在的首选语法(甚至 co 也谈到了它 ),这也是未来。但是,Generator 也在 ECMAScript 标准内,这意味着为了使用它们,除了写几个工具函数,你不需要任何东西。我试图向你们展示一些不那么简单的例子,这些实例的价值取决于你的看法。请记住,没有那么多人熟悉 Generator,并且如果在整个代码库中只有一个地方使用它们,那么使用 Promise 可能会更容易一些 —— 但是另一方面,通过 Generator 某些问题可以被优雅和简洁的处理。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
위 내용은 JavaScript에서 Generator 메소드를 사용하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!