本文分享自華為雲社群《3月閱讀周·你不知道的JavaScript | ES6生成器,看似同步的非同步流程控制表達風格》,作者: 葉一一。
生成器
打破完整執行
JavaScript開發者在程式碼中幾乎普遍依賴的一個假定:一個函式一旦開始執行,就會執行到結束,期間不會有其他程式碼能夠打斷它並插入其間。
ES6引入了一個新的函式型別,它並不符合這種執行到結束的特性。這類新的函式被稱為生成器。
var x = 1; function foo() { x++; bar(); // <-- 這一行在x++和console.log(x)語句之間執行 console.log('x:', x); } function bar() { x++; } foo(); // x: 3
如果bar()並不在那裡會怎樣呢?顯然結果就會是2,而不是3。最終的結果是3,所以bar()會在x++和console.log(x)之間執行。
但JavaScript並不是搶佔式的,(目前)也不是多執行緒的。然而,如果foo()自身可以透過某種形式在程式碼的這個位置指示暫停的話,那就仍然可以以一種合作式的方式實現這樣的中斷(併發)。
下面是實現合作式併發的ES6程式碼:
var x = 1; function* foo() { x++; yield; // 暫停! console.log('x:', x); } function bar() { x++; } // 構造一個迭代器it來控制這個生成器 var it = foo(); // 這裡啟動foo()! it.next(); console.log('x:', x); // 2 bar(); console.log('x:', x); // 3 it.next(); // x: 3
- it = foo()運算並沒有執行生成器*foo(),而只是構造了一個迭代器(iterator),這個迭代器會控制它的執行。
- *foo()在yield語句處暫停,在這一點上第一個it.next()呼叫結束。此時*foo()仍在執行並且是活躍的,但處於暫停狀態。
- 最後的it.next()呼叫從暫停處恢復了生成器*foo()的執行,並執行console.log(..)語句,這條語句使用當前x的值3。
生成器就是一類特殊的函式,可以一次或多次啟動和停止,並不一定非得要完成。
輸入和輸出
生成器函式是一個特殊的函式,它仍然有一些函式的基本特性。比如,它仍然可以接受引數(即輸入),也能夠返回值(即輸出)。
function* foo(x, y) { return x * y; } var it = foo(6, 7); var res = it.next(); res.value; // 42
向*foo(..)傳入實參6和7分別作為引數x和y。*foo(..)向呼叫程式碼返回42。
多個迭代器
每次構建一個迭代器,實際上就隱式構建了生成器的一個例項,透過這個迭代器來控制的是這個生成器例項。
同一個生成器的多個例項可以同時執行,它們甚至可以彼此互動:
function* foo() { var x = yield 2; z++; var y = yield x * z; console.log(x, y, z); } var z = 1; var it1 = foo(); var it2 = foo(); var val1 = it1.next().value; // 2 <-- yield 2 var val2 = it2.next().value; // 2 <-- yield 2 val1 = it1.next(val2 * 10).value; // 40 <-- x:20, z:2 val2 = it2.next(val1 * 5).value; // 600 <-- x:200, z:3 it1.next(val2 / 2); // y:300 // 20300 3 it2.next(val1 / 4); // y:10 // 200 10 3
簡單梳理一下執行流程:
(1) *foo()的兩個例項同時啟動,兩個next()分別從yield 2語句得到值2。
(2) val2 * 10也就是2 * 10,傳送到第一個生成器例項it1,因此x得到值20。z從1增加到2,然後20 * 2透過yield發出,將val1設定為40。
(3) val1 * 5也就是40 * 5,傳送到第二個生成器例項it2,因此x得到值200。z再次從2遞增到3,然後200 * 3透過yield發出,將val2設定為600。
(4) val2 / 2也就是600 / 2,傳送到第一個生成器例項it1,因此y得到值300,然後列印出x y z的值分別是20300 3。
(5) val1 / 4也就是40 / 4,傳送到第二個生成器例項it2,因此y得到值10,然後列印出x y z的值分別為200 10 3。
生成器產生值
生產者與迭代器
假定你要產生一系列值,其中每個值都與前面一個有特定的關係。要實現這一點,需要一個有狀態的生產者能夠記住其生成的最後一個值。
迭代器是一個定義良好的介面,用於從一個生產者一步步得到一系列值。JavaScript迭代器的介面,就是每次想要從生產者得到下一個值的時候呼叫next()。
可以為數字序列生成器實現標準的迭代器介面:
var something = (function () { var nextVal; return { // for..of迴圈需要 [Symbol.iterator]: function () { return this; }, // 標準迭代器介面方法 next: function () { if (nextVal === undefined) { nextVal = 1; } else { nextVal = 3 * nextVal + 6; } return { done: false, value: nextVal }; }, }; })(); something.next().value; // 1 something.next().value; // 9 something.next().value; // 33 something.next().value; // 105
next()呼叫返回一個物件。這個物件有兩個屬性:done是一個boolean值,標識迭代器的完成狀態;value中放置迭代值。
iterable
iterable(可迭代),即指一個包含可以在其值上迭代的迭代器的物件。
從ES6開始,從一個iterable中提取迭代器的方法是:iterable必須支援一個函式,其名稱是專門的ES6符號值Symbol.iterator。呼叫這個函式時,它會返回一個迭代器。通常每次呼叫會返回一個全新的迭代器,雖然這一點並不是必須的。
var a = [1, 3, 5, 7, 9]; for (var v of a) { console.log(v); } // 1 3 5 7 9
上面的程式碼片段中的a就是一個iterable。for..of迴圈自動呼叫它的Symbol.iterator函式來構建一個迭代器。
for (var v of something) { .. }
for..of迴圈期望something是iterable,於是它尋找並呼叫它的Symbol.iterator函式。
生成器迭代器
可以把生成器看作一個值的生產者,我們透過迭代器介面的next()呼叫一次提取出一個值。
生成器本身並不是iterable,當你執行一個生成器,就得到了一個迭代器:
function *foo(){ .. } var it = foo();
可以透過生成器實現前面的這個something無限數字序列生產者,類似這樣:
function* something() { var nextVal; while (true) { if (nextVal === undefined) { nextVal = 1; } else { nextVal = 3 * nextVal + 6; } yield nextVal; } }
因為生成器會在每個yield處暫停,函式*something()的狀態(作用域)會被保持,即意味著不需要閉包在呼叫之間保持變數狀態。
非同步迭代生成器
function foo(x, y) { ajax('http://some.url.1/? x=' + x + '&y=' + y, function (err, data) { if (err) { // 向*main()丟擲一個錯誤 it.throw(err); } else { // 用收到的data恢復*main() it.next(data); } }); } function* main() { try { var text = yield foo(11, 31); console.log(text); } catch (err) { console.error(err); } } var it = main(); // 這裡啟動! it.next();
在yield foo(11,31)中,首先呼叫foo(11,31),它沒有返回值(即返回undefined),所以發出了一個呼叫來請求資料,但實際上之後做的是yield undefined。
這裡並不是在訊息傳遞的意義上使用yield,而只是將其用於流程控制實現暫停/阻塞。實際上,它還是會有訊息傳遞,但只是生成器恢復執行之後的單向訊息傳遞。
看一下foo(..)。如果這個Ajax請求成功,我們呼叫:
it.next(data);
這會用響應資料恢復生成器,意味著暫停的yield表示式直接接收到了這個值。然後隨著生成器程式碼繼續執行,這個值被賦給區域性變數text。
總結
我們來總結一下本篇的主要內容:
- 生成器是ES6的一個新的函式型別,它並不像普通函式那樣總是執行到結束。取而代之的是,生成器可以在執行當中(完全保持其狀態)暫停,並且將來再從暫停的地方恢復執行。
- yield/next(..)這一對不只是一種控制機制,實際上也是一種雙向訊息傳遞機制。yield ..表示式本質上是暫停下來等待某個值,接下來的next(..)呼叫會向被暫停的yield表示式傳回一個值(或者是隱式的undefined)。
- 在非同步控制流程方面,生成器的關鍵優點是:生成器內部的程式碼是以自然的同步/順序方式表達任務的一系列步驟。其技巧在於,把可能的非同步隱藏在了關鍵字yield的後面,把非同步移動到控制生成器的迭代器的程式碼部分。
- 生成器為非同步程式碼保持了順序、同步、阻塞的程式碼模式,這使得大腦可以更自然地追蹤程式碼,解決了基於回撥的非同步的兩個關鍵缺陷之一。
點選關注,第一時間瞭解華為雲新鮮技術~