學習JavaScript中的非同步Generator

來源:互聯網
上載者:User

標籤:

本文和大家分享的主要是javascript中非同步Generator相關內容,一起來看看吧,希望對大家學習javascript 有所協助。

非同步generators和非同步iteration已經到來 ! 這是錯誤的, 它們現在還在 階段 3 ,這表示他們很有可能在JavaScript未來的一個版本中發布。 在他們發布之前,你可以通過 Babel 來在你的項目中使用還在階段3的建議內容。

網站基本上還是一些分散啟動並執行應用,因為任何在語言上的修改都會造成永久的影響,所以所有的未來的版本都需要向後相容。因此,被加入到ECMAScript標準的特性,它必須是十分的可靠,而且它的文法需要很優雅。

考慮到這一點,我們希望非同步generator和迭代器可以顯著地影響我們如何構建今後的代碼,同時也解決現在的問題。讓我們開始瞭解非同步generator是如何工作的,它在我們的正式開發中又會遇到什麼樣的問題。

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

簡而言之,非同步generators和普通的generator函數很像,但是它可以yield Promises。

總的來說,普通的generator函數基本上就是一個 迭代器 和 觀察者 模式的集合。generator是一個可以中止的函數,你可以通過調用 .next() 來一步步執行。可以同通過 .next() 來多次從generator輸出內容,也可以通過 .next(valueToPush) 來多次傳入參數。這種雙向的介面可以使你通過一種文法同時完成迭代器和觀察者的功能!

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

WebSocket訊息機制就是一個很好的非同步擷取資料的例子。如果我們已經接收到了所有的資料,那麼我們當然可以同步地遍曆它們。但是,我們也可能會遇到我們並不知道什麼時候會接收到資料,所以我們需要一個機制去等待資料接收完成後去遍曆。非同步generators和非同步迭代器可以讓我們做到這個。

簡單的來說就是:generator函數適用於資料可以被使用者控制的情況,非同步generators適用於允許資料來源本身控制的情況。

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

讓我們用一個例子來練習我們的非同步方案。我們需要編寫一個非同步generator函數,它可以重複的等待一個隨機的毫秒數後產生一個新的數字。在幾秒鐘中時間裡,它可能會從0開始產生5個左右的數字。首先我們先通過建立一個Promise來建立一個定時器:

// 建立一個Promise,並在ms後resolvesvar timer = function(ms) {

return new Promise(resolve => {

setTimeout(resolve, ms);

});

};

運行 timer(5000) 會返回一個Promise,並且會在5秒後resolve。現在我們可以寫一個非同步generator:

// Repeatedly generate a number starting// from 0 after a random amount of timevar source = async function\*() {

var i = 0;

while (true) {

await timer(Math.random() \* 1000);

yield i++;

}

};

如此複雜的功能卻可以寫的如此優雅!我們的非同步generator函數等待一個隨機的時間後 yield並減小i的值。如果我們沒有非同步generator,我們可以像下面一樣使用普通的generator函數,通過yield Promises來實現:

var source = function\*() {

var i = 0;

while (true) {

yield timer(Math.random() \* 1000)

.then(() => i++);

}

};

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

// 把所有都集合到一起var run = async function() {

var stream = source();

for await (let n of stream) {

console.log(n);

}

};

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

這是多麼神奇,只有20行不到的代碼。首先,我們先運行了非同步generator函數 source ,它返回了一個特殊的 AsyncGenerator 對象。然後,我們使用一個文法上叫“非同步迭代”的 for await...of 迴圈遍曆 source 產生的對象。

但是我們還可以再改進一下:假設我們是想要輸出 source 產生的數字。我們可以在 for await...of 迴圈裡面直接輸出它們,但是我們最好在迴圈的外面“轉換”stream 的值,像是使用 .map() 一樣來轉換數組裡的值。它是如此的簡單:

// Return a new async iterator that applies a// transform to the values from another async generatorvar map = async function\*(stream, transform) {

for await (let n of stream) {

yield transform(n);

}

};

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

// 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// ...

多麼感人啊!但是只是用於計算數字有一點大材小用了。

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

我們一般是通過綁定事件來監聽WebSocket的資料:

var ws = new WebSocket(’ws://localhost:3000/’);

ws.addEventListener(’message’, event => {

console.log(event.data);

});

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

// Tie everything togethervar 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 中寫到的內容,一個對象要使用 for...of 迴圈,必須要有 Symbol.iterator 屬性。同樣的,一個對象要想使用 for await...of 迴圈,它必須要有 Symbol.asyncIterator 屬性。下面就是具體的實現:

// 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 返回的資料的 data屬性進行 yield 。 oncePromise() 函數有一點黑科技:它返回了一個Promise,當事件觸發時它會被resolves,然後立即移除事件監聽。

// Generate a Promise that listens only once for an eventvar oncePromise = (emitter, event) => {

return new Promise(resolve => {

var handler = (...args) => {

emitter.removeEventListener(event, handler);

resolve(...args);

};

emitter.addEventListener(event, handler);

});

};

這樣看上去有一點低效,但是證明了websocket的資訊接收確實可以用我們的非同步迭代器實現。如果你在 http://localhost:3000 有一個啟動並執行WebSocket服務,那麼你可以通過調用 run()來監聽資訊流:

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

進階例子: 重寫 RxJS

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

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

1.監聽所有的點擊事件

2.過濾點擊事件只擷取點擊anchor標籤的事件

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

4.將點擊事件映射到點擊計數器和點擊事件

5.每500ms只可以觸發一次點擊

6.列印點擊的次數和事件

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

// Tie everything togethervar 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();

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

// 把所有的event emitter放入一個streamvar streamify = async function\*(event, element) {

while (true) {

yield await oncePromise(element, event);

}

};

streamify() 像是一個WebSocket非同步迭代器: oncePromise() 使用 .addEventListener()去監聽事件一次, 然後resolves Promise. 通過 while (true) 迴圈 , 我們可以一直監聽事件。

// Only pass along events that meet a conditionvar 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 streamvar 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() 函數和 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;

}

}

};

我們做了這麼多,最後,我們還需要列印出每次的點擊事件和當前的次數。 subscribe() 做了一些零碎的事情:它在每一次事件迴圈的時候運行,並執行callback,所以沒有必要使用 yield

// 每次事件到達都調用一次回呼函數var subscribe = async (stream, callback) => {

for await (var event of stream) {

callback(event);

}

};

到這裡,我們已經寫了一個我們自己的反應型函數式管道!

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

挑戰

非同步generators是如此的優雅。而generator函數允許我們從迭代器中回去資料,非同步generators可以讓我們迭代“推送”過來的資料。這是多麼好的非同步資料結構的抽象。當然,也有一些注意事項。

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

var streamify = async function\*(event, element) {

element.addEventListener(event, e => {

// 這裡將無法運行,因為yield

// 不可以在一個普通函數中被使用

yield e;

});

};

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

為了繞過這個問題,我們寫了一個 oncePromise() 函數來幫組我們。撇開一些潛在的效能問題,需要注意的是Promise的回調總是在當前的呼叫堆疊結束之後執行。在瀏覽器端,類似 microtasks 一樣運行Promise的回調是不會出現問題的,但是一些Promise的polyfill在下一次事件迴圈運行之前是不會運行callback。因此,調用 .preventDefault() 函數有時候會沒有有效果,因為可能DOM時間已經冒泡到瀏覽器了。

JavaScript現在已經有了多個非同步流資料類型: Stream , AsyncGenerator 和最後的 Observable 。雖然三個都是屬於“推送”資料來源,但是在處理回調和控制底層資源上還是有一些微妙的語義上的不同。

 

來源:眾成翻譯

學習JavaScript中的非同步Generator

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.