【ES6系列】Promise

JS 同步與異步

Javascript語言的執行環境是"單線程"(single thread)。

所謂"單線程",就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。html

這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。es6

爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。ajax

"同步模式"就是上一段的模式,後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的;"異步模式"則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。編程

"異步模式"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。json

常見異步處理

在咱們平常的開發中,特別是在ES6以前咱們經常使用的解決異步編程的方式主要有如下幾種:數組

  • 回調函數
  • 事件監聽
  • Deferred對象

下面咱們分別來看一下,假定咱們有兩個函數f1f2,後者等待前者的執行結果再執行。promise

回調函數

function f1(callback) {
  setTimeout(function() {
    // f1的任務代碼
    callback();
  }, 1000);
}

f1(f2)

採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。瀏覽器

回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,並且每一個任務只能指定一個回調函數。服務器

事件監聽

f1.on("done", f2);

function f1(callback) {
  setTimeout(function() {
    // f1的任務代碼
    f1.trigger("done")
  }, 1000);
}

這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合"(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。併發

Deferred對象

Jquery中提供了Deferred對象的概念,Deferred對象是一個延遲對象,意思是函數延遲到某個點纔開始執行,改變執行狀態的方法有兩個(成功:resolve和失敗:reject),分別對應兩種執行回調(成功回調函數:done和失敗回調函數fail)

這也是我在ES6以前最經常使用的自定義異步方法所使用的方法。

function f1(callback) {
  var $dfd = $.Deferred();
  setTineout(function() {
      if(任務完成) {
        dfd.resolve({
            info: "完成"
        })
      } else {
        dfd.reject({
          info: "失敗"
        })
      }
  }, 1000);
  return dfd.promise();
}

f1().then(f2);

能夠發現,經過上面的方法能夠實現鏈式的異步調用,並且執行過程更加的清晰,每一個處理之間也達到了「去耦合」的效果。

Promise對象概念

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。其實上面的Deferred對象的使用就已經相似於這種處理了。

Promise對象有如下兩個特色:

  • (1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變。

圖片描述

  • (2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。

有了Promise對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易。

Promise也有一些缺點。首先,沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。其次,若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。第三,當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。

Promise對象的用法

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

let ajax=function(){
  return new Promise(function(resolve,reject){
    setTimeout(function () {
      resolve()
    }, 1000);
  })
};

ajax().then(function(){
  console.log('promise','resolve');
})

resolve函數的做用是,將Promise對象的狀態從「未完成」變爲「成功」(即從 pending 變爲 resolved),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去;reject函數的做用是,將Promise對象的狀態從「未完成」變爲「失敗」(即從 pending 變爲 rejected),在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。

Promise實例生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。

ajax().then(function(){
  console.log('promise','resolve');
}, function(){
  console.log('promise', 'reject')
})

Promise 新建後就會當即執行。

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

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

ES6 Promise對象與jQuery Deferred對象

先看例子:

【ES6 Promise】
var pro=new Promise(function(resolve){
    resolve(1);
});

//已經resolve了,再設置then回調。
pro.then(function(v){
    alert(v);    //1
});
alert(2);

//仍是會已異步方式,發生回調。
//先alert(2)再alert(v);

//並且,之後何時註冊then,都會異步調用。

【Jquery Deferred】
var defer=$.Deferred();
defer.resolve(1);

//deferred對象已經resolve了
defer.done(function(v){
    alert(v);    //不會執行
});
alert(2);

//只執行alert(2);
//若是須要執行done,就要把註冊done回調放到defer.resolve()以前。

另外,ES6的Promise沒有resolve,reject,notify方法,不能進行狀態更改,
只能註冊回調。

Promise對象例子

下面是一個用Promise對象實現的 常見Ajax 操做的例子:

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出錯了', error);
});

Promise.prototype.then()

Promise.prototype.then()方法返回的是一個新的Promise對象,所以能夠採用鏈式寫法,即一個then後面再調用另外一個then。若是前一個回調函數返回的是一個Promise對象,此時後一個回調函數會等待第一個Promise對象有告終果,纔會進一步調用。

圖片描述

關於上圖中黃圈1的對value的處理是Promise裏面較爲複雜的一個地方,value的處理分爲兩種狀況:Promise對象、非Promise對象。

當value 不是Promise類型時,直接將value做爲第二個Promise的resolve的參數值便可;當爲Promise類型時,promise2的狀態、參數徹底由value決定,能夠認爲promsie2徹底是value的傀儡,promise2僅僅是鏈接不一樣異步的橋樑。

圖片描述

Promise.prototype.then = function(onFulfilled, onRejected) {
  return new Promise(function(resolve, reject) { //此處的Promise標註爲promise2
    handle({
      onFulfilled: onFulfilled,
      onRejected: onRejected,
      resolve: resolve,
      reject: reject
    })
  });
}

function handle(deferred) {
  var handleFn;
  if (state === 'fulfilled') {
    handleFn = deferred.onFulfilled;
  } else if (state === 'rejected') {
    handleFn = deferred.onRejected;
  }
  var ret = handleFn(value);
  deferred.resolve(ret); //注意,此時的resolve是promise2的resolve
}

function resolve(val) {
  if (val && typeof val.then === 'function') {
    val.then(resolve); // if val爲promise對象或類promise對象時,promise2的狀態徹底由val決定
    return;
  }
  if (callback) { // callback爲指定的回調函數
    callback(val);
  }
}

關於then的使用

let ajax = function() {
  return new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve()
      }, 1000);
  })
};

ajax()
  .then(function() {
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve()
      }, 2000);
    });
  })
  .then(function() {
    console.log('all done');
  })

Promise.prototype.catch()

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

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

使用Promise對象的catch方法能夠捕獲異步調用鏈中callback的異常,Promise對象的catch方法返回的也是一個Promise對象,所以,在catch方法後還能夠繼續寫異步調用方法。這是一個很是強大的能力。

const 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;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

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

通常來講,不要在then方法裏面定義 Reject 狀態的回調函數(即then的第二個參數),老是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

Promise.prototype.finally()

finally方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。該方法是 ES2018 引入標準的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally方法的回調函數不接受任何參數,這意味着沒有辦法知道,前面的 Promise 狀態究竟是fulfilled仍是rejected。這代表,finally方法裏面的操做,應該是與狀態無關的,不依賴於 Promise 的執行結果。

Promise異步併發

若是幾個異步調用有關聯,但它們不是順序式的,是能夠同時進行的,咱們很直觀地會但願它們可以併發執行(這裏要注意區分「併發」和「並行」的概念,不要搞混)。

Promise.all()

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

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

Promise.all方法接受一個數組做爲參數,p一、p二、p3都是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的回調函數。

一個具體的例子:

// 生成一個Promise對象的數組
const 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.race()也是將多個Promise實例包裝成一個新的Promise實例:

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

上述代碼中,p一、p二、p3只要有一個實例率先改變狀態,p的狀態就會跟着改變,那個率先改變的Promise實例的返回值,就傳遞給p的返回值。若是Promise.all方法和Promise.race方法的參數不是Promise實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

上面代碼中,若是 5 秒以內fetch方法沒法返回結果,變量p的狀態就會變爲rejected,從而觸發catch方法指定的回調函數。

Promise.resolve()

有時候需將現有對象轉換成Promise對象,可使用Promise.resolve()。

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

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

var p = Promise.resolve('Hello');

p.then(function (s){
    console.log(s)
});
// Hello

Promise.reject()

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

var p = Promise.reject('Wrong');

p.then(null, function (s){
    console.log(s)
});
// Wrong

應用舉例

加載圖片

let preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

小結

在JS開發過程當中,異步編程是很是常見和很是重要的處理方式,而ES6中將Promise對象列爲標準,方便了咱們的開發使用,使得咱們再也不須要依賴高耦合的方式或者第三方庫來完成這一目標。

本篇參照了一些相關文章的內容,具體可見參考部分。


相關參考:

相關文章
相關標籤/搜索