Generator用法詳解+co

WZZ發表於2018-03-24

Generator函式是ES6提供的一種非同步程式設計解決方案

先來看個Generator的簡單的用法

function* read() {
    console.log(100);
    let a = yield '200';
    console.log(a);
    let b = yield 300;
    console.log(b);
    return b;
}
let it = read();
console.log(it.next('400'));  
console.log(it.next('500')); 
console.log(it.next('600')); 
console.log(it.next('700'));  
複製程式碼

列印結果為

100
{ value: '200', done: false }
500
{ value: 300, done: false }
600
{ value: '600', done: true }
{ value: undefined, done: true }
複製程式碼

首先,Generatror函式有兩個特徵:

  • function關鍵字與函式名之間有一個星號
  • 函式體內部使用yield語句,定義不同的內部狀態

yield語句在英語裡的意思就是“產出”,yield會將函式分割成好多個部分,每產出一次,就暫停一次

  • Genenrator是一個生成器,呼叫Genenrator函式,不會立即執行函式體,只是建立了一個迭代器物件,如上例中的it就是呼叫read這個Generator函式得到的一個迭代器
  • 迭代器有一個next方法,呼叫一次就會繼續向下執行,直到遇到下一個yield或return
  • next()方法可以帶一個引數,該引數會被當做上一條yield語句的返回值,並賦值給yield前面等號前的變數
  • 每遇到一個yield,就會返回一個{value:xxx,done:bool}的物件,然後暫停,返回的value就是跟在yield後面的返回值,done表示這個generator是否已經執行結束了;
  • 當遇到return 時,return後的值就是value指,done此時就是true;
  • 函式末尾如果沒有return,就是隱含的return undefined;

上例中每一次迭代器呼叫next的所執行的語句可以理解為下圖

Generator用法詳解+co

  • 第一次執行 it.next('400') ,先執行了紅色區域內程式碼,這裡沒有接收引數400的語句,是無效的,程式先列印出100,再執行yield,next返回的{value:xxx,done:bool}物件中的value值,即yield後面跟著的值,此時迭代沒有執行完,done為false,所以接著列印出{ value: '200', done: false }
  • 第二次執行it.next('500') ,執行紅色和藍色區間內的程式碼,yield 前面的等號前的變數,就是接收這次next傳進來的引數的,也就是說a等於500,yield的輸出同裡,會輸出{ value: 300, done: false }
  • 第三次執行it.next('600'),到了藍色和黑色區間內的程式碼,b接收next引數600,最後return b,會將b的值作為value,同時迭代器執行完畢,done為true,所以返回結果為 { value: '600', done: true }
  • 第四次執行it.next('700'),已經沒有接收next引數的地方,也沒有return,即value為undefined,done仍然是完成,返回{ value: undefined, done: true }

Generator函式有多種理解角度。從語法上,可以把它理解成一個狀態機,封裝了多個內部狀態。

執行Generator函式會返回一個遍歷器物件,也就是說,Generator函式除了狀態機,還是一個遍歷器物件生成函式

generator用來與promise搭配使用 將非同步回撥看起來變成同步模式

先來看一個讀取檔案的例子

讀取檔案1.txt中的內容content1,content1又是一個檔案的路徑,繼續讀取檔案content1中的內容content2,並返回結果

function read(path) {
    return new Promise(function (resolve, reject) {
        require('fs').readFile(path, 'utf8', function (err, data) {
            if (err) reject(err);
            resolve(data);
        })
    })
}
function* r() {
    let content1 = yield read('1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
let it = r();
it.next().value.then(function(data1){
    it.next(data1).value.then(function(data2){
        console.log(data2);
        console.log(it.next(data2).value);
    })
})
複製程式碼

先將非同步readFile方法promise化,即呼叫read方法會得到一個執行readFile方法的promise;
Generator生成器函式依次輸出(yield)各檔案的內容,最後return返回
使用時,先得到迭代器物件it,第一次呼叫next()得到的value即為讀取1.txt的promise,既然是promise,呼叫其then方法處理成功回撥,data1就是1.txt讀取成功時返回的內容,將data1作為下一個next的引數,即content1這時就是data1,同理next返回的value是讀取content1檔案的promise,所以data2就是成功讀取content1的返回值

上例可以簡化read函式,利用一些庫提供的promisify可以直接將函式promise化,如

let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);
複製程式碼

promisify參照 juejin.im/post/5ab4f4…

promise的使用方法參照juejin.im/post/5aae65…

但是上面不停的呼叫next()方法,並容易形成巢狀,也是我們希望簡化的
這裡使用co庫,來幫我們自動的將generator迭代

co庫

安裝: npm install co 上例可以簡化為

let bluebird = require('bluebird');
let fs = require('fs');
let co = require('co');
 
let read = bluebird.promisify(fs.readFile);
function* r() {
    let content1 = yield read('1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
co(r()).then(function (data) {
    console.log(data)
})
複製程式碼

那麼co庫是怎麼實現自動迭代的呢,基於上例這裡簡單實現一下

function co(it) {
    return new Promise(function (resolve, reject) {
        function step(d) {
            let { value, done } = it.next(d);
            if (!done) {
                value.then(function (data) { // 2,txt
                    step(data)
                }, reject)
            } else {
                resolve(value);
            }
        }
        step();
    });
}
複製程式碼

首先從用法可以看出,co執行會返回一個promise,用then註冊成功/失敗回撥,所以先return 一個promise
co將迭代器it作為引數,這裡每呼叫一次step,就執行一次next
既然是自動執行,那麼promise的executor中先執行一次it.next()方法,返回value和done;value是一個pending的Promise;如果done=false,說明還沒有走完,繼續在value.then的成功回撥中執行下一次next,即呼叫step方法;直到done為true,走完所有程式碼,呼叫resolve;中間有任何一次next異常,直接呼叫reject,停止迭代

相關文章