ES6中除了上篇文章講過的語法新特性和一些新的API以外,還有兩個很是重要的新特性就是Promise和Generator,今天咱們將會詳細講解一下這兩個新特性。node
Promise 是異步編程的一種解決方案,比傳統的解決方案「回調函數和事件」更合理和更強大。es6
所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。ajax
從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。編程
Promise有兩個特色:json
Promise對象表明一個異步操做,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。數組
只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。promise
Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。框架
這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。koa
Promise將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。異步
Promise對象提供統一的接口,使得控制異步操做更加容易。
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]);
Promise.race方法一樣是將多個Promise實例,包裝成一個新的Promise實例
var p = Promise.race([p1,p2,p3]);
只要p一、p二、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數.
Promise.resolve()將現有對象轉爲Promise對象.
Promise.resolve('js'); //等價於 new Promise(resolve => resolve('js'));
那麼什麼樣的對象可以轉化成爲Promise對象呢?
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的博客
歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!