>  기사  >  웹 프론트엔드  >  JavaScript에서 Generator를 사용하는 방법에 대한 자세한 예

JavaScript에서 Generator를 사용하는 방법에 대한 자세한 예

小云云
小云云원래의
2017-12-29 14:12:311499검색

이 글에서는 주로 JavaScript에서 Generator를 사용하는 방법을 공유합니다. Generator는 매우 강력한 구문이지만 널리 사용되지는 않습니다(아래 트위터 설문조사 참조!). 왜 그럴까요? async/await에 비해 사용이 더 복잡하고 디버깅이 쉽지 않습니다(대부분의 경우는 과거로 거슬러 올라갑니다). 매우 간단한 방법으로 비슷한 경험을 얻을 수 있음에도 불구하고 사람들은 일반적으로 async/await를 선호합니다.

그러나 Generator를 사용하면 Yield 키워드를 통해 자체 코드를 반복할 수 있습니다! 이것은 매우 강력한 구문이며 실제로 실행을 조작할 수 있습니다! 덜 명확한 취소 작업부터 시작하여 동기화 작업부터 시작해 보겠습니다.

기사에 언급된 기능에 대한 코드 저장소를 만들었습니다 - github.com/Bloomca/obs…

일괄 처리(또는 계획)

생성기 기능을 실행하면 트래버스 개체가 반환됩니다. 즉, 이를 통해 우리는 다음을 수행할 수 있습니다. 동기적으로 통과해야 합니다. 우리는 왜 이것을 하고 싶은가? 그 이유는 일괄 처리를 구현하기 위한 것일 수 있습니다. 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 내에서 처리하면 Promise가 자체적으로 호출하여 동일한 지점으로 다시 돌아갈 수 있습니다. 그리고 이것은 아직 보편적인 솔루션이 아닙니다. 안타깝게도 Generator조차도 여기서 우리를 도울 수 없습니다. 우리는 Generator의 한계를 발견했습니다. 실행 흐름을 제어할 수는 있지만 Generator 함수의 본체를 이동할 수 없으므로 뒤로 물러서서 명령을 다시 실행할 수 없습니다. 가능한 해결책은 항복 결과의 데이터 구조를 알려주는 명령 패턴을 사용하는 것입니다. 이 명령을 다시 실행할 수 있도록 이 명령을 실행하는 데 필요한 모든 정보여야 합니다. 따라서 우리의 방법은 다음과 같이 변경되어야 합니다:

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 某些问题可以被优雅和简洁的处理。

相关推荐:

Promise,Generator(生成器),async(异步)函数的用法

JS异步实现Generator的方法

详解yield和Generators生成器

위 내용은 JavaScript에서 Generator를 사용하는 방법에 대한 자세한 예의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.