Heim  >  Artikel  >  Web-Frontend  >  So verwenden Sie Generatormethoden in JavaScript

So verwenden Sie Generatormethoden in JavaScript

亚连
亚连Original
2018-06-14 16:08:541438Durchsuche

Generator ist eine sehr leistungsfähige Syntax, die jedoch nicht weit verbreitet ist. In diesem Artikel wird hauptsächlich die Verwendung von Generator in JavaScript vorgestellt. Freunde, die es benötigen, können sich auf

Generator beziehen. Es handelt sich um eine sehr leistungsfähige Syntax, deren Verwendung jedoch nicht weit verbreitet ist (siehe die Umfrage auf Twitter unten!). Warum ist das so? Im Vergleich zu async/await ist seine Verwendung komplizierter und das Debuggen ist nicht einfach (die meisten Fälle gehören der Vergangenheit an). Obwohl wir auf sehr einfache Weise eine ähnliche Erfahrung erzielen können, bevorzugen die Leute im Allgemeinen async/await.

Generator ermöglicht uns jedoch, über das Schlüsselwort yield über unseren eigenen Code zu iterieren! Das ist eine superstarke Syntax und wir können die Ausführung tatsächlich manipulieren! Beginnen wir mit den weniger offensichtlichen Löschvorgängen und beginnen wir mit den Synchronisierungsvorgängen.

Ich habe ein Code-Repository für die im Artikel erwähnten Funktionen erstellt – github.com/Bloomca/obs…

Batch (oder Zeitplan)

Das Ausführen der Generator-Funktion gibt ein Traverser-Objekt zurück, was bedeutet, dass wir es synchron durchlaufen können. Warum wollen wir das tun? Der Grund kann darin liegen, eine Stapelverarbeitung zu implementieren. Stellen Sie sich vor, wir müssen 1000 Elemente herunterladen und sie Zeile für Zeile in einer Tabelle anzeigen (fragen Sie mich nicht nach dem Grund, vorausgesetzt, wir verwenden kein Framework). Es ist zwar nichts Falsches daran, sie sofort zur Schau zu stellen, aber manchmal ist das vielleicht nicht die beste Lösung – vielleicht kommt Ihr MacBook Pro damit problemlos zurecht, der Computer eines Durchschnittsmenschen jedoch nicht (ganz zu schweigen von einem Telefon). Das bedeutet also, dass wir die Ausführung irgendwie verzögern müssen.

Bitte beachten Sie, dass es in diesem Beispiel um Leistungsoptimierung geht. Es besteht keine Notwendigkeit, dies zu tun, bis Sie auf dieses Problem stoßen – vorzeitige Optimierung ist die Wurzel allen Übels!

// 最初的同步实现版本
function renderItems(items) {
 for (item of items) {
 renderItem(item);
 }
}
// 函数将由我们的执行器遍历执行
// 实际上,我们可以用相同的同步方式来执行它!
function* renderItems(items) {
 // 我使用 for..of 遍历方法来避免新函数的产生
 for (item of items) {
 yield renderItem(item);
 }
}

Das bringt nichts ein Unterschied, oder? Der Unterschied besteht darin, dass wir diese Funktion jetzt anders ausführen können, ohne den Quellcode zu ändern. Wie ich bereits erwähnt habe, besteht eigentlich keine Notwendigkeit zu warten, wir können es synchron tun. Also, lasst uns unseren Code optimieren. Wie wäre es mit dem Hinzufügen einer Verzögerung von 4 ms (ein Herzschlag in der JavaScript-VM) nach jedem Yield? Wir haben 1000 Elemente und das Rendern dauert 4 Sekunden – nicht schlecht, wenn ich davon ausgehe, dass ich in 2 Sekunden rendern möchte, kann ich mir das einfacher vorstellen, indem ich zwei auf einmal rendere. Plötzlich wird die Lösung mit Promises komplizierter – wir müssen einen weiteren Parameter übergeben: die Anzahl der Elemente, die jedes Mal gerendert werden sollen. Über unseren Executor müssen wir diesen Parameter noch übergeben, aber der Vorteil ist, dass er absolut keine Auswirkungen auf unsere renderItems-Methode hat.

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);
 }
});

Wie Sie sehen, können wir die Anzahl der Elemente pro Stapel unabhängig vom Ausführenden problemlos ändern und zur normalen synchronen Ausführung zurückkehren – alles ohne Auswirkungen auf unsere renderItems-Methode.

Abbrechen

Betrachten wir die traditionelle Funktion – Abbrechen. Ich habe darüber ausführlich in meinem Artikel „Kündigung von Versprechen im Allgemeinen“ gesprochen (Übersetzung: Wie kündige ich mein Versprechen?). Deshalb verwende ich einen Teil dieses Codes:

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

Das Beste daran ist, dass wir alle Anfragen abbrechen können, die noch keine Chance zur Ausführung hatten (wir können auch einen Objektparameter wie AbortController an unseren übergeben Executor, sodass er sogar die aktuelle Anfrage abbrechen kann!), und wir haben keine einzige Codezeile in unserer Geschäftslogik geändert.

Pause/Fortsetzen

Ein weiterer besonderer Bedarf könnte die Pause/Fortsetzungsfunktion sein. Warum möchten Sie diese Funktion? Stellen Sie sich vor, wir rendern 1000 Datenzeilen und es ist sehr langsam. Wir möchten dem Benutzer die Möglichkeit geben, das Rendern anzuhalten/fortzusetzen, damit er die gesamte Hintergrundarbeit stoppen und lesen kann, was heruntergeladen wurde. Fangen wir an!

// 实现渲染的方法还是一样的
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
 };
}

Der Aufruf dieses Executors kann uns ein Objekt mit Pausen-/Fortsetzungsfunktionen zurückgeben, die alle leicht erhältlich sind, oder unseren vorherigen Geschäftscode verwenden! Wenn Sie also viele „schwere“ Anforderungsketten haben, die lange dauern, und Sie Ihren Benutzern eine Pause-/Fortsetzungsfunktion bieten möchten, können Sie diesen Executor gerne in Ihren Code implementieren.

Fehlerbehandlung

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

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在JS中实现点击下拉菜单内容同步输入框

在vue中使用cli如何实现重构多页面脚手架

在JS中如何实现通过拖拽改变物体大小

在JS中如何改变单物体透明度

Das obige ist der detaillierte Inhalt vonSo verwenden Sie Generatormethoden in JavaScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn