天天看點

ES6中的Promise和Generator詳解

目錄

  • 簡介
  • Promise
    • 什麼是Promise
    • Promise的特點
    • Promise的優點
    • Promise的缺點
    • Promise的用法
    • Promise的執行順序
    • Promise.prototype.then()
    • Promise.prototype.catch()
    • Promise.all()
    • Promise.race()
    • Promise.resolve()
    • Promise.reject()
    • done()
    • finally()
  • Generator
    • 什麼是Generator
    • yield
    • yield *
    • 異步操作的同步化表達
  • Generator 的異步應用
    • 回調函數
    • Thunk函數和異步函數自動執行
  • 總結

ES6中除了上篇文章講過的文法新特性和一些新的API之外,還有兩個非常重要的新特性就是Promise和Generator,今天我們将會詳細講解一下這兩個新特性。

Promise 是異步程式設計的一種解決方案,比傳統的解決方案“回調函數和事件”更合理和更強大。

所謂Promise,簡單說就是一個容器,裡面儲存着某個未來才會結束的事件(通常是一個異步操作)的結果。

從文法上說,Promise 是一個對象,從它可以擷取異步操作的消息。

Promise有兩個特點:

  1. 對象的狀态不受外界影響。

Promise對象代表一個異步操作,有三種狀态:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。

隻有異步操作的結果,可以決定目前是哪一種狀态,任何其他操作都無法改變這個狀态。

  1. 一旦狀态改變,就不會再變,任何時候都可以得到這個結果。

Promise對象的狀态改變,隻有兩種可能:從Pending變為Resolved和從Pending變為Rejected。

這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。

Promise将異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。

Promise對象提供統一的接口,使得控制異步操作更加容易。

  1. 無法取消Promise,一旦建立它就會立即執行,無法中途取消。
  2. 如果不設定回調函數,Promise内部抛出的錯誤,不會反應到外部。
  3. 當處于Pending狀态時,無法得知目前進展到哪一個階段(剛剛開始還是即将完成)。

Promise對象是一個構造函數,用來生成Promise執行個體:

var promise = new Promise(function(resolve, reject) { 
// ... some code 
if (/* 異步操作成功 */){ 
resolve(value); 
} else { reject(error); } 
}
);
           

promise可以接then操作,then操作可以接兩個function參數,第一個function的參數就是建構Promise的時候resolve的value,第二個function的參數就是建構Promise的reject的error。

promise.then(function(value) { 
// success 
}, function(error) { 
// failure }
);
           

我們看一個具體的例子:

function timeout(ms){
    return new Promise(((resolve, reject) => {
        setTimeout(resolve,ms,'done');
    }))
}

timeout(100).then(value => console.log(value));
           

Promise中調用了一個setTimeout方法,并會定時觸發resolve方法,并傳入參數done。

最後程式輸出done。

Promise一經建立就會立馬執行。但是Promise.then中的方法,則會等到一個調用周期過後再次調用,我們看下面的例子:

let promise = new Promise(((resolve, reject) => {
    console.log('Step1');
    resolve();
}));

promise.then(() => {
    console.log('Step3');
});

console.log('Step2');

輸出:
Step1
Step2
Step3
           

then方法傳回的是一個新的Promise執行個體(注意,不是原來那個Promise執行個體)。是以可以采用鍊式寫法,即then方法後面再調用另一個then方法.

getJSON("/users.json").then(function(json){
    return json.name;
}).then(function(name){
    console.log(name);
});
           

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成以後,會将傳回結果作為參數,傳入第二個回調函數

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定發生錯誤時的回調函數。

getJSON("/users.json").then(function(json){
    return json.name;
}).catch(function(error){
    console.log(error);
});
           

Promise 對象的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲

getJSON("/users.json").then(function(json){
    return json.name;
}).then(function(name){
    console.log(name);
}).catch(function(error){
    //處理前面所有産生的錯誤
    console.log(error);
});
           

Promise.all方法用于将多個Promise執行個體,包裝成一個新的Promise執行個體

var p = Promise.all([p1,p2,p3]);
           
  1. 隻有p1、p2、p3的狀态都變成fulfilled,p的狀态才會變成fulfilled,此時p1、p2、p3的傳回值組成一個數組,傳遞給p的回調函數。
  2. 隻要p1、p2、p3之中有一個被rejected,p的狀态就變成rejected,此時第一個被reject的執行個體的傳回值,會傳遞給p的回調函數。

Promise.race方法同樣是将多個Promise執行個體,包裝成一個新的Promise執行個體

var p = Promise.race([p1,p2,p3]);
           

隻要p1、p2、p3之中有一個執行個體率先改變狀态,p的狀态就跟着改變。那個率先改變的 Promise 執行個體的傳回值,就傳遞給p的回調函數.

Promise.resolve()将現有對象轉為Promise對象.

Promise.resolve('js');
//等價于
new Promise(resolve => resolve('js'));
           

那麼什麼樣的對象能夠轉化成為Promise對象呢?

  1. 參數是一個Promise執行個體
  2. 參數是一個thenable對象
  3. 參數不是具有then方法的對象,或根本就不是對象
  4. 不帶有任何參數

Promise.reject(reason)方法也會傳回一個新的 Promise 執行個體,該執行個體的狀态為rejected

var p = Promise.reject('error');
//等價于
var p = new Promise((resolve,reject) => reject('error'));
           

Promise.reject()方法的參數,會原封不動地作為reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一緻

Promise對象的回調鍊,不管以then方法或catch方法結尾,要是最後一個方法抛出錯誤,都有可能無法捕捉到(因為Promise内部的錯誤不會冒泡到全局)。是以,我們可以提供一個done方法,總是處于回調鍊的尾端,保證抛出任何可能出現的錯誤

asyncFunc().then(f1).catch(f2).then(f3).done();
           

finally方法用于指定不管Promise對象最後狀态如何,都會執行的操作。它與done方法的最大差別,它接受一個普通的回調函數作為參數,該函數不管怎樣都必須執行.

server.listen(1000).then(function(){
    //do something
}.finally(server.stop);
           

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

從文法上,首先可以把它了解成,Generator函數是一個狀态機,封裝了多個内部狀态

執行 Generator 函數會傳回一個周遊器對象.

形式上,Generator 函數是一個普通函數,但是有兩個特征。一是,function關鍵字與函數名之間有一個星号;二是,函數體内部使用yield語句,定義不同的内部狀态。

舉個例子:

function * helloWorldGenerator(){
    yield 'hello';
    yield 'world';
    return 'ending';
}

var gen = helloWorldGenerator();
           

輸出結果:

console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

{ value: 'hello', done: false }
{ value: 'world', done: false }
{ value: 'ending', done: true }
           

周遊器對象的next方法的運作邏輯如下:

(1)遇到yield語句,就暫停執行後面的操作,并将緊跟在yield後面的那個表達式的值,作為傳回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield語句。

(3)如果沒有再遇到新的yield語句,就一直運作到函數結束,直到return語句為止,并将return語句後面的表達式的值,作為傳回的對象的value屬性值。

(4)如果該函數沒有return語句,則傳回的對象的value屬性值為undefined。

注意,yield句本身沒有傳回值,或者說總是傳回undefined。

next方法可以帶一個參數,該參數就會被當作上一個yield語句的傳回值。

function * f() {
    for( let i =0; true; i++){
        let reset = yield i;
        if(reset){
            i = -1;
        }
    }
}

let g = f();
console.log(g.next());
console.log(g.next());
console.log(g.next(true));
           
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 0, done: false }
           

可以看到最後的一步,我們使用next傳入的true替代了i的值,最後導緻i= -1 + 1 = 0.

我們再看一個例子:

function * f2(x){
    var y = 2 * ( yield ( x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var r1= f2(5);
console.log(r1.next());
console.log(r1.next());
console.log(r1.next());

var r2= f2(5);
console.log(r2.next());
console.log(r2.next(12));
console.log(r2.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本身是沒有傳回值的,是以我們會得到NaN。

但是如果next傳入特定的值,則該值會替換該yield,成為真正的傳回值。

如果在 Generator 函數内部,調用另一個 Generator 函數,預設情況下是沒有效果的

function * a1(){
    yield 'a';
    yield 'b';
}

function * b1(){
    yield 'x';
    a1();
    yield 'y';
}

for(let v of b1()){
    console.log(v);
}
           
x
y
           

可以看到,在b1中調用a1是沒有效果的。

将上面的例子修改一下:

function * a1(){
    yield 'a';
    yield 'b';
}

function * b1(){
    yield 'x';
    yield * a1();
    yield 'y';
}

for(let v of b1()){
    console.log(v);
}
           
x
a
b
y
           

Generator函數的暫停執行的效果,意味着可以把異步操作寫在yield語句裡面,等到調用next方法時再往後執行。這實際上等同于不需要寫回調函數了,因為異步操作的後續操作可以放在yield語句下面,反正要等到調用next方法時再執行。是以,Generator函數的一個重要實際意義就是用來處理異步操作,改寫回調函數。

我們看一個怎麼通過Generator來擷取一個Ajax的結果。

function * ajaxCall(){
    let result = yield request("http://www.flydean.com");
    let resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url){
    makeAjaxCall(url, function(response){
        it.next(response);
    });
}

var it = ajaxCall();
it.next();
           

我們使用一個yield來擷取異步執行的結果。但是我們如何将這個yield傳給result變量呢?要記住yield本身是沒有傳回值的。

我們需要調用generator的next方法,将異步執行的結果傳進去。這就是我們在request方法中做的事情。

什麼是異步應用呢?

所謂"異步",簡單說就是一個任務不是連續完成的,可以了解成該任務被人為分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。

比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統送出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統傳回檔案,再接着執行任務的第二段(處理檔案)。這種不連續的執行,就叫做異步。

相應地,連續的執行就叫做同步。由于是連續執行,不能插入其他任務,是以作業系統從硬碟讀取檔案的這段時間,程式隻能幹等着。

ES6誕生以前,異步程式設計的方法,大概有下面四種。

事件監聽

釋出/訂閱

Promise 對象

fs.readFile(fileA, 'utf-8', function(error,data){
    fs.readFile(fileB, 'utf-8', function(error,data){
}
})
           

如果依次讀取兩個以上的檔案,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。因為多個異步操作形成了強耦合,隻要有一個操作需要修改,它的上層回調函數和下層回調函數,可能都要跟着修改。這種情況就稱為"回調函數地獄"(callback hell)。

Promise 對象就是為了解決這個問題而提出的。它不是新的文法功能,而是一種新的寫法,允許将回調函數的嵌套,改成鍊式調用。

let readFile = require('fs-readfile-promise');
readFile(fileA).then(function(){
    return readFile(fileB);
}).then(function(data){
    console.log(data);
})
           

在講Thunk函數之前,我們講一下函數的調用有兩種方式,一種是傳值調用,一種是傳名調用。

"傳值調用"(call by value),即在進入函數體之前,就計算x + 5的值(等于6),再将這個值傳入函數f。C語言就采用這種政策。

“傳名調用”(call by name),即直接将表達式x + 5傳入函數體,隻在用到它的時候求值。

編譯器的“傳名調用”實作,往往是将參數放到一個臨時函數之中,再将這個臨時函數傳入函數體。這個臨時函數就叫做 Thunk 函數。

function f(m){
    return m * 2;
}

f(x + 5);
           

上面的代碼等于:

var thunk = function () {
    return x + 5;
}
function f(thunk){
    return thunk() * 2;
}
           

在 JavaScript 語言中,Thunk函數替換的不是表達式,而是多參數函數,将其替換成一個隻接受回調函數作為參數的單參數函數。

怎麼解釋呢?

比如nodejs中的:

fs.readFile(filename,[encoding],[callback(err,data)])
           

readFile接收3個參數,其中encoding是可選的。我們就以兩個參數為例。

一般來說,我們這樣調用:

fs.readFile(fileA,callback);
           

那麼有沒有辦法将其改寫成為單個參數的function的級聯調用呢?

var Thunk = function (fn){
    return function (...args){
        return functon (callback){
            return fn.call(this,...args, callback);
        }
    }
}

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
           

可以看到上面的Thunk将兩個參數的函數改寫成為了單個參數函數的級聯方式。或者說Thunk是接收一個callback并執行方法的函數。

這樣改寫有什麼用呢?Thunk函數現在可以用于 Generator 函數的自動流程管理。

之前在講Generator的時候,如果Generator中有多個yield的異步方法,那麼我們需要在next方法中傳入這些異步方法的執行結果。

手動傳入異步執行結果當然是可以的。但是有沒有自動執行的辦法呢?

let fs = require('fs');
let thunkify = require('thunkify');
let readFileThunk = thunkify(fs.readFile);

let gen = function * (){
    let r1 = yield readFileThunk('/tmp/file1');
    console.log(r1.toString());

    let r2 = yield readFileThunk('/tmp/file2');
    console.log(r2.toString());
}

let g = gen();

function run(fn){
    let gen = fn();

    function next (err, data){
        let result = gen.next(data);
        if(result.done) return;
        result.value(next);
    }
    next();
}

run(g);
           

gen.next傳回的是一個對象,對象的value就是Thunk函數,我們向Thunk函數再次傳入next callback,進而出發下一次的yield操作。

有了這個執行器,執行Generator函數友善多了。不管内部有多少個異步操作,直接把 Generator 函數傳入run函數即可。當然,前提是每一個異步操作,都要是Thunk函數,也就是說,跟在yield指令後面的必須是Thunk函數。

Promise和Generator是ES6中引入的非常中要的文法,後面的koa架構就是Generator的一種具體的實作。我們會在後面的文章中詳細講解koa的使用,敬請期待。

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/es6-promise-generator/

本文來源:flydean的部落格

歡迎關注我的公衆号:「程式那些事」最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!