學習JavaScript中的異步Generator

原文:https://www.bignerdranch.com/blog/asyncing-feeling-about-javascript-generators/

你想看精簡版本么 這里是所有三個例子的要點

異步的generators和異步iteration已經(jīng)到來! 這是錯誤的, 它們現(xiàn)在還在階段 3,這表示他們很有可能在JavaScript未來的一個版本中發(fā)布。 在他們發(fā)布之前,你可以通過Babel來在你的項目中使用還在階段3的建議內(nèi)容。

網(wǎng)站基本上還是一些分散運行的應用,因為任何在語言上的修改都會造成永久的影響,所以所有的未來的版本都需要向后兼容。因此,被加入到ECMAScript標準的特性,它必須是十分的可靠,而且它的語法需要很優(yōu)雅。

考慮到這一點,我們希望異步generator和迭代器可以顯著地影響我們?nèi)绾螛嫿ń窈蟮拇a,同時也解決現(xiàn)在的問題。讓我們開始了解異步generator是如何工作的,它在我們的正式開發(fā)中又會遇到什么樣的問題。

總結: 異步的Generators是如何工作的呢

簡而言之,異步的generators和普通的generator函數(shù)很像,但是它可以yield Promises。如果你想很了解ES2015的generator函數(shù),那么可以先去看一下Chris Aquino的博客,再去看一下Jafar Husain的一篇異步編程的很棒的演講

總的來說,普通的generator函數(shù)基本上就是一個迭代器觀察者 模式的集合。generator是一個可以中止的函數(shù),你可以通過調(diào)用.next()來一步步執(zhí)行??梢酝ㄟ^.next()來多次從generator輸出內(nèi)容,也可以通過.next(valueToPush)來多次傳入?yún)?shù)。這種雙向的接口可以使你通過一種語法同時完成迭代器和觀察者的功能!

當然generators也有它的缺點:它在調(diào)用.next()的時候必須立即(同步)返回數(shù)據(jù)。換句話來說,就是代碼在調(diào)用.next()的時候就需要得到數(shù)據(jù)。在generator需要時能夠生成新數(shù)據(jù)的情況下是可以的,但是沒有辦法處理迭代一個異步的(或者臨時的)數(shù)據(jù)來源,它們需要自己控制在下一次數(shù)據(jù)準備好的時候執(zhí)行下一次。

WebSocket消息機制就是一個很好的異步獲取數(shù)據(jù)的例子。如果我們已經(jīng)接收到了所有的數(shù)據(jù),那么我們當然可以同步地遍歷它們。但是,我們也可能會遇到我們并不知道什么時候會接收到數(shù)據(jù),所以我們需要一個機制去等待數(shù)據(jù)接收完成后去遍歷。異步generators和異步迭代器可以讓我們做到這個。

簡單的來說就是:generator函數(shù)適用于數(shù)據(jù)可以被使用者控制的情況,異步generators適用于允許數(shù)據(jù)源本身控制的情況。

一個簡單的例子: 生成和使用AsyncGenerator

讓我們用一個例子來練習我們的異步方案。我們需要編寫一個異步的generator函數(shù),它可以重復的等待一個隨機的毫秒數(shù)后生成一個新的數(shù)字。在幾秒鐘中時間里,它可能會從0開始生成5個左右的數(shù)字。首先我們先通過創(chuàng)建一個Promise來創(chuàng)建一個定時器:

// 創(chuàng)建一個Promise,并在ms后resolves
var timer = function(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
};

運行timer(5000)會返回一個Promise,并且會在5秒后resolve?,F(xiàn)在我們可以寫一個異步generator:

// Repeatedly generate a number starting
// from 0 after a random amount of time
var source = async function\*() {
  var i = 0;
  while (true) {
    await timer(Math.random() \* 1000);
    yield i++;
  }
};

如此復雜的功能卻可以寫的如此優(yōu)雅!我們的異步generator函數(shù)等待一個隨機的時間后yield并減小i的值。如果我們沒有異步generator,我們可以像下面一樣使用普通的generator函數(shù),通過yieldPromises來實現(xiàn):

var source = function\*() {
  var i = 0;
  while (true) {
    yield timer(Math.random() \* 1000)
      .then(() => i++);
  }
};

當然,這里還有一些特殊情況和引用需要我們處理,所以最好有一個專門的函數(shù)類型!現(xiàn)在是時候編寫使用代碼了;因為我們需要await操作符,所以我們將會創(chuàng)建一個異步的run()函數(shù)。

// 把所有都集合到一起
var run = async function() {
  var stream = source();
  for await (let n of stream) {
    console.log(n);
  }
};

run();
// => 0
// => 1
// => 2
// => 3
// ...

這是多么神奇,只有20行不到的代碼。首先,我們先運行了異步generator函數(shù)source,它返回了一個特殊的AsyncGenerator對象。然后,我們使用一個語法上叫“異步迭代”的for await...of循環(huán)遍歷source生成的對象。

但是我們還可以再改進一下: 假設我們是想要輸出source生成的數(shù)字。我們可以在for await...of循環(huán)里面直接輸出它們,但是我們最好在循環(huán)的外面“轉(zhuǎn)換”stream 的值,像是使用.map()一樣來轉(zhuǎn)換數(shù)組里的值。它是如此的簡單:

// Return a new async iterator that applies a
// transform to the values from another async generator
var map = async function\*(stream, transform) {
  for await (let n of stream) {
    yield transform(n);
  }
};

接下來我們只需要再往run()函數(shù)中加一行代碼就好了:

 // Tie everything together
 var run = async function() {
   var stream = source();
+  // Square values generated by source() as they arrive
+  stream = map(stream, n => n \* n);
   for await (let n of stream) {
     console.log(n);
   }
 };

當我們運行 run()就會輸出:

// => 0
// => 1
// => 4
// => 9
// ...

多么感人??!但是只是用于計算數(shù)字有一點大材小用了。

中級例子: 在WebSockets中使用AsyncIterator(異步迭代器)

我們一般是通過綁定事件來監(jiān)聽WebSocket的數(shù)據(jù):

var ws = new WebSocket('ws://localhost:3000/');
ws.addEventListener('message', event => {
  console.log(event.data);
});

但是如果可以把WebSocket的信息當做stream,這樣就可以用我們上面的辦法“iterate”這些信息。不幸的是,WebSockets還沒有異步迭代器的功能,但是我們只需要寫短短的幾行就可以自己來實現(xiàn)這個功能。我們的run()函數(shù)大概的樣子如下:

// Tie everything together
var run = async () => {
  var ws = new WebSocket('ws://localhost:3000/');
  for await (let message of ws) {
    console.log(message);
  }
};

Now for that polyfill.你可能會回憶起Chris Aquino’s blog series中寫到的內(nèi)容,一個對象要使用for...of循環(huán),必須要有Symbol.iterator屬性。同樣的,一個對象要想使用for await...of循環(huán),它必須要有Symbol.asyncIterator屬性。下面就是具體的實現(xiàn):

// Add an async iterator to all WebSockets
WebSocket.prototype[Symbol.asyncIterator] = async function\*() {
  while(this.readyState !== 3) {
    yield (await oncePromise(this, 'message')).data;
  }
};

這個異步迭代器會等待接受信息,然后會對WebSocket的MessageEvent返回的數(shù)據(jù)的data屬性進行yieldoncePromise()函數(shù)有一點黑科技:它返回了一個Promise,當事件觸發(fā)時它會被resolves,然后立即移除事件監(jiān)聽。

// Generate a Promise that listens only once for an event
var oncePromise = (emitter, event) => {
  return new Promise(resolve => {
    var handler = (...args) => {
      emitter.removeEventListener(event, handler);
      resolve(...args);
    };
    emitter.addEventListener(event, handler);
  });
};

這樣看上去有一點低效,但是證明了websocket的信息接收確實可以用我們的異步迭代器實現(xiàn)。如果你在http://localhost:3000 有一個運行的WebSocket服務,那么你可以通過調(diào)用run()來監(jiān)聽信息流:

run();
// => "hello"
// => "sandwich"
// => "otters"
// ...

高級例子: 重寫 RxJS

現(xiàn)在是時候面對最后的挑戰(zhàn)了。反應型函數(shù)編程 (FRP)在UI編程和JavaScript中被大量使用, RxJS是這種編程方式中最流行的框架。RxJS中模型事件來源例如Observable--它們很想一個一個事件流或者lazy array,它們可以被類似數(shù)組語法中的map()filter()處理。

自從FRP補充了JavaScript中的非阻塞式理念,類RxJS的API很有可能會加入到JavaScript未來的一個版本中。同時,我們可以使用異步generators編寫我們自己的類似RxJS的功能,而這僅僅只需要80行代碼。下面就是我們要實現(xiàn)的目標:

  1. 監(jiān)聽所有的點擊事件

  2. 過濾點擊事件只獲取點擊anchor標簽的事件

  3. 只允許不同的點擊Only allow distinct clicks

  4. 將點擊事件映射到點擊計數(shù)器和點擊事件

  5. 每500ms只可以觸發(fā)一次點擊

  6. 打印點擊的次數(shù)和事件

這些問題都是RxJS解決了的問題,所以我們將要嘗試重新實現(xiàn)。下面是我們的實現(xiàn):

// Tie everything together
var run = async () => {
  var i = 0;
  var clicks = streamify('click', document.querySelector('body'));

  clicks = filter(clicks, e => e.target.matches('a'));
  clicks = distinct(clicks, e => e.target);
  clicks = map(clicks, e => [i++, e]);
  clicks = throttle(clicks, 500);

  subscribe(clicks, ([ id, click ]) => {
    console.log(id);
    console.log(click);
    click.preventDefault();
  });
};

run();

為了使上面的函數(shù)正常運行,我們還需要6個函數(shù):streamify(), filter(), distinct(), map(), throttle()subscribe()。

// 把所有的event emitter放入一個stream
var streamify = async function\*(event, element) {
  while (true) {
    yield await oncePromise(element, event);
  }
};

streamify() 像是一個WebSocket異步迭代器: oncePromise() 使用 .addEventListener() 去監(jiān)聽事件一次, 然后resolves Promise. 通過while (true)循環(huán) , 我們可以一直監(jiān)聽事件。

// Only pass along events that meet a condition
var filter = async function\*(stream, test) {
  for await (var event of stream) {
    if (test(event)) {
      yield event;
    }
  }
};

filter() 會只允許通過test的事件被 yield. map()幾乎是相同的:

// Transform every event of the stream
var map = async function\*(stream, transform) {
  for await (var event of stream) {
    yield transform(event);
  }
};

map()可以簡單地在yield之前變換事件。distinct()展示了異步generator的其中一個強大的功能:它可以保存局部變量!

var identity = e => e;

// 只允許與最后一個不相同的事件通過
var distinct = async function\*(stream, extract = identity) {
  var lastVal;
  var thisVal;
  for await (var event of stream) {
    thisVal = extract(event);
    if (thisVal !== lastVal) {
      lastVal = thisVal;
      yield event;
    }
  }
};

最后,強大的throttle()函數(shù)和distinct()很像:它記錄最后一個事件的時間,且只允許超過最后一次yield事件一個確定的時間的事件通過。

// 只允許超過最后一次事件確定時間的事件通過。
var throttle = async function\*(stream, delay) {
  var lastTime;
  var thisTime;
  for await (var event of stream) {
    thisTime = (new Date()).getTime();
    if (!lastTime || thisTime - lastTime > delay) {
      lastTime = thisTime;
      yield event;
    }
  }
};

我們做了這么多,最后,我們還需要打印出每次的點擊事件和當前的次數(shù)。subscribe()做了一些零碎的事情:它在每一次事件循環(huán)的時候運行,并執(zhí)行callback,所以沒有必要使用yield。

// 每次事件到達都調(diào)用一次回調(diào)函數(shù)
var subscribe = async (stream, callback) => {
  for await (var event of stream) {
    callback(event);
  }
};

到這里,我們已經(jīng)寫了一個我們自己的反應型函數(shù)式管道!

你可以在這里獲取到所有的例子的代碼和要點。

挑戰(zhàn)

異步generators是如此的優(yōu)雅。而generator函數(shù)允許我們從迭代器中回去數(shù)據(jù),異步generators可以讓我們迭代“推送”過來的數(shù)據(jù)。這是多么好的異步數(shù)據(jù)結構的抽象。當然,也有一些注意事項。

首先,對一個objects增加支持for await...of的功能有一些粗糙,除非你可以避免使用yieldawait。尤其是,使用.addEventListener()轉(zhuǎn)換任何東西都很棘手,因為你不可以在一個回調(diào)中使用yield操作:

var streamify = async function\*(event, element) {
  element.addEventListener(event, e => {
    // 這里將無法運行,因為yield
    // 不可以在一個普通函數(shù)中被使用
    yield e;
  });
};

同樣的,你也不可以在.forEach()和其他函數(shù)型的方法中使用yield。這是一個固有的限制因為我們不能保證在generator已經(jīng)完成后不使用yield。

為了繞過這個問題,我們寫了一個oncePromise()函數(shù)來幫組我們。撇開一些潛在的性能問題,需要注意的是Promise的回調(diào)總是在當前的調(diào)用堆棧結束之后執(zhí)行。在瀏覽器端,類似microtasks一樣運行Promise的回調(diào)是不會出現(xiàn)問題的,但是一些Promise的polyfill在下一次事件循環(huán)運行之前是不會運行callback。因此,調(diào)用.preventDefault()函數(shù)有時候會沒有有效果,因為可能DOM時間已經(jīng)冒泡到瀏覽器了。

JavaScript現(xiàn)在已經(jīng)有了多個異步流數(shù)據(jù)類型:Stream, AsyncGenerator和最后的Observable。雖然三個都是屬于“推送”數(shù)據(jù)源,但是在處理回調(diào)和控制底層資源上還是有一些微妙的語義上的不同。如果你想了解更多關于反應函數(shù)式語法的細節(jié),可以瀏覽General Theory of Reactivity.

更多

在程序語言的競賽中,JavaScript不是一個懶鬼。ES2015中的變量的解構賦值,ES2016中的異步函數(shù),而現(xiàn)在的異步迭代器可以使JavaScript使用優(yōu)雅的解決復雜UI和I/O編程的問題而不是使用充滿不可控的多線程方案。

除此之外,還有很多的新內(nèi)容和新特性!所以請關注博客和TC39 proposals repo來獲取最新的好東西。同時,你也可以通過在Babel中開啟Stage 3 提案的方式在你的代碼中使用異步generator函數(shù)。

你是否有興趣學習網(wǎng)頁平臺的下一代的JavaScript? 歡迎來我們的前端訓練營, 或者 我們可以提供企業(yè)培訓g!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容