本文分享自華爲雲社區《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的後面,把異步移動到控制生成器的迭代器的代碼部分。
- 生成器爲異步代碼保持了順序、同步、阻塞的代碼模式,這使得大腦可以更自然地追蹤代碼,解決了基於回調的異步的兩個關鍵缺陷之一。