ES6的Promise物件的使用

曲小強發表於2019-09-03

Promise物件,ES6新增的一個全新特性,雖然它出現很久了,but我相信,有很多的小夥伴還是沒有怎麼用過,今天讓我們來好好的學習一下它。

ES6的Promise物件的使用

Promise 的誕生

1、Promise 的含義

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。

在日常開發中,經常需要用到ajax請求資料,拿到資料後,再進行一些處理,完成一些複雜的互動效果。

假如你需要用ajax進行多次請求,而且,每次請求都依賴上一次請求返回的資料來作為引數,然後繼續發出請求,你寫成這樣,場景還原

$.ajax({
    success:function(res1){
        $.ajax({
            success:function(res2){
                $.ajax({
                    success:function(res3){
                    }
                });
            }    
        });
    }
});
複製程式碼

可能會有更多的巢狀,如此一層一層的執行,無疑是消耗了更多的等待時間,而且多個請求之間如果有先後關係的話,就會出現回撥地獄,ES6想了辦法整治這種情況,這時候Promise 誕生了。

所以我們知道了 Promise 是非同步程式設計的一種解決方案,比傳統的回撥函式和事件更合理和強大。

Promise物件有以下兩個特點:

  • 物件的狀態不受外界影響。Promise物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是 Promise 這個名字的由來。
  • 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise物件新增回撥函式,也會立即得到這個結果。

有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥地獄

有好就有壞,Promise也有一些缺點。1、首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。2、其次,如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。3、當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

Promise 的基本用法

2、Promise 的基本用法

接下來,我們就看看它的基本用法:

const promise = new Promise(function(resolve, reject) {
  
});
複製程式碼

Promise 物件是全域性物件,你也可以理解為一個類,建立Promise例項的時候,要有那個new關鍵字。Promise建構函式接受一個函式作為引數,其中有兩個引數:resolvereject,兩個函式均為方法。resolve方法用於處理非同步操作成功後業務(即從 pending 變為 resolved)。reject方法用於操作非同步操作失敗後的業務(即從 pending 變為 rejected)。在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

Promise 的三種狀態

上面也提到了,Promise 物件有三種狀態:

  • 1、pending:剛剛建立一個 Promise 例項的時候,表示初始狀態;
  • 2、fulfilledresolve 方法呼叫的時候,表示操作成功;
  • 3、rejectedreject 方法呼叫的時候,表示操作失敗;
    ES6的Promise物件的使用
    下面程式碼創造了一個Promise例項。
const promise = new Promise(function(resolve, reject) {
  //例項化後狀態:pending

  if (/* 非同步操作成功 */){
    resolve(value);
    // resolve方法呼叫,狀態為:fulfilled
  } else {
    reject(error);
    // reject方法呼叫,狀態為:rejected
  }
});
複製程式碼

初始化例項後,物件的狀態就變成了pending;當resolve方法被呼叫的時候,狀態就會由pending變成了成功fulfilled;當reject方法被呼叫的時候,狀態就會由pending變成失敗rejected

Promise例項生成以後,可以用then()方法分別指定resolved狀態和rejected狀態的回撥函式,用於繫結處理操作後的處理程式。

ES6的Promise物件的使用
看以下操作:

promise.then(function(value) {
  // 操作成功的處理程式
}, function(error) {
  // 操作失敗的處理程式
});
複製程式碼

then()方法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise物件的狀態變為resolved時呼叫,第二個回撥函式是Promise物件的狀態變為rejected時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受Promise物件傳出的值作為引數。

說簡單點就是引數是兩個函式,第一個用於處理操作成功後的業務,第二個用於處理操作異常後的業務

ES6的Promise物件的使用

舉一個Promise物件的簡單例子。

ES6的Promise物件的使用

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

timeout(100).then((value) => {
  console.log(value); // hello world
});
複製程式碼

上面程式碼中,timeout()方法返回一個Promise例項,表示一段時間以後才會發生的結果。過了指定的時間(ms引數)以後,Promise例項的狀態變為resolved,就會觸發then()方法繫結的回撥函式。

ES6的Promise物件的使用
Promise 新建後就會立即執行。

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

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

console.log('Hi!');

// Promise
// Hi!
// resolved
複製程式碼

Promise 新建後立即執行,所以首先輸出的是Promise。然後,then()方法指定的回撥函式,將在當前指令碼所有同步任務執行完才會執行,所以resolved最後輸出。

catach( ) 方法

對於操作異常的程式,Promise專門提供了一個例項方法來處理:catch()方法。

catch() 只接受一個引數,用於處理操作異常後的業務。

getJSON('/posts.json').then(function(posts) {
  // 處理成功
}).catch(function(error) {
  // 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
  console.log('發生錯誤!', error);
});
複製程式碼

Promise使用鏈式呼叫,是因為then方法catch方法呼叫後,都會返回promise物件

上面程式碼中,getJSON()方法返回一個 Promise 物件,如果該物件狀態變為resolved,則會呼叫then()方法指定的回撥函式;如果非同步操作丟擲錯誤,狀態就會變為rejected,就會呼叫catch()方法指定的回撥函式,處理這個錯誤。另外,then()方法指定的回撥函式,如果執行中丟擲錯誤,也會被catch()方法捕獲。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test
複製程式碼

上面程式碼中,promise丟擲一個錯誤,就被catch方法指定的回撥函式捕獲。

ES6的Promise物件的使用

如果 Promise 狀態已經變成resolved,再丟擲錯誤是無效的。

const 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
複製程式碼

上面程式碼中,Promiseresolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了

因為 Promise.prototype.thenPromise.prototype.catch 方法返回promise 物件, 所以它們可以被鏈式呼叫

ES6的Promise物件的使用

注意: 如果一個promise物件處在fulfilledrejected狀態而不是pending狀態,那麼它也可以被稱為 settled 狀態。你可能也會聽到一個術語resolved ,它表示promise物件處於settled狀態。

舉了這麼多栗子,要是沒看懂,讓我們串一串吧,梳理一下上面提到的。(認真的看註釋)

// 首先用 new 關鍵字建立一個 `Promise` 例項
const promise = new Promise(function(resolve, reject){
    // 假設 state 的值為 true
    let state = true;
    if( state ){
        // 呼叫操作成功方法
        resolve('操作成功');
        //狀態:從pending 到 fulfilled
    }else{
        // 呼叫操作異常方法
        reject('操作失敗');
        //狀態:從pending 到 rejected
    }
});
promise.then(function (res) {
    //操作成功的處理程式
    console.log(res)
}).catch(function (error) {
    //操作失敗的處理程式
    console.log(error)
})
// 控制檯輸出:操作成功
複製程式碼

上面示例介紹了從 建立例項,狀態轉換,then方法和catch方法的使用

如果多個操作之間層層依賴,我們用Promise又是怎麼處理的呢?

const promise = new Promise(function(resolve, reject){
  if( true ){
    // 呼叫操作成功方法
    resolve('操作成功');
    //狀態:從pending 到 fulfilled
  }else{
    // 呼叫操作異常方法
    reject('操作失敗');
    //狀態:從pending 到 rejected
  }
});
promise.then(a)
       .then(b)
       .then(c)
       .catch(requestError);

function a() {
    console.log('請求A成功');
    return '請求B,下一個是誰';
}
function b(res) {
    console.log('上一步的結果:'+ res);
    console.log('請求B成功');
    return '請求C,下一個是誰';
}
function c(res) {
    console.log('上一步的結果:'+ res);
    console.log('請求C成功');
}
function requestError() {
    console.log('請求失敗');
}
複製程式碼

如圖所示:

ES6的Promise物件的使用

上面的程式碼,先是建立一個例項,還宣告瞭4個函式,其中三個是分別代表著請求A,請求B,請求C;有了then方法,三個請求操作再也不用層層巢狀了。我們使用then方法,按照呼叫順序,很直觀地完成了三個操作的繫結,並且,如果請求B依賴於請求A的結果,那麼,可以在請求A的程式用使用return語句把需要的資料作為引數,傳遞給下一個請求,示例中我們就是使用return實現傳遞引數給下一步操作的。

為了更直觀的看到示例所展示的情況,在下做了一張圖:

ES6的Promise物件的使用

再舉個Promise 中微任務順序的栗子1:

var p = new Promise( (resolve, reject) => {
    setTimeout( () => {
        console.log('1');
    }, 3000);
    resolve(1);
}).then( () => {  // 描述:.then() 1-1
    Promise.resolve().then( () => { // 描述:.then() 2-1
        Promise.resolve().then( () => { // 描述:.then() 3-1
            console.log('2');
        })
    })
}).then( () => { // 描述:.then() 1-2
    console.log('3');
})
// 3
// 2
// 1    (3秒之後執行列印的值)
複製程式碼

如上示例解釋:

  • 1.先執行new Promise第一層的程式碼,遇到setTimeout,將其推入巨集任務佇列中(此時未執行,排在當前script程式碼塊的巨集任務之後執行),然後遇到了resolve,執行Promise後面的程式碼。
  • 2.遇到.then 1-1,推入微任務佇列裡(只是推入,並未執行,所以.then 1-2的執行時機還沒有到),這個時候發現沒有其他的操作需要處理(比如推其他的微任務到佇列裡),那麼就執行當前微任務佇列裡的函式,也就是執行.then 1-1的回撥函式。
  • 3.執行.then 1-1的回撥函式的時候,發現了裡面有一個完成態的Promise物件,不用管繼續走,遇到了.then 2-1,推入微任務佇列(只是推入,並未執行),此時.then 1-1回撥執行完畢(沒有return值,相當於return了一個undefined),然後Promise得以繼續往下執行,遇到了.then 1-2,繼續推入微任務佇列(依然沒執行),這時發現沒有其他操作,開始順位執行當前微任務佇列裡的函式(此時微任務佇列裡存放了.then 2-1.then 1-2的回撥函式),執行.then 2-1的回撥函式時,又遇到了一個完成態的Promise,不用管繼續走,遇到了.then 3-1,將其推入微任務佇列(未執行),然後執行.then1-2的回撥,列印 3 ,此時已經沒有了其他的操作,所以繼續執行微任務佇列裡剩餘的函式,即.then 3-1的回撥函式,列印 2
  • 4.至此,微任務佇列已經執行完畢,開始執行巨集任務佇列中的下一個巨集任務,列印 1

ES6的Promise物件的使用

再舉個Promise 中微任務順序的栗子2:

var p2 = new Promise( (resolve, reject) => {
    setTimeout( () => {
      console.log('1');  
    }, 3000)
    resolve(1);
}).then( () => { // 描述:.then() 1-1
    Promise.resolve().then( () => { // 描述:.then() 2-1
        console.log('2');
    }).then( () => { // 描述:.then() 1-2
        console.log('3');
    })
})
// 2
// 3
// 1   (3秒之後執行列印的值)
複製程式碼

如上示例解釋:

  • 1.如同栗子1。
  • 2.如同栗子2。
  • 3.執行.then 1-1的回撥函式的時候,發現了裡面有一個完成態的Promise物件,不用管繼續走,遇到了.then 2-1,推入微任務佇列(只是推入,並未執行),此時.then 1-1回撥執行完畢(沒有return值,相當於return了一個undefined),然後Promise得以繼續往下執行,遇到了.then 1-2,繼續推入微任務佇列(依然沒執行),這時發現沒有其他操作,開始順位執行當前微任務佇列裡的函式(此時微任務佇列裡存放了.then 2-1.then 1-2的回撥函式),執行.then 2-1的回撥函式,列印 2,然後執行.then1-2的回撥,列印 3
  • 4.至此,微任務佇列已經執行完畢,開始執行巨集任務佇列中的下一個巨集任務,列印 1

ES6的Promise物件的使用

ES6的Promise物件的使用

除了提供了例項方法以外,Promise還提供了一些類方法,也就是不用建立例項,也可以呼叫的方法,下面列舉幾個栗子:

Promise.all( ) 方法

Promise.all(iterable) 方法返回一個 Promise 例項,此例項在 iterable 引數內所有的 promise 都“完成(resolved)”或引數中不包含 promise 時回撥完成(resolve);如果引數中 promise 有一個失敗(rejected),此例項回撥失敗(reject),失敗原因的是第一個失敗 promise 的結果。

var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 3000, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(values => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]
複製程式碼

解析:

因為例項promise3還沒有進入成功fulfilled狀態;等到了3000毫秒以後,例項promise3也進入了成功fulfilled狀態,Promise.all( )才會進入then方法,然後在控制檯輸出:[3, 42, "foo"]

應用場景:我們執行某個操作,這個操作需要得到需要多個介面請求回來的資料來支援,但是這些介面請求之前互不依賴,不需要層層巢狀。這種情況下就適合使用Promise.all( )方法,因為它會得到所有介面都請求成功了,才會進行操作。

注意:如果傳入的 promise 中有一個失敗(rejected),Promise.all非同步地將失敗的那個結果給失敗狀態的回撥函式,而不管其它 promise 是否完成。

Promise.finally( ) 方法

finally方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。這避免了同樣的語句需要在then()catch()中各寫一次的情況。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
複製程式碼

上面程式碼中,不管promise最後的狀態,在執行完thencatch指定的回撥函式以後,都會執行finally方法指定的回撥函式。

下面是一個例子,伺服器使用 Promise 處理請求,然後使用finally方法關掉伺服器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);
複製程式碼

如果你想在 promise 執行完畢後無論其結果怎樣都做一些處理或清理時,finally() 方法可能是有用的。

Promise.race( ) 方法

Promise.race()方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.race([p1, p2, p3]);
複製程式碼

上面程式碼中,只要p1p2p3之中有一個例項率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。

Promise.race方法的引數與Promise.all方法一樣,如果不是 Promise 例項,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。

const p = Promise.race([
  fetch('index.php'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);
複製程式碼

上面程式碼中,如果 5 秒之內fetch方法無法返回結果,變數p的狀態就會變為rejected,從而觸發catch方法指定的回撥函式。


let promise1 = new Promise(function(resolve){
    setTimeout(function () {
        resolve('promise1例項成功');
    },4000);
});
let promise2 = new Promise(function(resolve,reject){
    setTimeout(function () {
        reject('promise2例項失敗');
    },2000);
});
Promise.race([promise1, promise2]).then(function(result){
    console.log(result);
}).catch(function(error){
    console.log(error);
});
// expected output: promise2例項失敗
複製程式碼

由於promise2例項中2000毫秒之後就執行reject方法,早於例項promise15000毫秒,所以最後輸出的是:promise2例項失敗。

Promise.resolve( ) 方法

有時需要將現有物件轉為 Promise 物件,Promise.resolve方法就起到這個作用。

const jsPromise = Promise.resolve($.ajax('/whatever.json'));
複製程式碼

上面程式碼將 jQuery 生成的deferred物件,轉為一個新的 Promise 物件。

Promise.resolve等價於下面的寫法。

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製程式碼

Promise.resolve(value)方法返回一個以給定值解析後的Promise 物件。如果該值為promise,返回這個promise;如果這個值是thenable(即帶有"then" 方法)),返回的promise會“跟隨”這個thenable的物件,採用它的最終狀態;否則返回的promise將以此值完成。此函式將類promise物件的多層巢狀展平。

警告不要在解析為自身的thenable 上呼叫Promise.resolve。這將導致無限遞迴,因為它試圖展平無限巢狀的promise。一個例子是將它與Angular中的非同步管道一起使用。

使用靜態Promise.resolve方法

Promise.resolve("Success").then(function(value) {
  console.log(value); // "Success"
}, function(value) {
  // 不會被呼叫
});
複製程式碼

resolve一個陣列

var p = Promise.resolve([1,2,3]);
p.then(function(v) {
  console.log(v[0]); // 1
});
複製程式碼

Resolve另一個promise

var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
  console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

/*
*  列印順序如下,這裡有一個同步非同步先後執行的區別
*  original === cast ? true
*  value: 33
*/
複製程式碼
Promise.reject( ) 方法

Promise.reject(reason)方法返回一個帶有拒絕原因reason引數的Promise物件。

const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s)
});
// 出錯了
複製程式碼

上面程式碼生成一個 Promise 物件的例項 p,狀態為rejected,回撥函式會立即執行。

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

const thenable = {
  then(resolve, reject) {
    reject('出錯了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
複製程式碼

ES6的Promise物件的使用
舉一個promise 應用的栗子:

我們可以將圖片的載入寫成一個Promise,一旦載入完成,Promise的狀態就發生變化。

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
複製程式碼

Generator 函式與 Promise 的結合

使用 Generator函式 管理流程,遇到非同步操作的時候,通常返回一個Promise物件。

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const 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(error));
    });
  }

  go(it.next());
}

run(g);
複製程式碼

上面程式碼的 Generator 函式g之中,有一個非同步操作getFoo,它返回的就是一個Promise物件。函式run用來處理這個Promise物件,並呼叫下一個next方法。

我目前所寫的專案大多數都是Generator函式 與 Promise 的結合。

這個篇幅有點長,如果你沒有收藏可以收藏,以後慢慢的觀看。

以下是我的公眾號,關注我,會讓你有意想不到的收穫~

ES6的Promise物件的使用

相關文章