Promise,Async,await簡介

Promise 對象

轉載:http://wiki.jikexueyuan.com/project/es6/promise.htmljavascript

基本用法

ES6 原生提供了 Promise 對象。所謂 Promise 對象,就是表明了某個將來纔會知道結果的事件(一般是一個異步操做),而且這個事件提供統一的 API,可供進一步處理。html

有了 Promise 對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供的接口,使得控制異步操做更加容易。Promise 對象的概念的詳細解釋,請參考《JavaScript標準參考教程》java

ES6 的 Promise 對象是一個構造函數,用來生成 Promise 實例。es6

1
2
3
4
5
6
7
8
9
10
11
12
13
var  promise =  new  Promise( function (resolve, reject) {
   if  ( /* 異步操做成功 */ ){
     resolve(value);
   else  {
     reject(error);
   }
});
 
promise.then( function (value) {
   // success
},  function (value) {
   // failure
});

  

上面代碼中,Promise 構造函數接受一個函數做爲參數,該函數的兩個參數分別是 resolve 方法和 reject 方法。若是異步操做成功,則用 resolve 方法將 Promise 對象的狀態,從「未完成」變爲「成功」(即從 pending 變爲 resolved);若是異步操做失敗,則用 reject 方法將 Promise 對象的狀態,從「未完成」變爲「失敗」(即從 pending 變爲 rejected)。ajax

Promise 實例生成之後,能夠用 then 方法分別指定 resolve 方法和 reject 方法的回調函數。shell

下面是一個使用 Promise 對象的簡單例子。json

1
2
3
4
5
6
7
8
9
function  timeout(ms) {
   return  new  Promise((resolve) => {
     setTimeout(resolve, ms);
   });
}
 
timeout(100).then(() => {
   console.log( 'done' );
});

  

上面代碼中,timeout 方法返回一個 Promise 實例,表示一段時間之後纔會發生的結果。一旦 Promise 對象的狀態變爲 resolved,就會觸發 then 方法綁定的回調函數。數組

下面是一個用 Promise 對象實現的 Ajax 操做的例子。promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var  getJSON =  function (url) {
   var  promise =  new  Promise( function (resolve, reject){
     var  client =  new  XMLHttpRequest();
     client.open( "GET" , url);
     client.onreadystatechange = handler;
     client.responseType =  "json" ;
     client.setRequestHeader( "Accept" "application/json" );
     client.send();
 
     function  handler() {
       if  ( this .status === 200) {
         resolve( this .response);
       else  {
         reject( new  Error( this .statusText));
       }
     };
   });
 
   return  promise;
};
 
getJSON( "/posts.json" ).then( function (json) {
   console.log( 'Contents: '  + json);
},  function (error) {
   console.error( '出錯了' , error);
});

  

上面代碼中,getJSON 是對 XMLHttpRequest 對象的封裝,用於發出一個針對 JSON 數據的 HTTP 請求,而且返回一個 Promise 對象。須要注意的是,在 getJSON 內部,resolve 方法和 reject 方法調用時,都帶有參數。併發

若是調用 resolve 方法和 reject 方法時帶有參數,那麼它們的參數會被傳遞給回調函數。reject 方法的參數一般是 Error 對象的實例,表示拋出的錯誤;resolve 方法的參數除了正常的值之外,還多是另外一個 Promise 實例,表示異步操做的結果有多是一個值,也有多是另外一個異步操做,好比像下面這樣。

1
2
3
4
5
6
7
8
var  p1 =  new  Promise( function (resolve, reject){
   // ...
});
 
var  p2 =  new  Promise( function (resolve, reject){
   // ...
   resolve(p1);
})

  

上面代碼中,p1 和 p2 都是 Promise 的實例,可是 p2 的 resolve 方法將 p1 做爲參數,p1 的狀態就會傳遞給 p2。

注意,這時 p1 的狀態決定了 p2 的狀態。若是 p1 的狀態是 pending,那麼 p2 的回調函數就會等待 p1 的狀態改變;若是 p1 的狀態已是 fulfilled 或者 rejected,那麼 p2 的回調函數將會馬上執行。

Promise.prototype.then()

Promise.prototype.then 方法返回的是一個新的Promise對象,所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。

1
2
3
4
5
getJSON( "/posts.json" ).then( function (json) {
   return  json.post;
}).then( function (post) {
   // ...
});

  

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。

若是前一個回調函數返回的是Promise對象,這時後一個回調函數就會等待該Promise對象有了運行結果,纔會進一步調用。

 

1
2
3
4
5
getJSON( "/post/1.json" ).then( function (post) {
   return  getJSON(post.commentURL);
}).then( function (comments) {
   // ...
});

  

then方法還能夠接受第二個參數,表示Promise對象的狀態變爲rejected時的回調函數。

Promise.prototype.catch()

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

1
2
3
4
5
6
getJSON( "/posts.json" ).then( function (posts) {
   // ...
}). catch ( function (error) {
   // 處理前一個回調函數運行時發生的錯誤
   console.log( '發生錯誤!' , error);
});

  

上面代碼中,getJSON方法返回一個Promise對象,若是該對象運行正常,則會調用then方法指定的回調函數;若是該方法拋出錯誤,則會調用catch方法指定的回調函數,處理這個錯誤。

下面是一個例子。

1
2
3
4
5
var  promise =  new  Promise( function (resolve, reject) {
   throw  new  Error( 'test' )
});
promise. catch ( function (error) { console.log(error) });
// Error: test

  

上面代碼中,Promise拋出一個錯誤,就被catch方法指定的回調函數捕獲。

若是Promise狀態已經變成resolved,再拋出錯誤是無效的。

1
2
3
4
5
6
7
8
var  promise =  new  Promise( function (resolve, reject) {
   resolve( "ok" );
   throw  new  Error( 'test' );
});
promise
   .then( function (value) { console.log(value) })
   . catch ( function (error) { console.log(error) });
// ok

  

上面代碼中,Promise在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。

Promise對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。

1
2
3
4
5
6
7
getJSON( "/post/1.json" ).then( function (post) {
   return  getJSON(post.commentURL);
}).then( function (comments) {
   // some code
}). catch ( function (error) {
   // 處理前面三個Promise產生的錯誤
});

  

上面代碼中,一共有三個Promise對象:一個由getJSON產生,兩個由then產生。它們之中任何一個拋出的錯誤,都會被最後一個catch捕獲。

跟傳統的try/catch代碼塊不一樣的是,若是沒有使用catch方法指定錯誤處理的回調函數,Promise對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。

1
2
3
4
5
6
7
8
9
10
var  someAsyncThing =  function () {
   return  new  Promise( function (resolve, reject) {
     // 下面一行會報錯,由於x沒有聲明
     resolve(x + 2);
   });
};
 
someAsyncThing().then( function () {
   console.log( 'everything is great' );
});

  

上面代碼中,someAsyncThing函數產生的Promise對象會報錯,可是因爲沒有調用catch方法,這個錯誤不會被捕獲,也不會傳遞到外層代碼,致使運行後沒有任何輸出。

1
2
3
4
5
6
7
var  promise =  new  Promise( function (resolve, reject) {
   resolve( "ok" );
   setTimeout( function () {  throw  new  Error( 'test' ) }, 0)
});
promise.then( function (value) { console.log(value) });
// ok
// Uncaught Error: test

  

上面代碼中,Promise指定在下一輪「事件循環」再拋出錯誤,結果因爲沒有指定catch語句,就冒泡到最外層,成了未捕獲的錯誤。

Node.js有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤。

1
2
3
process.on( 'unhandledRejection' function  (err, p) {
   console.error(err.stack)
});

  

上面代碼中,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是報錯的Promise實例,它能夠用來了解發生錯誤的環境信息。。

須要注意的是,catch方法返回的仍是一個Promise對象,所以後面還能夠接着調用then方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var  someAsyncThing =  function () {
   return  new  Promise( function (resolve, reject) {
     // 下面一行會報錯,由於x沒有聲明
     resolve(x + 2);
   });
};
 
someAsyncThing().then( function () {
   return  someOtherAsyncThing();
}). catch ( function (error) {
   console.log( 'oh no' , error);
}).then( function () {
   console.log( 'carry on' );
});
// oh no [ReferenceError: x is not defined]
// carry on

  

上面代碼運行完catch方法指定的回調函數,會接着運行後面那個then方法指定的回調函數。

catch方法之中,還能再拋出錯誤。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var  someAsyncThing =  function () {
   return  new  Promise( function (resolve, reject) {
     // 下面一行會報錯,由於x沒有聲明
     resolve(x + 2);
   });
};
 
someAsyncThing().then( function () {
   return  someOtherAsyncThing();
}). catch ( function (error) {
   console.log( 'oh no' , error);
   // 下面一行會報錯,由於y沒有聲明
   y + 2;
}).then( function () {
   console.log( 'carry on' );
});
// oh no [ReferenceError: x is not defined]

  

上面代碼中,catch方法拋出一個錯誤,由於後面沒有別的catch方法了,致使這個錯誤不會被捕獲,也不會到傳遞到外層。若是改寫一下,結果就不同了。

1
2
3
4
5
6
7
8
9
10
11
someAsyncThing().then( function () {
   return  someOtherAsyncThing();
}). catch ( function (error) {
   console.log( 'oh no' , error);
   // 下面一行會報錯,由於y沒有聲明
   y + 2;
}). catch ( function (error) {
   console.log( 'carry on' , error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

  

上面代碼中,第二個catch方法用來捕獲,前一個catch方法拋出的錯誤。

Promise.all(),Promise.race()

Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。

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

  

上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是Promise對象的實例。(Promise.all方法的參數不必定是數組,可是必須具備iterator接口,且返回的每一個成員都是Promise實例。)

p的狀態由p一、p二、p3決定,分紅兩種狀況。

(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

下面是一個具體的例子。

1
2
3
4
5
6
7
8
9
10
// 生成一個Promise對象的數組
var  promises = [2, 3, 5, 7, 11, 13].map( function (id){
   return  getJSON( "/post/"  + id +  ".json" );
});
 
Promise.all(promises).then( function (posts) {
   // ... 
}). catch ( function (reason){
   // ...
});

  

Promise.race方法一樣是將多個Promise實例,包裝成一個新的Promise實例。

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

  

上面代碼中,只要p一、p二、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的Promise實例的返回值,就傳遞給p的返回值。

若是Promise.all方法和Promise.race方法的參數,不是Promise實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。

Promise.resolve(),Promise.reject()

有時須要將現有對象轉爲Promise對象,Promise.resolve方法就起到這個做用。

1
var  jsPromise = Promise.resolve($.ajax( '/whatever.json' ));

  

上面代碼將jQuery生成deferred對象,轉爲一個新的ES6的Promise對象。

若是Promise.resolve方法的參數,不是具備then方法的對象(又稱thenable對象),則返回一個新的Promise對象,且它的狀態爲fulfilled。

1
2
3
4
5
6
var  p = Promise.resolve( 'Hello' );
 
p.then( function  (s){
   console.log(s)
});
// Hello

  

上面代碼生成一個新的Promise對象的實例p,它的狀態爲fulfilled,因此回調函數會當即執行,Promise.resolve方法的參數就是回調函數的參數。

因此,若是但願獲得一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。

1
2
3
4
5
var  p = Promise.resolve();
 
p.then( function  () {
   // ...
});

  

上面代碼的變量p就是一個Promise對象。

若是Promise.resolve方法的參數是一個Promise對象的實例,則會被原封不動地返回。

Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態爲rejected。Promise.reject方法的參數reason,會被傳遞給實例的回調函數。

1
2
3
4
5
6
var  p = Promise.reject( '出錯了' );
 
p.then( null function  (s){
   console.log(s)
});
// 出錯了

  

上面代碼生成一個Promise對象的實例p,狀態爲rejected,回調函數會當即執行。

Generator函數與Promise的結合

使用Generator函數管理流程,遇到異步操做的時候,一般返回一個Promise對象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function  getFoo () {
   return  new  Promise( function  (resolve, reject){
     resolve( 'foo' );
   });
}
 
var  g =  function * () {
   try  {
     var  foo =  yield  getFoo();
     console.log(foo);
   catch  (e) {
     console.log(e);
   }
};
 
function  run (generator) {
   var  it = generator();
 
   function  go(result) {
     if  (result.done)  return  result.value;
 
     return  result.value.then( function  (value) {
       return  go(it.next(value));
     },  function  (error) {
       return  go(it. throw (value));
     });
   }
 
   go(it.next());
}
 
run(g);

  

上面代碼的Generator函數g之中,有一個異步操做getFoo,它返回的就是一個Promise對象。函數run用來處理這個Promise對象,並調用下一個next方法。

async函數

概述

async函數與Promise、Generator函數同樣,是用來取代回調函數、解決異步操做的一種方法。它本質上是Generator函數的語法糖。async函數並不屬於ES6,而是被列入了ES7,可是traceur、Babel.js、regenerator等轉碼器已經支持這個功能,轉碼後馬上就能使用。

下面是一個Generator函數,依次讀取兩個文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var  fs = require( 'fs' );
 
var  readFile =  function  (fileName){
   return  new  Promise( function  (resolve, reject){
     fs.readFile(fileName,  function (error, data){
       if  (error) reject(error);
       resolve(data);
     });
   });
};
 
var  gen =  function * (){
   var  f1 =  yield  readFile( '/etc/fstab' );
   var  f2 =  yield  readFile( '/etc/shells' );
   console.log(f1.toString());
   console.log(f2.toString());
};

  

上面代碼中,readFile函數是fs.readFile的Promise版本。

寫成async函數,就是下面這樣。

1
2
3
4
5
6
var  asyncReadFile = async  function  (){
   var  f1 = await readFile( '/etc/fstab' );
   var  f2 = await readFile( '/etc/shells' );
   console.log(f1.toString());
   console.log(f2.toString());
};

  

一比較就會發現,async函數就是將Generator函數的星號(*)替換成async,將yield替換成await,僅此而已。

async函數對Generator函數的改進,體如今如下三點。

(1)內置執行器。Generator函數的執行必須靠執行器,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。

1
var  result = asyncReadFile();

  

(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

(3)更廣的適用性。co函數庫約定,yield命令後面只能是Thunk函數或Promise對象,而async函數的await命令後面,能夠跟Promise對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

實現

async函數的實現,就是將Generator函數和自動執行器,包裝在一個函數裏。

1
2
3
4
5
6
7
8
9
10
11
async  function  fn(args){
   // ...
}
 
// 等同於
 
function  fn(args){
   return  spawn( function *() {
     // ...
   });
}

  

全部的async函數均可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。

下面給出spawn函數的實現,基本就是前文自動執行器的翻版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function  spawn(genF) {
   return  new  Promise( function (resolve, reject) {
     var  gen = genF();
     function  step(nextF) {
       try  {
         var  next = nextF();
       catch (e) {
         return  reject(e);
       }
       if (next.done) {
         return  resolve(next.value);
       }
       Promise.resolve(next.value).then( function (v) {
         step( function () {  return  gen.next(v); });
       },  function (e) {
         step( function () {  return  gen. throw (e); });
       });
     }
     step( function () {  return  gen.next(undefined); });
   });
}

  

用法

同Generator函數同樣,async函數返回一個Promise對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。

下面是一個例子。

1
2
3
4
5
6
7
8
9
async  function  getStockPriceByName(name) {
   var  symbol = await getStockSymbol(name);
   var  stockPrice = await getStockPrice(symbol);
   return  stockPrice;
}
 
getStockPriceByName( 'goog' ).then( function  (result){
   console.log(result);
});

  

上面代碼是一個獲取股票報價的函數,函數前面的async關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個Promise對象。

上面的例子用Generator函數表達,就是下面這樣。

1
2
3
4
5
6
7
function  getStockPriceByName(name) {
   return  spawn( function *(name) {
     var  symbol =  yield  getStockSymbol(name);
     var  stockPrice =  yield  getStockPrice(symbol);
     return  stockPrice;
   });
}

  

上面的例子中,spawn函數是一個自動執行器,由JavaScript引擎內置。它的參數是一個Generator函數。async...await結構本質上,是在語言層面提供的異步任務的自動執行器。

下面是一個更通常性的例子,指定多少毫秒後輸出一個值。

1
2
3
4
5
6
7
8
9
10
11
12
function  timeout(ms) {
   return  new  Promise((resolve) => {
     setTimeout(resolve, ms);
   });
}
 
async  function  asyncPrint(value, ms) {
   await timeout(ms);
   console.log(value)
}
 
asyncPrint( 'hello world' , 50);

  

上面代碼指定50毫秒之後,輸出「hello world」。

注意點

await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catch代碼塊中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async  function  myFunction() {
   try  {
     await somethingThatReturnsAPromise();
   catch  (err) {
     console.log(err);
   }
}
 
// 另外一種寫法
 
async  function  myFunction() {
   await somethingThatReturnsAPromise(). catch ( function  (err){
     console.log(err);
   };
}

  

await命令只能用在async函數之中,若是用在普通函數,就會報錯。

1
2
3
4
5
6
7
8
async  function  dbFuc(db) {
   let  docs = [{}, {}, {}];
 
   // 報錯
   docs.forEach( function  (doc) {
     await db.post(doc);
   });
}

  

上面代碼可能不會正常工做,緣由是這時三個db.post操做將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。

1
2
3
4
5
6
7
async  function  dbFuc(db) {
   let  docs = [{}, {}, {}];
 
   for  ( let  doc of docs) {
     await db.post(doc);
   }
}

  

若是確實但願多個請求併發執行,可使用Promise.all方法。

複製代碼
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的寫法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}
複製代碼

ES6將await增長爲保留字。使用這個詞做爲標識符,在ES5是合法的,在ES6將拋出SyntaxError。

與Promise、Generator的比較

咱們經過一個例子,來看Async函數與Promise、Generator函數的區別。

假定某個DOM元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。若是當中有一個動畫出錯,就再也不往下執行,返回上一個成功執行的動畫的返回值。

首先是Promise的寫法。

複製代碼
function chainAnimationsPromise(elem, animations) {

  // 變量ret用來保存上一個動畫的返回值
  var ret = null;

  // 新建一個空的Promise
  var p = Promise.resolve();

  // 使用then方法,添加全部動畫
  for(var anim in animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    })
  }

  // 返回一個部署了錯誤捕捉機制的Promise
  return p.catch(function(e) {
    /* 忽略錯誤,繼續執行 */
  }).then(function() {
    return ret;
  });

}
複製代碼

雖然Promise的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是Promise的API(then、catch等等),操做自己的語義反而不容易看出來。

接着是Generator函數的寫法。

複製代碼
function chainAnimationsGenerator(elem, animations) {

  return spawn(function*() {
    var ret = null;
    try {
      for(var anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略錯誤,繼續執行 */
    }
      return ret;
  });

}
複製代碼

上面代碼使用Generator函數遍歷了每一個動畫,語義比Promise寫法更清晰,用戶定義的操做所有都出如今spawn函數的內部。這個寫法的問題在於,必須有一個任務運行器,自動執行Generator函數,上面代碼的spawn函數就是自動執行器,它返回一個Promise對象,並且必須保證yield語句後面的表達式,必須返回一個Promise。

最後是Async函數的寫法。

複製代碼
async function chainAnimationsAsync(elem, animations) {
  var ret = null;
  try {
    for(var anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略錯誤,繼續執行 */
  }
  return ret;
}
複製代碼

能夠看到Async函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將Generator寫法中的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。若是使用Generator寫法,自動執行器須要用戶本身提供。

相關文章
相關標籤/搜索