JavaScript ES6 中的Promise

1. Promises/A+規範html

CommonJS之Promises/A規範,經過規範API接口來簡化異步編程,使咱們的異步邏輯代碼更易理解。jquery

這裏就不作更多的囉嗦了,詳細請看
官方文檔https://promisesaplus.com/
中文翻譯http://www.ituring.com.cn/article/66566git

Promises/A+規範的實現有不少:
JQuery的deferred https://github.com/jquery/jquery
bluebird模塊https://github.com/petkaantonov/bluebird
q模塊https://github.com/kriskowal/q
async模塊https://github.com/caolan/asyncgithub

本文主要介紹ES6規範的Promise對象web

2.Promise對象ajax

一個Promise實例對象,表示一次異步操做的封裝,異步操做的結果有兩種結果:成功或失敗。而後根據異步操做的結果採起不一樣的操做。而且能夠多 個Promise串聯起來使用,也就是把異步操做串聯起來,組織成併發或者串行工做流等。經過Promise咱們能夠把複雜的異步代碼看起來更簡潔,理解 起來更容易。數據庫

圖片描述
圖片描述
圖片描述

2.1生成Promise實例對象編程

Promise對象是一個構造器函數對象,在使用Promise以前,須要先生成一個Promise對象的實例對象。json

let promise = new Promise(function(resolve,  reject) {
          //異步操做
    if (•••) {
               resolve(value); // success
           } else {
               reject(reason); // failure
          }});

須要傳入一個固定格式的函數做爲參數:function(resolve,  reject) {}。數組

通常狀況下,按照這一標準格式去使用,在這個參數函數體中,編寫異步操做代碼。而後根據異步操做的結果判斷,若是成功,則調用resolve,不然 調用reject。能夠這樣理解:一次異步執行,首先等獲得異步執行的結果,而後根據結果決定下一步作什麼,這和流程圖的步驟相似。

resolve和reject都是內部提供的方法,Promise實例對象是JS引擎自動調用的,因此這兩個參數有JS內部提供,直接使用就能夠。

(1)resolve(value):將Promise對象的狀態從「未完成」變爲「成功」(即從Pending變爲Resolved)。並把Promise實例對象的value設置爲參數value。以後會調用then方法中的onFulfilled。(2) reject(reason):將Promise對象的狀態從「未完成」變爲「失敗」(即從Pending變爲Rejected)。並把Promise實例對象的reason設置爲參數reason。以後會調用then方法中的onRejected。

小樣:Promise生成的實例對象會被系統異步自動調用

生成Promise實例對象後,function(resolve,  reject) {}參數會被異步自動調用,知道這點很重要。
圖片描述
圖片描述
咱們模擬了一次生成Promise實例對象,根據打印的結果,發現,function(resolve,  reject) {}被自動調用了,可是這不能肯定這就是異步調用的,因爲操做的複雜性,就不真實模擬了,記住這個結論:function(resolve,   reject) {}是系統自動異步調用的,具體時間是不肯定的。

咱們使用了setTimeout模擬了一次異步操做,把它封裝到一個Promise實例對象中。當異步操做完成,使用resolve把pm狀態修改成resolved,而且把pm的值設置」success」。

2.2Promise原型方法

2.2.1 Promise.prototype.then

promise.then(onFulfilled, onRejected)

then方法用來註冊Promise實例對象狀態從pending變成其餘狀態後調用的回調函數。onFulfilled和 onRejected都必須爲函數。then方法使得異步編程能夠實現鏈式調用。

參數:(1) onFulfilled:可選參數,函數, Promise實例對象從pending變成fulfilled狀態,以後會使用Promise實例對象的value調用onFulfilled。例如:在function(resolve,  reject) {}調用 resolve(value)以後。
 (2) onRejected:可選參數,函數,Promise實例對象從pending變成rejected狀態,以後會使用Promise實例對象的reason調用onRejected。例如:在function(resolve,  reject) {}調用reject (reason)以後。返回值:then方法會當即返回一個新的Promise實例對象。當onFulfilled和onRejected返回值是Promise實例pm的時候,then返回的Promise實例的狀態會由pm的狀態決定,詳細請參照Promise規範,知道這點很是重要!

小樣:then的基本使用
圖片描述

2.2.2 catch

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

在Promise實例對象的狀態變成fulfilled或者rejected以前,只要發生錯誤,就會執行這個回調。
參數和返回值和then相似。

圖片描述
圖片描述
小樣:then(onRejected)和catch(onRejected)的異同
圖片描述
圖片描述
圖片描述

拋出的異常不能被 onRejected捕獲,卻能夠被catch捕獲。因此最佳實踐是:使用catch而不是onRejected,避免有些異常不能被捕捉到。

小樣:異常冒泡
圖片描述
異常會向下冒泡,直到遇到catch,中間的then會被忽略。

2.3Promise對象的方法

2.3.1 resolve

Promise. resolve([value])

把value轉換成Promise。通常用來把一個對象方便轉換成Promise實例對象。TJ的CO模塊就用到了這個方法。

能夠這樣理解:
Promise.resolve(value)
// 等價於
new Promise(resolve => resolve(value))

參數:(1)value:可選。能夠是任意的JavaScript合法值。返回值:若是value是非Promise實例對象,則返回一個新的Promise實例對象,且它的狀態是fulfilled,它的值爲value;若是value是一個Promise實例對象,那麼返回的也是這個實例對象。

小樣:value爲非Promise實例對象
圖片描述
圖片描述
不論是value是通常對象仍是函數對象,都做爲Promise實例對象的值。

小樣:value爲Promise實例對象
圖片描述
當value爲Promise實例對象的時候,返回的是本身。

2.3.2 reject

Promise. reject ([reason])

reject和resolve是相似的,請參照resolve作類推。

2.3.3 all  

Promise.All(promisesIterator)

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

參數:一個包含Promise實例對象的迭代器。例如數組[p1, p2, …]返回值:一個新的Promise實例對象pm。

*pm狀態由參數中的全部Promise實例對象的狀態決定:

(1)當參數中全部的Promise實例對象的狀態爲fulfilled的時候,pm的狀態爲fulfilled。全部參數Promise實例對象的返回值組成一個數組,做爲pm的值。(2)當參數中的Promise實例對象有一個的狀態爲rejected的時候,它的狀態爲rejected。此時第一個被reject的Promise實例對象的返回值做爲pm的reason。

小樣:全部參數的Promise實例對象的狀態都爲fulfilled

圖片描述
圖片描述
小樣:有一個參數的Promise實例對象的狀態爲rejected
圖片描述
圖片描述

2.3.4 race

Promise.race(promisesIterator)

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

參數:一個包含Promise實例對象的迭代器。例如數組[p1, p2, …]返回值:一個新的Promise實例對象pm。*pm狀態由參數中的第一個改變狀態的Promise實例對象的狀態決定(正如它的名字同樣,race-比賽,勝者爲王!):(1)若是第一個改變的狀態爲fulfilled,那麼pm的狀態爲fulfilled,而且pm的值爲第一個改變狀態的Promise實例對象的值。(2)若是第一個改變的狀態爲rejected,那麼pm的狀態爲reject,而且pm的reason爲第一個狀態改變的Promise實例對象的值。

小樣:第一個改變的狀態爲fulfilled

圖片描述

使用setTimeout函數,讓pm2比pm1延遲一秒修改狀態。pm1在一秒後狀態變爲fulfilled,此時pms的狀態也跟着變爲fulfilled,已經不須要考慮pm2的執行。

小樣:第一個改變的狀態爲rejected

圖片描述
圖片描述

使用setTimeout函數,讓pm1比pm2延遲一秒修改狀態。pm2在一秒後狀態變爲rejected,此時pms的狀態也跟着變爲rejected,已經不須要考慮pm1的執行。

3.Promise的應用

前面已經詳細介紹了Promise的做用和Promise的API使用。下面將把前面介紹的東西整合,作些複雜的小樣。

偶然的機會,發現了Jake Archibald很是棒的Promise實踐例子,因此這裏主要仍是使用他的例子,並加上一些我的的理解。

首先是基於Promise實現的ajax異步加載數據的實現,而後在這個基礎上拓展成複雜的例子。

3.1在Ajax中使用Promise

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };
    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };
    // Make the request
    req.send();
  });}

咱們定義了一個get函數,傳入一個url,並返回一個Promise對象。正如前面強調的同樣,Promise實例對象表明一次完整的異步操做過 程。把異步代碼放在function(resolve, reject) {}中。最後根據ajax的status也就異步操做的結果,決定ajax是否成功執行。

經過這樣巧妙的異步代碼封裝到Promise實例對象中,咱們實現了ajax操做Promise化,能夠實現鏈式使用。避免了傳統的只能使用回調的麻煩。

小樣:鏈式調用

get('story.json').then(function(response) {
  return JSON.parse(response);}).then(function(response) {
  console.log("Yey JSON!", response);});

實現了ajax的鏈式調用後,發現代碼閱讀更美觀,理解起來比使用原來的回調好了不少。第一步,加載了story.json的數據;第二步,把數據轉換成JSON格式;第三步,把JSON數據打印出來。

因爲後面的例子都是使用JSON數據,因此須要對get進一步拓展,直接返回JSON數據:

function getJSON(url) {
  return get(url).then(JSON.parse);}

3.2 Promise實現異步任務順序執行

在實際開發中,會遇到這樣的業務需求:須要把任務按照必定的順序執行。例如:Task-1 ——> Task-2 ——> Task-3……。

例如:須要查詢一個用戶名爲Weber用戶寫過的文章。

第一步:到數據庫查找user name 爲Weber的用戶;第二步:根據Weber的用戶id到數據查找Weber的文章。第三步:返回Weber的用戶信息和Weber的文章
   在Java這樣的IO阻塞語言中很好實現,直接按照步驟寫代碼便可。可是在JS中,數據庫操做是異步的,每次數據庫查詢都要傳入回調函數處理結果。

JS使用傳統方式實現需求:

function getPostsByUserName(name, response) {db.findUserByName(‘weber’, function(user) {
          db.findPostByUserId(user.id, function(posts) {
              response.json(‘userPosts’, {
                   user: user,
                   posts:posts});
          });});}

回調方式能夠實現功能,可是代碼看起來很不美觀,不容易理解,容易形成」回調黑洞」。
下面來看Promise實現異步任務順序執行的例子:

業務需求:使用Ajax加載一篇文章信息story.json,文章信息包含各個段落的基本信息,須要在後臺獲取。要求在頁面按照1,2,3這樣的順序顯示段落。

第一步:取得文章列表story.json第二步:按照story.json文章段落列表的順序,加載段落並按照順序在瀏覽器渲染。第二步能夠看作一個總體,繼續細化:2-1:加載第一段落2-2:加載第二段落2-3:加載第三段落2-n:以此類推。
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // 當前一個章節的 Promise 完成以後……
    return sequence.then(function() {
      // ……獲取下一章
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // 並添加到頁面
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());}).then(function() {
  // 如今所有完成了!
  addTextToPage("All done");}).catch(function(err) {
  // 若是過程當中發生任何錯誤
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  // 保證 spinner 最終會隱藏
  document.querySelector('.spinner').style.display = 'none';});

上述代碼的效果圖(請另預覽git圖片)
圖片描述

2-1,2-2……前後的順序,每一個Promise實例是一個異步操做,必須是前一個Promise實例完成(fulfilled或者 rejected)後一個才進行。這裏巧妙的使用了promise.then方法實現了這樣的控制。then方法會當即返回一個新的Promise實例對 象pm1,咱們在onFulfilled中返回下一個獲取章節的Promise實例對象pm2,pm2決定了pm1的狀態,也就是pm1的then方法需 要pm2的狀態肯定了纔會執行。以此推理,這樣每一個加載章節的Promise實例就被加上前一個控制後一個的限制。

3.3 Promise實現異步任務並行執行

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  // 接受一個 Promise 數組並等待他們所有完成
  return Promise.all(
    // 把章節 URL 數組轉換成對應的 Promise 數組
    story.chapterUrls.map(getJSON)
  );}).then(function(chapters) {
  // 如今咱們有了順序的章節 JSON,遍歷它們……
  chapters.forEach(function(chapter) {
    // ……並添加到頁面中
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");}).catch(function(err) {
  // 捕獲過程當中的任何錯誤
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  document.querySelector('.spinner').style.display = 'none';});

上述代碼的效果圖(請另預覽git圖片)
圖片描述

3.2實現了異步任務的順序執行,可是異步IO是很耗時的,假如其中一個任務很是耗時,那麼後面的任務就會受到很大的影響。因此,在有些狀況下,咱們須要像多線程同樣,併發執行不少任務,用資源換取時間。慶幸的是,瀏覽器是支持ajax多任務併發執行的。

如今3.2的業務要求不變,咱們換種方法實現。注意到,業務要求是按照順序在網頁顯示全部的段落。與其一個個的按照順序加載每個段落,爲何咱們不一樣時把全部的段落加載過來,而後再按照順序渲染每一個段落嗯?

每一個Promise實例對象表示一次異步操做,如今咱們把ajax Promise化了,如今咱們同時有多個異步任務,也就是多個Promise實例對象須要處理,沒錯,Promise.all上場了。

Promise.all中的Promise實例是並行執行的,在全部Promise實例的狀態都爲fulfilled的狀況,Promise實例返回的值的順序也是一致的,因此咱們能夠這樣實現並行加載和順序渲染。

可是Promise.all也有個問題,假如其中一個ajax請求失敗,那麼總體的Promise實例就被rejected,一個任務失敗致使所有段落數據得不到渲染。有時候咱們不但願一個失敗的請求對其餘形成影響,當其中一個請求失敗,其餘請求的數據仍然按照順序渲染。

其實很簡單:讓全部的Promise不管如何都是fulfilled就行了。

if (req.status == 200) {
        // 以響應文本爲結果,完成此 Promise
        resolve(req.response);
      }
      else {
        //reject(Error(req.statusText));resolve(‘loading error:’ + req.statusText);
      }

方法有些猥瑣,可是仍是實用的,渲染了其餘數據,又給了用戶錯誤信息提示。

某些狀況下,這種方法用來避免Promise.all的這樣的問題仍是不錯的選擇。

3.4 並行仍是順序執行?

3.1順序執行效率低,3.2並行執行可是須要全部的數據加載完成才能開始渲染。有沒有一種魚和熊掌兼得的方法呢?

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());}).then(function() {
  addTextToPage("All done");}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  document.querySelector('.spinner').style.display = 'none';});

上述代碼的效果圖(請另預覽git圖片)
圖片描述

從圖看出,ajax併發執行,而且是按照順序渲染的,對比3.3,第一章很快就獲得更快的渲染。改善了用戶體驗。

reduce(function(sequence, chapterPromise) {
      return sequence.then(function() {
          return chapterPromise;
      }).then(function(chapter) {
          addHtmlToPage(chapter.html);
      });}, Promise.resolve());

(1)並行加載的實現:

story.chapterUrls.map(getJSON)把全部章節url轉換成Promise實例對象。Promise實例對象被異步執行,調用function(resolve,  reject) {},也就是全部的ajax請求併發執行。

(2)順序渲染數據的實現:

併發發送了請求,可是返回結果的順序不是固定順序的。因此只能從順序渲染數據下手,必須按照章節的順序渲染數據。爲了實現這個,咱們大概想到了3-2中,順序加載的作法,使得前一個Promise實例控制後一個Promise實例的進行。

3-3和3-4都是併發加載,可是3-4能夠更快的顯示第一章的數據。3-3須要把所有的數據加載完成纔開始按照順序渲染。而3-4等到加載完第一 章開始就渲染。這樣的技巧,能夠提升用戶的體驗,用戶能夠很快的看到第一章的內容,然後面的內容則悄悄的加載,這樣的用戶體驗無疑是更好的。

4.總結

本文介紹了Promise的做用和Promise主要API的使用和特色,而後展現了三個使用Promise的例子,異步工做流的處理。雖然ES7 即將出現Async Await這樣的異步編程利器,可是掌握Promise也是有所用處的,實際上,Promise能夠和Generator和Async配合使用,使得代碼 更優雅。

相關文章
相關標籤/搜索