Generator
語法node
yield
yield *
表達式next
方法的參數Generator
爲何是異步編程解決方案異步應用git
Thunk
函數co
模塊JavaScript是單線程的,異步編程對於 JavaScript語言很是重要。若是沒有異步編程,根本無法用,得卡死不可。github
JavaScript開發者在代碼中幾乎廣泛依賴一個假定:一個函數一旦開始執行,就會運行結束,期間不會有其餘代碼打斷它並插入其中。可是ES6引入了一種新的函數類型,它並不符合這種運行到結束的特徵。這類新的函數被稱爲生成器。編程
更正一下上一篇文章對Iterator對象的翻譯,翻譯成中文應該爲迭代器。遍歷是一個動詞, 迭代器是名詞。
執行 Generator 函數返回一個迭代器對象。先來簡單回顧一下什麼是迭代器對象json
function makeIterator(array) { var nextIndex = 0; return { next: function() { return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true }; } }; } const it = makeIterator(['a', 'b']); it.next() // { value: "a", done: false } it.next() // { value: "b", done: false } it.next() // { value: undefined, done: true }
makeIterator
函數就是用於生成迭代器對象的。Generator
函數返回的遍歷其對象,能夠依次遍歷 Generator
函數內部的每個狀態。api
Generator
函數是一個普通函數,可是有兩個特徵。promise
function
關鍵字與函數名以前有個星號yield
表達式function *helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } const hw = helloWorldGenerator();
上面的 定義了一個Generator
函數 helloWorldGenerator
,它的內部有兩個yield
表達式(Hello
和world
),即函數有三個狀態: Hello
, world
和return
語句。數據結構
Generator
函數的調用方式和普通函數同樣,可是調用它並不執行,而是返回一個指向內部狀態的指針對象(Iterator對象
)多線程
hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
上面一共調用了4
次next方法異步
Generator
函數 開始執行,知道遇到第一個 yield
表達式,next()
方法返回一個對象,它的done
屬性就是當前yield
表達式的值 Hello
(這裏注意是yield
表達值的值,並非yield
表達式的返回值,yield表達式自己沒有返回值)。next
方法時,再繼續往下執行,直到遇到下一個 yield
表達式。yield
表達式,就一直運行到函數結束,直到return
語句爲止,並將return
語句後面的表達式的值,做爲返回的對象的value
屬性值。return
語句,則返回的對象的value
屬性值爲undefined
。yield
表達式是暫停標誌。
迭代器對象的next
方法的運行邏輯:
yield
表達式,就暫停執行後面的操做,並將緊跟在yield
後面的那個表達式的值,做爲返回對象的 value
屬性值。next
方法,再繼續往下執行,直到遇到下一個yield
表達式。yield
表達式,就一直運行到函數結束,直到 return
語句爲止,並將 return
語句後面的表達式的值,做爲返回值對象的value
屬性值。return
語句,則返回的對象的value
屬性值爲undefined
。相同點:
都能返回緊跟在語句後面的那個表達式的值。
不一樣點:
yield
,函數暫停執行,下一次再從該位置繼續日後執行,而return
語句不具有位置記憶的能力。return
語句, 可是能夠執行屢次 yield
表達式return
; Generator
函數能夠返回一系列的值,由於有任意多個yield
。(Generator
函數生成了一系列的值,也就是它爲何叫生成器的來歷)。若是在 Generator
函數內部,調用另外一個Generator
函數,須要在前者的函數體內部,本身手動完成遍歷。
function *foo() { yield 'a'; yield 'b'; } function *bar() { yield 'x'; // 手動遍歷 foo() for (let i of foo()) { console.log(i); } yield 'y'; } for (let v of bar()){ console.log(v); } // x // a // b // y
foo
和bar
都是 Generator
函數,在bar
裏面調用foo
,就須要手動遍歷foo
。ES6
提供了yield*
表達式,做爲解決辦法,用來在一個 Generator
函數裏面執行另外一個 Generator
函數。
function *bar() { yield 'x'; yield *foo(); yield 'y'; } // 等同於 function *bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } // 等同於 function *bar() { yield 'x'; for (let v of foo()) { yield v; } yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "a" // "b" // "y"
next
方法能夠帶有一個參數,該參數會被當作上一個yield
表達式的返回值。yield
表達式沒有返回值,或者說總返回 undefined
。
記住,next
方法帶有的參數,會被當作上一個yield
表達式的返回值,yield
表達式沒有返回值。
本身默唸幾遍。而後看看下面代碼運行的輸出是什麼
function *foo(x) { const y = 2 * (yield (x + 1)); const z = yield (y / 3); return (x + y + z); } const a = foo(5); console.log(a.next()); console.log(a.next()); console.log(a.next()); const b = foo(5); console.log(b.next()); console.log(b.next(12)); console.log(b.next(13));
上面的運行結果是什麼
// { value: 6, done: false } // { value: NaN, done: false } // { value: NaN, done: true } // { value: 6, done: false } // { value: 8, done: false } // { value: 42, done: true }
若是你真正理解了next方法帶有的參數,會被當作上一個yield表達式的返回值,yield表達式沒有返回值。這句話,相信這個題你必定能回答出來。
咱們來一塊兒看一下它的完整運行過程。
先看使用Generator函數生成的迭代器a
:
5 + 1 = 6
;undefined
, 致使y
的值等於2*undefined
即(NaN
),除以 3
之後仍是NaN
,所以返回對象的value
屬性也等於NaN
。return (x + y + z)
,此時x
的值爲 5
, y
的值爲 NaN
, 因爲next方法沒有帶參數,上一個yield表達式返回值爲undefined
,致使z爲 undefined,返回對象的 value屬性等於5 + NaN + undefined
,即 NaN在來看看使用Generator函數生成的迭代器b
:
5 + 1 = 6
;12
,因此上一個yield表達式返回值爲12
, 所以y
的值等於2*12
即(24
),除以 3
是8
,所以返回對象的value
屬性爲8
。return (x + y + z)
,此時x
的值爲 5
, y
的值爲 24
, 因爲next方法沒有帶參數13
,所以z爲13
,返回對象的 value屬性等於5 + 24 + 13
,即 42
這個功能有很重要的語法意義。Generator函數從暫停狀態到恢復運行,它的上下文狀態是不變的,經過next方法的參數,就有辦法在 Generator函數開始運行以後,繼續想函數體內注入值。
因爲next
方法的參數表示上一個yield
表達式的返回值,因此在第一次使用next
方法時,傳遞參數是無效的。V8引擎直接忽略第一次使用next
方法時的參數,只有從第二次使用next
方法開始,參數纔是有效的。從語義上講,第一個next
方法用來啓動迭代器對象,因此不用帶有參數。
Generator 函數 經過 yield
和next(...)
實現了內建消息輸入輸出能力。
function *foo(x) { const y = x * (yield); return y; } // 啓動foo(...) const it = foo(6); it.next(); const res = it.next(7); console.log(res.value);
首先,傳入6做爲參數x。而後調用 it.next(),這會啓動 *foo(..)
。
在 *foo(..)
內部,開始執行語句 const y = x ...
,可是就遇到了一個yield表達式。它就會在這一點上暫停 *foo(..)
(在賦值語句中間!),並在本質上要求調用代碼爲 yield 表達式提供一個結果值。
接下來,調用 it.next(7)
`,這一句把值7傳回被暫停的 yield
表達式的結果。
因此,這時賦值語句實際上就是 const y = 6 * 7
。如今,return y 返回值42做爲調用 it.next(7)
的結果。
注意,這裏有一點很是重要,yield
和next(..)
調用有一個不匹配。通常來講,須要的 next(..)
調用要比 yield
語句多一個,上面代碼片斷有一個yield
和兩個next(..)
調用。
爲何會有這個不匹配呢?
由於第一個next()
老是啓動一個生成器,並運行到第一個yield
處。不過,是第二個next(...)
調用完第一個被暫定的yield
表達式,第三個next()
調用完成第二個yield,以此類推。
Generator 函數返回的迭代器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲。
const g = function* () { try { yield; } catch (e) { console.log(e); } }; const i = g(); i.next(); i.throw(new Error('出錯了!')); // Error: 出錯了!(…)
Generator 函數返回的迭代器對象,還有一個return方法,能夠返回給定的值,而且終結遍歷 Generator 函數。
function *gen() { yield 1; yield 2; yield 3; } const g = gen(); g.next(); // { value: 1, done: false } g.return('foo'); // { value: "foo", done: true } g.next(); // { value: undefined, done: true }
迭代器對象g調用return方法後,返回值的value屬性就是return方法的參數foo。而且,Generator 函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性老是返回true。
next()、throw()、return()這三個方法本質上是同一件事,能夠放在一塊兒理解。它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。
next()是將yield表達式替換成一個值。
const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.next(); // Object {value: 3, done: false} gen.next(1); // Object {value: 1, done: true} // 至關於將 let result = yield x + y // 替換成 let result = 1;
throw()是將yield表達式替換成一個throw語句。
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了 // 至關於將 let result = yield x + y // 替換成 let result = throw(new Error('出錯了'));
return()是將yield表達式替換成一個return語句。
gen.return(2); // Object {value: 2, done: true} // 至關於將 let result = yield x + y // 替換成 let result = return 2;
同一個 Generator函數的多個實例能夠同時運行,他們甚至能夠彼此交互
let z = 1; function *foo() { const x = yield 2; z++; const y = yield (x * z); console.log(x, y, z); } const a = foo(); const b = foo(); let val1 = a.next().value; console.log(val1); // 2 <-- yield 2; let val2 = b.next().value; console.log(val2); // 2 <-- yield 2; val1 = a.next(val2 * 10).value; console.log(val1); // 40 <-- x: 20,z:2 val2 = b.next(val1 * 5).value; console.log(val2); // 600 <-- x: 200,z:3 a.next(val2 / 2); // 20, 300, 3 <-- y: 300 b.next(val1 / 4); // 200, 10, 3 <-- y: 10
咱們簡單梳理一下執行流程
*foo()
的兩個實例同時啓用,兩個next()
分別從yield 2
語句獲得2
val2 * 10
也就是2 * 10
,發送到第一個生成器實例 a
, 由於x獲得的值20
。z
從1
增長到2
,而後 20 * 2
經過 yield
發出,將val1
設置爲40
val1 * 5
也就是 40 * 5
,發送到第二個生成器實例 b
,所以x獲得的值200
。z
再從 2
遞增到3
,而後 200*3
經過 yield
發出,將val2
設置爲 600
val2 / 2
也就是 600 / 2
發動到第一個生成器實例 a
, 所以 y獲得值 300
, 而後打印出 x y z
的值分別爲 20, 300, 3
。val1 / 4
也就是 40 / 4
, 發送到第二個生成器實例 b
, 所以 y
獲得的值10
, 而後打印出 x y z
的值分別爲 200, 10, 3
。使用for...of語句時不須要使用next方法。由於它能夠自動遍歷 Generator 函數運行時生成的 Iterator對象。
function* foo() { yield 1; yield 2; yield 3; return 4; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
爲何只顯示3個yield表達式的值呢,
這是由於一旦 next方法的返回 對象的 done屬性爲 true,for...of 循環就中止,且不包含該返回對象,因此上面代碼的return語句返回的4
,不包括在for...of
循環之中。
咱們能夠直觀的來看一下 Generator
函數 foo
的遍歷過程
const it = foo(); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: 4, done: true } console.log(it.next()); // { value: undefined, done: true }
能夠看到第一次 done
返回爲true時,value
爲4
,即執行到最後一個 return
語句。因此 for...of
循環中不包含 4
;
異步:一個任務不是連續完成的,能夠理解爲,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。
好比,你渴了要燒水(假如你的水壺能夠響),第一段任務是你要把水壺放到火上,這個時候你能夠先去幹其餘事情好比去看電視,過了一會,壺響了你聽到了執行第二段任務去倒水喝。這個就叫異步。
同步:連續的執行就叫同步。好比上面的例子,你把水壺放到火上以後,就一直等着水燒開,再去看電視,這就叫同步。
JavaScript語言對於異步編程的實現,就是回調函數。
回調函數自己並無問題,它的問題出如今多個回調函數嵌套。假定讀取A文件以後,再讀取B文件,
fs.readFile(fileA, 'utf-8', function (err, data) { fs.readFile(fileB, 'utf-8', function (err, data) { // ... }); });
上面這種狀況就稱爲"回調函數地獄"(callback hell)。代碼不是縱向發展,而是橫向發展,很快就會亂作一團,沒法管理。由於多個異步操做造成了強耦合,只要有一個操做須要更改,它的上層回調函數和下層回調函數,可能都要跟着修改。
// fs-readfile-promise模塊,它的做用就是返回一個 Promise 版本的readFile函數。 const readFile = require('fs-readfile-promise'); readFile(fileA) .then(function (data) { console.log(data.toString()); }) .then(function () { return readFile(fileB); }) .then(function (data) { console.log(data.toString()); }) .catch(function (err) { console.log(err); });
Promise爲了解決 "回調函數地獄",它不是一種新語法,而是一種新寫法,把嵌套改爲了鏈式調用。並且代碼也很冗餘,一眼看上去一大堆then
。
傳統的編程語言,早有異步編程的解決方案(實際上是多任務的解決方案)。其中有一種叫作"協程"(coroutine),意思是多個線程互相協做,完成異步任務。協程並非一個新的概念,其餘語言中很早就又了。
它的運行流程大體以下:
協程既能夠用單線程實現,也能夠用多線程實現。
多個線程(單線程的狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態,線程(或函數)之間能夠交換執行權,也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。
Generator 函數
是協程在 ES6
的實現,Generator
函數是根據JavaScript
單線程的特色實現的。
使用Generator 函數
,徹底能夠將多個須要相互協做的任務寫成 Generator
函數 ,它們之間使用yield
表達式交換控制權。
JavaScript 代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),包含了當前全部的變量和對象。而後,執行函數(或塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此造成一個上下文環境的堆棧。
這個堆棧是「後進先出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後再執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。
Generator 函數不是這樣,它執行產生的上下文環境,一旦遇到yield命令,就會暫時退出堆棧,可是並不消失,裏面的全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
能夠看到,雖然 Generator 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。
它不能自動執行,若是每次使用它都要本身手動寫一個執行函數的話,也使用起來其實反而更加麻煩了。相信你必定也想到了,咱們能夠實現一個自動執行的功能,自動控制 Generator函數的流程,接收和交換程序的執行權。
JavaScript 語言的 Thunk 函數是將多參數函數,替換成一個只接受回調函數做爲參數的單參數函數。
任何函數,只要參數有回調函數,就能寫成 Thunk 函數的形式
const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; };
使用上面的轉換器,生成fs.readFile
的 Thunk
函數。
const readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);
Thunk 函數用於 Generator 函數的自動流程管理
function run(fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); } next(); } function* g() { // ... } run(g);
Generator 函數只要傳入co函數,就會自動執行。
co模塊的源碼
首先,co 函數接受 Generator 函數做爲參數,返回一個 Promise 對象。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { }); }
在返回的 Promise 對象裏面,co 先檢查參數gen是否爲 Generator 函數。若是是,就執行該函數,獲得一個內部指針對象;若是不是就返回,並將 Promise 對象的狀態改成resolved。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); }); }
接着,co
將 Generator
函數的內部指針對象的next方法,包裝成onFulfilled
函數。這主要是爲了可以捕捉拋出的錯誤。
function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } }); }
最後,就是關鍵的next函數,它會反覆調用自身。
function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected( new TypeError( 'You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"' ) ); }
上面代碼中,next
函數的內部代碼,一共只有四行
命令。
第一行,檢查當前是否爲 Generator
函數的最後一步,若是是就返回。
第二行,確保每一步的返回值,是 Promise
對象。
第三行,使用then
方法,爲返回值加上回調函數,而後經過onFulfilled
函數再次調用next
函數。
第四行,在參數不符合要求的狀況下(參數非 Thunk
函數和 Promise
對象),將 Promise
對象的狀態改成rejected
,從而終止執行。
爲何 Thunk 函數和 co 模塊能夠自定執行 Generator函數?
Generator函數的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。兩種方法能夠作到
then
方法交回執行權。co 模塊其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個模塊。使用 co 的前提條件是,Generator 函數的yield命令後面,只能是 Thunk 函數或 Promise 對象。
暫停執行(yield)
和恢復執行(next)
是Generator
函數能封裝異步任務的根本緣由。函數體內外的數據交換
(next
返回值的value
,是向外輸出
數據,next
方法的參數
,是向內輸入
數據)和錯誤處理機制
(Generator 函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。)是它能夠成爲異步編程的完整解決方案。