序
在Javascript中,大家討論的最多的就是非同步程式設計的操作,如何避免回呼的多次巢狀。非同步操作的回調一旦嵌套很多,不僅程式碼會變的臃腫,還很容易出錯。各種各樣的非同步程式設計解決方案也被不斷提出,例如大家所熟知的Promise,co等等。今天所講的Generator和yield就是和非同步程式設計有關,可以幫助我們把非同步程式設計同步化。
Generator簡介
Generator在形式上和函數差不多,只是在function和函數名之間多了一個*。 Generator內部必須使用yield關鍵字。例如:
function * gen(){ var result1 = yield 'hello'; var result2 = yield 'world'; return result1 + result2; }
當呼叫Generator函數時,並不會執行函數內部的程式碼,而是傳回一個遍歷器,該遍歷器包含一個next方法。每次執行next方法,Generator函數體會開始執行,直到遇到yield語句,執行該語句並在此暫停。用法如下:
var g = gen(); g.next(1); //{value : 'hello', done : false} g.next(2); //{value : 'world', done : false} g.next(); //{value : 3, done: true} g.next(); //{value : undefined, done: true}
呼叫next方法會傳回一個對象,這個物件包含兩個屬性,value和done,value即是目前yield語句的值。 done表示Generator函數體是否被執行完。 next方法同時接受一個參數,這個參數會作為yield語句的回傳值,可以被後面的程式所使用。當程式執行完或遇到return語句,value即為函數體的回傳值,done就變成了true。至此,如果再執行next方法,value就為undefined, done還是true。
Generator在遍歷中的應用
在js中,我們要遍歷一個數組,我們可以用for...of這樣的語句來進行遍歷,這其實也是因為數組中包含了一個Generator遍歷器。如果我們的自己定義的物件也包含一個遍歷器,我們也就可以透過for...of等遍歷語句來遍歷自訂的物件。這個遍歷器被存在著Symbol.iterator屬性中。
var myArray = { 0: '你', 1: '的', 2: '名字', length: 3 }; myArray[Symbol.iterator] = function * (){ for(var i = 0; i < this.length; i++) { yield this[i]; } }; for(var item of myArray) { console.log(item); } //你 //的 //名字
Generator在非同步編程中的應用
Javascript的核心就是非同步編程,每個非同步操作都會提供一個callback回呼函數來傳回執行的結果。假設我們有幾個操作,後一個操作依賴前一個操作的結果,如果採用回調的方式:
step1(function(value1) { step2(value1, function(value2) { step3(value2, function(value3)) { //some code } }); })
這樣的程式碼一單回呼的嵌套變多,會讓程式變的非常難理解,同時也很容易出錯。我們要做的就是將回調變的扁平化。 Promise物件就是這樣的功能,將上述的操作Promise化:
step1().then(function(value1){ return step2(value1); }).then(function(value2){ return step3(value2); }).then(function(){ //some code })
我們可以看到嵌套變少了,但是這並不是最理想的解決方案,如果我們能將異步操作變成同步操作那樣,即沒了嵌套,程式也會變的好理解。 Generator函數就給我們提供的這樣的機會。
function *workflow(){ var value1 = yield step1(); var value2 = yield step2(); var value3 = yield step3(); //some code }
這樣就是我們希望結果,非同步程式設計了同步程式設計的形式。我們接下來要做的是讓這個Generator執行起來,所以我們需要一個執行器。 co就是一個執行器,讓Generator自動執行。
co(function *workflow(){ var value1 = yield step1(); var value2 = yield step2(); var value3 = yield step3(); //some code });
co有個限制,yield語句後面跟的只能是Promise物件或Thunk函數,關於co更詳細的介紹,可以參考阮老師的文章co 函式庫的意義和用法。然而這樣的方法依然需要依賴外在的函式庫函數,於是ES6中提出了async和await關鍵字。 async和await其實就是Generator的語法糖。只是它自帶執行器。將上面的程式碼改寫成async形式:
async function workflow(){ var value1 = await step1(); var value2 = await step2(); var value3 = await step3(); //some code } var result = workflow();
async没有了co的限制。await关键字后面可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。