Promise 對(duì)象

目錄:

  1. Promise 的含義
  2. 基本用法
  3. Promise.prototype.then()
  4. Promise.prototype.catch()
  5. Promise.prototye.finally()
  6. Promise.all()
  7. Promise.race()
  8. Promise.resolve()
  9. Promise.reject()
  10. 應(yīng)用
  11. Promise.try()

1. Promise 的含義

Promise 是異步編程的一種解決方案,比傳統(tǒng)方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它由社區(qū)最早提出和實(shí)現(xiàn),ES6 將其寫入了語(yǔ)言標(biāo)準(zhǔn),統(tǒng)一了用法,原生提供了 Promise 對(duì)象。

所謂 Promise,簡(jiǎn)單說(shuō)就是一個(gè)容器,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語(yǔ)法上說(shuō),Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。

Promise 對(duì)象有以下兩個(gè)特點(diǎn)。

(1)對(duì)象的狀態(tài)不受外界影響。Promise 對(duì)象代表一個(gè)異步操作,有三種狀態(tài):pending(進(jìn)行中)、fulfilled (已成功)和 rejected (已失敗)。只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無(wú)法改變這個(gè)狀態(tài)。這也是Promise 這個(gè)名字的由來(lái),它的英文意思是"承諾",表示其他手段無(wú)法改變。

(2)一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。Promise 對(duì)象的狀態(tài)改變,只有兩種可能:從 pending 變?yōu)?fulfilled 和從 pending 變?yōu)?rejected。只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會(huì)再變了,會(huì)一直保持這個(gè)結(jié)果,這時(shí)就稱為 resolved(已定型)。如果改變已經(jīng)發(fā)生了,你再對(duì) Promise 對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過了它,再去監(jiān)聽,是得不到結(jié)果的。

注意,為了行文方便,本章后面的 resolved 統(tǒng)一只指 fulfilled 狀態(tài),不包含 rejected 狀態(tài)。

有了 Promise 對(duì)象,就可以將異步操作以同步操作的流程表達(dá)出來(lái),避免了層層嵌套的回調(diào)函數(shù)。此外, Promise 對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易。

Promise 也有一些缺點(diǎn)。首先,無(wú)法取消 Promise,一旦新建了它就會(huì)立即執(zhí)行,無(wú)法中途取消,其次,如果不設(shè)置回調(diào)函數(shù),Promise 內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。第三,當(dāng)處于 pending 狀態(tài)時(shí),無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開始還是即將完成)。

如果某些事件不斷地反復(fù)發(fā)生,一般來(lái)說(shuō),使用 Steam 模式是比部署 Promise 更好的選擇。


2. 基本用法

ES6 規(guī)定,Promise 對(duì)象是一個(gè)構(gòu)造函數(shù),用來(lái)生成 promise 實(shí)例。

下面代碼創(chuàng)造了一個(gè) Promise 實(shí)例。

const promise = new Promise(function(resolve, reject) {
    // ... some code

    if (/* 異步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
});

Promise 構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是 resolvereject。它們是兩個(gè)函數(shù),由 JavaScript 引擎提供,不用自己部署。

resolve 函數(shù)的作用是,將 Promise 對(duì)象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?resolved),在異步操作成功時(shí)調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去; reject 函數(shù)的作用是,將 Promise 對(duì)象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected),在異步操作失敗時(shí)調(diào)用,并將異步操作報(bào)出的錯(cuò)誤,作為參數(shù)傳遞出去。

Promise 實(shí)例生成后,可以用 then 方法分別指定 resolved 狀態(tài)和 rejected 狀態(tài)的回調(diào)函數(shù)。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then 方法可以接受兩個(gè)回調(diào)函數(shù)作為參數(shù)。第一個(gè)回調(diào)函數(shù)是 Promise 對(duì)象的狀態(tài)變?yōu)?resolved 時(shí)調(diào)用,第二個(gè)回調(diào)函數(shù)是 Promise 對(duì)象的狀態(tài)變?yōu)?rejected 時(shí)調(diào)用。其中,第二個(gè)函數(shù)是可選的,不一定要提供。這兩個(gè)函數(shù)都接受 Promise 對(duì)象傳出的值作為參數(shù)。

下面是一個(gè) Promise 對(duì)象的簡(jiǎn)單例子。

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

timeout(100).then((value) => {
  console.log(value);
});

上面的代碼中,timeout 方法返回一個(gè) Promise實(shí)例,表示一段時(shí)間以后才會(huì)發(fā)生的結(jié)果。過了指定的時(shí)間(ms 參數(shù))以后,Promise 實(shí)例的狀態(tài)為 resolved,就會(huì)觸發(fā) then 方法綁定的回調(diào)函數(shù)。

Promise 新建后就會(huì)立即執(zhí)行。

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

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

console.log('Hi!');
// Promise
// Hi!
// resolved

上面代碼中,Promise 新建后立即執(zhí)行,所以首先輸出的是 Promise 。然后,then 方法指定的回調(diào)函數(shù),將在當(dāng)前腳本所有同步任務(wù)執(zhí)行完才會(huì)執(zhí)行,所以 resolved 最后輸出。

下面是異步加載圖片的例子。

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at' + url));
    };

    image.src = url;
  });
};

上面代碼中,使用 Promise 包裝了一個(gè)圖片加載的異步操作。如果加載成功,就調(diào)用 resolve 方法,否則就調(diào)用 reject 方法。

下面是一個(gè)用 Promise 方法事項(xiàng)的 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.log('出錯(cuò)了', error);
});

上面代碼中,getJSON 是對(duì) XMLHttpRequest 對(duì)象的封裝,用于發(fā)出一個(gè)針對(duì) JSON 數(shù)據(jù)的 HTTP 請(qǐng)求,并且返回一個(gè) Promise 對(duì)象。需要注意的是,在 getJSON 內(nèi)部,resolve 函數(shù)和 reject函數(shù)調(diào)用時(shí),都帶有參數(shù)。

如果調(diào)用 resolve 函數(shù)和 reject 函數(shù)時(shí)都帶有參數(shù),那么它們的參數(shù)會(huì)被傳遞給回調(diào)函數(shù)。reject 函數(shù)的參數(shù)通常是 Error 對(duì)象的實(shí)例,表示拋出的錯(cuò)誤; resolve 函數(shù)的參數(shù)除了正常的值以外,還可能是另一個(gè) Promise 實(shí)例,比如像下面這樣。

const p1 = new Promise(function(resolve, reject) {
  // ...
});

const p2 = new Promise(function(resolve, reject) {
  // ...
  resolve(p1);
});

上面代碼中,p1p2 都是 Promise 實(shí)例,但是 p2resolve 方法將 p1 作為參數(shù),即一個(gè)異步操作的結(jié)果是返回另一個(gè)異步操作。

注意,這時(shí) p1 的狀態(tài)就會(huì)傳遞給 p2,也就是說(shuō),p1 的狀態(tài)決定了 p2 的狀態(tài)。如果 p1 的狀態(tài)是 pending,那么 p2 的回調(diào)函數(shù)就會(huì)等待 p1 的狀態(tài)改變;如果 p1 的狀態(tài)已經(jīng)是 resolved 或者 rejected,那么 p2 的回調(diào)函數(shù)將會(huì)立即執(zhí)行。

const p1 = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000);
});

const p2 = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(p1), 1000);
});

p2.then(result => console.log(result));
  .catch(error => console.log(error));
// Error: fail

上面代碼中,p1 是一個(gè) Rromise,3秒之后變?yōu)?rejected。p2 的狀態(tài)在1秒之后改變,resolve 方法返回的是 p1。由于 p2 返回的是另一個(gè) Promise,導(dǎo)致 p2 自己的狀態(tài)無(wú)效了,由 p1 的狀態(tài)決定 p2 的狀態(tài)。所以,后面的 then 都變成針對(duì)后者(p1)。又過了2秒,p1 變?yōu)?rejected,導(dǎo)致觸發(fā) catch 方法指定的回調(diào)函數(shù)。

注意,調(diào)用 resolvereject 并不會(huì)終結(jié) Promise 的參數(shù)函數(shù)的執(zhí)行。

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代碼中,調(diào)用 resolve(1) 以后,后面的 console.log(2) 還是會(huì)執(zhí)行,并且會(huì)首先打印出來(lái)。這是因?yàn)榱⒓?resolved 的 Promise 是在本輪事件循環(huán)的末尾執(zhí)行,總是晚于本輪循環(huán)的同步任務(wù)。

一般來(lái)說(shuō),調(diào)用 resolvereject 以后,Promise 的使命就完成了,后繼操作應(yīng)該放到 then 方法里面,而不應(yīng)該直接寫在 resolvereject 的后面。所以,最好在它們前面加上 return 語(yǔ)句,這樣就不會(huì)有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的語(yǔ)句不會(huì)執(zhí)行
  console.log(2);
});

3. Promise.prototype.then()

Promise 實(shí)例具有 then 方法,也就是說(shuō), then 方法是定義在原型對(duì)象 Promise.prototype 上的。它的作用是為 Promise 實(shí)例添加狀態(tài)改變時(shí)的回調(diào)函數(shù)。前面說(shuō)過, then 方法的第一個(gè)參數(shù)是 resolved 狀態(tài)的回調(diào)函數(shù),第二個(gè)參數(shù)(可選)是 rejected 狀態(tài)的回調(diào)函數(shù)。

then 方法返回的是一個(gè)新的實(shí)例(注意,不是原來(lái)那個(gè) Promise 實(shí)例)。因此可以采用鏈?zhǔn)綄懛?,?then 方法后面再調(diào)用另一個(gè) then 方法。

getJSON('/posts.json')
  .then((json) => {
    return json.post;
  })
  .then((post) => {
    // ...
  });

上面的代碼使用 then 方法,依次指定了兩個(gè)回調(diào)函數(shù)。第一個(gè)回調(diào)函數(shù)完成以后,會(huì)將返回結(jié)果作為參數(shù),傳入第二個(gè)回調(diào)函數(shù)。

采用鏈?zhǔn)降?then ,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時(shí),前一個(gè)回調(diào)函數(shù),有可能返回的還是一個(gè) Promise 對(duì)象(即有異步操作),這時(shí)后一個(gè)回調(diào)函數(shù),就會(huì)等待該 Promise 對(duì)象的狀態(tài)發(fā)生變化,才會(huì)被調(diào)用。

getJSON("/posts.json")
  .then((post) => {
    return getJSON(post.commentURL);
  })
  .then(function funcA(comments) {
    console.log('resolved:', comments);
  }, function funcB(err) {
    console.log('rejected:', err);
  });

上面代碼中,第一個(gè) then 方法指定的回調(diào)函數(shù),返回的是另一個(gè) Promise 對(duì)象。這時(shí),第二個(gè) then 方法指定的回調(diào)函數(shù),就會(huì)等待這個(gè)新的 Promise 對(duì)象狀態(tài)發(fā)生變化。如果變?yōu)?resolved ,就調(diào)用 funcA ,如果狀態(tài)變?yōu)?rejected ,就調(diào)用 funcB。

如果采用箭頭函數(shù),上面的代碼可以寫的更簡(jiǎn)潔。

getJSON('/posts.json')
  .then(post => getJSON(post.commentURL))
  .then(
    comments => console.log('resolved:', comments),
    err => console.log('rejected:', err)
  );

4. Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null,rejection) 的別名,用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)。

getJSON('/posts.json')
  .then(posts => {
    // ...
  })
  .catch((err) => {
    // 處理 getJSON 和前一個(gè)回調(diào)函數(shù)運(yùn)行時(shí)發(fā)生的錯(cuò)誤
    console.log('發(fā)生錯(cuò)誤!', error);
  });

上面代碼中, getJSON 方法返回一個(gè) Promise 對(duì)象,如果該對(duì)象狀態(tài)變?yōu)?resolved ,則會(huì)調(diào)用 then 方法指定的回調(diào)函數(shù);如果異步函數(shù)拋出錯(cuò)誤,狀態(tài)就會(huì)變?yōu)?rejected ,就會(huì)調(diào)用 catch 方法指定的回調(diào)函數(shù),處理這個(gè)錯(cuò)誤。另外, then 方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯(cuò)誤,也會(huì)被 catch 方法捕獲。

p.then(val => console.log('fulfilled', val))
  .catch((err) => console.log('rejected', err));

// 等同于
p.then(val => console.log('fulfilled', val))
  .then(null, (err) => console.log('rejected', err));

下面是一個(gè)例子

const promise = new Promise((resolve, reject) => {
  throw new Error('test');
});

promise.catch((err) => {
  console.log(error);
})
// Error: test

上面代碼中, promise 拋出一個(gè)錯(cuò)誤,就被 catch 方法指定的回調(diào)函數(shù)捕獲。注意,上面的寫法與下面兩種寫法時(shí)等價(jià)的

// 寫法一
const promise = new Promise((resolve, reject) => {
  try {
    throw ner Error('test');
  } catch (e) {
    reject(e);
  }
});

promise.catch(error => console.log(error));

// 寫法二
const promise = new Promise((resolve, reject) => {
  reject(new Error('test'));
});

promise.catch(error => console.log(error));

比較上面兩種寫法,可以發(fā)現(xiàn) reject 方法的作用,等同于拋出錯(cuò)誤。

如果 Promise 狀態(tài)已經(jīng)變成 resolved ,再拋出錯(cuò)誤是無(wú)效的。

const promise = new Promise((resolve, reject) => {
  resolve('ok');
  throw new Error('test');
});

promise
  .then((value) => {console.log(value);})
  .catch((error) => {console.log(error);});
// ok

上面的代碼中,Promise 在 resolve 語(yǔ)句后面,再拋出錯(cuò)誤,不會(huì)被捕獲,等于沒有拋出。因?yàn)?Promise 的狀態(tài)一旦改變,就永久保持該狀態(tài),不會(huì)再變了。

Promise 對(duì)象的錯(cuò)誤具有“冒泡”性質(zhì),會(huì)一直向后傳遞,直到被捕獲為止。也就是說(shuō),錯(cuò)誤總是會(huì)被下一個(gè) catch 語(yǔ)句捕獲。

getJSON('/posts.json')
  .then((post) => {
    return getJSON(post.commentURL);
  })
  .then((comments) => {
    // some code
  })
  .catch((error) => {
    // 處理前面三個(gè) Promise 產(chǎn)生的錯(cuò)誤
  })

上面的代碼中,一共有三個(gè) Promise 對(duì)象:一個(gè)由 getJSON 產(chǎn)生,兩個(gè)由 then 產(chǎn)生。它們之中任何一個(gè)拋出的錯(cuò)誤,都會(huì)被最后一個(gè) catch 捕獲。

一般來(lái)說(shuō),不要在 then 方法里面定義 Reject 狀態(tài)的回調(diào)函數(shù)( then 的第二個(gè)參數(shù)),總是使用 catch 方法。

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

// good
promise
  .then(data => {
    // success
  })
  .catch((err) => {
    // error
  })

上面代碼中,第二中寫法要好于第一種寫法,理由是第二種可以捕獲前面 then 方法執(zhí)行中的錯(cuò)誤,也更接近同步的寫法( try/catch )。因此,建議總是使用 catch 方法,而不使用 then 方法的第二個(gè)參數(shù)。

跟傳統(tǒng)的 try/catch 代碼塊不同的是,如果沒有使用 catch 方法指定錯(cuò)誤處理的回調(diào)函數(shù),Promise 對(duì)象拋出的錯(cuò)誤不會(huì)傳遞到外層代碼,即不會(huì)有任何反應(yīng)。

const someAsyncThing = function() {
  return new Promise((resolve, reject) => {
    // 下面一行會(huì)報(bào)錯(cuò),因?yàn)?x 沒有聲明
    resolve(x + 2);
  });
};
someAsyncThing()
  .then(() => console.log('everything is great'));

setTimeout(() => {
  console.log(123);
}, 2000);
// uncaught (in promise) ReferenceError: x is not defined
// 123

上面代碼中, someAsyncThing 函數(shù)產(chǎn)生的 Promise 對(duì)象,內(nèi)部有語(yǔ)法錯(cuò)誤。瀏覽器運(yùn)行到這一行,會(huì)打印出錯(cuò)誤提示 ReferenceError: x is not defined ,但是不會(huì)退出進(jìn)程、終止腳本執(zhí)行,2 秒之后還是會(huì)輸出 123 。這就是說(shuō),Promise 內(nèi)部的錯(cuò)誤不會(huì)影響到 Promise 外部的代碼,通俗的說(shuō)法是“Promise 會(huì)吃掉錯(cuò)誤”。

這個(gè)腳本放在服務(wù)器執(zhí)行,退出碼就是 0 (即表示執(zhí)行成功)。不過,Node 有一個(gè) unhandleRejection 事件,專門監(jiān)聽未捕獲的 reject 錯(cuò)誤,上面的腳本會(huì)觸發(fā)這個(gè)事件的監(jiān)聽函數(shù),可以在監(jiān)聽函數(shù)里面拋出錯(cuò)誤。

process.on('unhandleRejection', (err, p) => {
  throw err;
});

上面代碼中, unhandleRejection 事件的監(jiān)聽函數(shù)有兩個(gè)參數(shù),第一個(gè)是錯(cuò)誤對(duì)象,第二個(gè)是報(bào)錯(cuò)的 Promise 實(shí)例,它可以用來(lái)了解發(fā)生錯(cuò)誤的環(huán)境信息。

注意,Node 有計(jì)劃在未來(lái)廢除 unhandleRejection 事件。如果 Promise 內(nèi)部有未捕獲的錯(cuò)誤,會(huì)直接終止進(jìn)程,并且進(jìn)程的退出碼不為 0。

再看下面的例子。

const promise = new Promise((resolve, reject) => {
  resolve('ok');
  setTimeout(function() {
    throw new Error('test')
  }, 0);
});

promise.then(value => console.log(value));
// ok
// uncaught Error: test

上面代碼中,Promise 指定在下一輪“事件循環(huán)”再拋出錯(cuò)誤。到了那個(gè)時(shí)候,Promise 的運(yùn)行已經(jīng)結(jié)束了,所以這個(gè)錯(cuò)誤是在 Promise 函數(shù)體外拋出的,會(huì)冒泡到最外層,成了未捕獲的錯(cuò)誤。

一般總是建議,Promise 對(duì)象后面要跟 catch 方法,這樣可以處理 Promise 內(nèi)部發(fā)生的錯(cuò)誤。 catch 方法返回的還是一個(gè) Promise 對(duì)象,因此后面還可以接著調(diào)用 then 方法。

const someAsyncThing = function() {
  return new Promise((resolve, reject) => {
    // 下面一行會(huì)報(bào)錯(cuò),因?yàn)?x 沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing
  .catch((err) => {
    console.log('oh on', error);
  })
  .then(() => {
    console.log('carry on');
  })
// oh no [ReferenceError: x is not defined]
// carry on

上面代碼運(yùn)行完 catch 方法指定的回調(diào)函數(shù),會(huì)接著運(yùn)行后面那個(gè) then 方法指定的回調(diào)函數(shù)。如果沒有報(bào)錯(cuò),則會(huì)跳過 catch 方法。

Promise
  .resolve()
  .catch((err) => {
    console.log('oh on', error);
  })
  .then(() => {
    console.log('carry on');
  })
// carry on

上面的代碼因?yàn)闆]有報(bào)錯(cuò),跳過了 catch 方法,直接執(zhí)行后面的 then 方法。此時(shí),要是 then 方法里面報(bào)錯(cuò),就與前面的 catch 無(wú)關(guān)了。

catch 方法之中,還能再拋出錯(cuò)誤。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會(huì)報(bào)錯(cuò),因?yàn)閤沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing()
  .then(function() {
  return someOtherAsyncThing();
  })
  .catch(function(error) {
    console.log('oh no', error);
    // 下面一行會(huì)報(bào)錯(cuò),因?yàn)?y 沒有聲明
    y + 2;
  })
  .then(function() {
    console.log('carry on');
  });
// oh no [ReferenceError: x is not defined]

上面的代碼中, catch 方法拋出一個(gè)錯(cuò)誤,因?yàn)楹竺鏇]有別的 catch 方法了,導(dǎo)致這個(gè)錯(cuò)誤不會(huì)被捕獲,也不會(huì)傳到外層。如果改寫一下,結(jié)果就不一樣了。

someAsyncThing()
  .then(function() {
    return someOtherAsyncThing();
  })
  .catch(function(error) {
    console.log('oh no', error);
    // 下面一行會(huì)報(bào)錯(cuò),因?yàn)閥沒有聲明
    y + 2;
  })
  .catch(function(error) {
    console.log('carry on', error);
  });
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

上面代碼中,第二個(gè) catch 方法用來(lái)捕獲,前一個(gè) catch 方法拋出的錯(cuò)誤。


5. Promise.prototype.finally()

finally 方法用于指定不管 Promise 對(duì)象最后的狀態(tài)如何,都會(huì)執(zhí)行的操作。 該方法是 ES2018 引入標(biāo)準(zhǔn)的。

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

上面代碼中,不管 promise 最后的狀態(tài),在執(zhí)行完 thencatch 指定的回調(diào)函數(shù)以后,都會(huì)執(zhí)行 finally 方法指定的回調(diào)函數(shù)。

下面是一個(gè)例子,服務(wù)器使用 Promise 處理請(qǐng)求,然后使用 finally 方法關(guān)掉服務(wù)器。

server.listen(port)
  .then(() => {
    // ...
  })
  .finally(server.stop);

finally 方法的回調(diào)函數(shù)不接受任何參數(shù),這意味著沒有辦法知道,前面的 Promise 狀態(tài)到底是 fulfilled 還是 rejected 。這表明, finally 方法里面的操作,應(yīng)該是與狀態(tài)無(wú)關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。

finally 本質(zhì)上是 then 方法的特例。

promise
 .finally(() => {
   // 語(yǔ)句
 });

// 等同于
promise
 .then(result => {
   // 語(yǔ)句
   return result;
 }, error => {
   // 語(yǔ)句
   return error;
 })

上面代碼中,如果不使用 finally 方法,同樣的語(yǔ)句需要為成功和失敗兩種情況各寫一次。有了 finally 方法,則只需要寫一次。

它的實(shí)現(xiàn)也很簡(jiǎn)單

Promise.prototype.finally = function(callbakc) {
  let P = this.constructor;
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

上面代碼中,不管前面的 Promise 是 fulfilled 還是 rejected ,都會(huì)執(zhí)行回調(diào)函數(shù) callback。

從上面的實(shí)現(xiàn)還可以看到, finally 方法總是會(huì)返回原來(lái)的值。

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})

6. Promise.all()

Promise.all 方法用于將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。

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

上面代碼中, Promise.all 方法接受一個(gè)數(shù)組作為參數(shù), p1p2 、 p3 都是 Promise 實(shí)例,如果不是,就會(huì)先調(diào)用下面講到的 Promise.resolve 方法,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例,再進(jìn)一步處理。( promise.all 方法的參數(shù)可以不是數(shù)組,但必須具有 Iterator 接口,且返回的每一個(gè)成員都是 Promise 實(shí)例。)

p 的狀態(tài)由 p1p2 、 p3 決定,分成兩種情況。

(1)只有 p1p2 、 p3 的狀態(tài)都變成 fulfilled ,此時(shí) p1 、 p2 、 p3 的返回值組成一個(gè)數(shù)組,傳遞給 p 的回調(diào)函數(shù)。

(2)只要 p1 、 p2 、 p3 之中有一個(gè)被 rejectedp 的狀態(tài)就變成 rejected ,此時(shí)第一個(gè)被 reject 的實(shí)例的返回值,會(huì)傳遞給 p 的回調(diào)函數(shù)。

下面是一個(gè)具體的例子。

// 生成一個(gè) Promise 對(duì)象的數(shù)組
const promises = [2, 3, 5, 7, 11, 13].map((id) => {
  return getJSON('/post/' + id + '.json');
});

Promise
  .all(promises)
  .then((posts) => {
    // ...
  })
  .catch((reason) => {
    // ...
  });

上面代碼中, promises 是包含 6 個(gè) Promise 實(shí)例的數(shù)組,只有這 6 個(gè)實(shí)例的狀態(tài)都變成 fulfilled ,或者其中有一個(gè)變?yōu)?rejected ,才會(huì)調(diào)用 promise.all 方法后面的回調(diào)函數(shù)。

下面是另一個(gè)例子。

const databasePromise = connectDatabase();

const bookPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

Promise
  .all([
    bookPromise,
    userPromise
  ])
  .then(([books, user]) => pickTopRecommentations(books, user));

上面代碼中, bookPromiseuserPromise 是兩個(gè)異步操作,只有等它們的結(jié)果都返回了,才會(huì)觸發(fā) pickTopRecommentations 這個(gè)回調(diào)函數(shù)。

注意,如果作為參數(shù)的 Promise 實(shí)例,自己定義了在 catch 方式,那么它一旦被 rejected ,并不會(huì)觸發(fā) Promise.all()catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報(bào)錯(cuò)了');
})
  .then(result => result)
  .catch(e => e);

Promise.all([p1, p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

// ['hello', Error: 報(bào)錯(cuò)了]

上面代碼中, p1 會(huì) resolved ,但是 p2 有自己的 catch 方法,該方法返回的是一個(gè)新的 Promise 實(shí)例, p2 指向的實(shí)際上是這個(gè)實(shí)例。該實(shí)例執(zhí)行完 catch 方法后,也會(huì)變成 resolved ,導(dǎo)致 Promise.all() 方法參數(shù)里面的兩個(gè)實(shí)例都會(huì) resolved ,因此會(huì)調(diào)用 then 方法指定的回調(diào)函數(shù),但是不會(huì)調(diào)用 catch 方法指定的回調(diào)函數(shù)。

如果 p2 沒有自己的 catch 方法,就會(huì)調(diào)用 Promise.all()catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報(bào)錯(cuò)了');
})
.then(result => result);

Promise
  .all([p1, p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));
// Error: 報(bào)錯(cuò)了

7. Promise.race()

Promise.race 方法同樣是講多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。

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

上面代碼中,只要 p1 、 p2p3 之中有一個(gè)實(shí)例率先改變狀態(tài), p 的狀態(tài)就跟著變。那個(gè)率先改變的 Promise 實(shí)例的返回值,就傳遞給 p 的回調(diào)函數(shù)。

Promise.race 方法的參數(shù)與 Promise.all 方法一樣,如果不是 Promise實(shí)例,就會(huì)先調(diào)用下面講到的 Promise.resolve 方法,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例,再進(jìn)一步處理。

下面是一個(gè)例子,如果指定時(shí)間內(nèi)沒有獲得結(jié)果,就將 Promise 的狀態(tài)變?yōu)? reject ,否則變?yōu)?resolve

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

p.then(response => console.log(response));
p.catch(error => console.log(error));

上面的代碼中,如果 5 秒之內(nèi) fetch 方法無(wú)法返回結(jié)果,變量 p 的狀態(tài)就會(huì)變?yōu)?rejected ,從而觸發(fā) catch 方法指定的回調(diào)函數(shù)。

題外話:這個(gè)例子可以用來(lái)解決 fetch 沒有 timeout API 的問題。


8. Promise.resolve()

有時(shí)需要將現(xiàn)有對(duì)象轉(zhuǎn)為 Promise 對(duì)象, Promise.resolve 方法就起到這個(gè)作用。

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

上面代碼將 jQuery 生成的 deferred 對(duì)象,轉(zhuǎn)為一個(gè)新的 Promise 對(duì)象。

Promise.resolve 等價(jià)于下面的寫法

Promise.resolve('foo');
// 等價(jià)于
new Promise(resolve => resolve('foo'));

Promise.resolve 方法共分為四種情況。

(1)參數(shù)是一個(gè) Promise 實(shí)例
如果參數(shù)是 Promise 實(shí)例,那么 Promise.resolve 將不做任何修改、原封不動(dòng)地返回這個(gè)實(shí)例。

(2)參數(shù)是一個(gè) thenable 對(duì)象
thenable 對(duì)象是指具有 then 方法的對(duì)象,比如下面這個(gè)對(duì)象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

Promise.resolve 方法會(huì)將這個(gè)對(duì)象轉(zhuǎn)為 Promise 對(duì)象,然后就立即執(zhí)行 thenable 對(duì)象的 then 方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value); // 42
});

上面代碼中, thenable 對(duì)象的 then 方法執(zhí)行后,對(duì)象 p1 的狀態(tài)就變?yōu)?resolved ,從而立即執(zhí)行最后那個(gè) then 方法指定的回調(diào)函數(shù),輸出 42。

(3)參數(shù)不具有 then 方法的對(duì)象,或者根本就不是對(duì)象

如果參數(shù)是一個(gè)原始值,或者是一個(gè)不具有 then 方法的對(duì)象,則 Promise.resolve 方法返回一個(gè)新的 Promise 對(duì)象,狀態(tài)為 resolved

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

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

上面代碼生成一個(gè)新的 Promise 對(duì)象的實(shí)例 p 。由于字符串 Hello 不屬于異步操作(判斷方式字符串對(duì)象不具有 then 方法),返回 Promise 實(shí)例的狀態(tài)從一生成就是 resolved ,所以回調(diào)函數(shù)會(huì)立即執(zhí)行。
Promise.resolve 方法的參數(shù),會(huì)同時(shí)傳遞給回調(diào)函數(shù)。

(4)不帶有任何參數(shù)

Promise.resolve 方法允許調(diào)用時(shí)不帶參數(shù),直接返回一個(gè) resolved 狀態(tài)的 Promise 對(duì)象。

所以,如果希望得到一個(gè) Promise 對(duì)象,比較方便的方法就是直接調(diào)用 Promise.resolve 方法。

const p = Promise.resolve();

p.then((value) => {
  // ...
})

上面代碼的變量 p 就是一個(gè) Promise 對(duì)象。

需要注意的是,立即 resolve 的 Promise 對(duì)象,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí),而不是在下一輪“事件循環(huán)”開始時(shí)。

setTimeout(() => {
  console.log('three');
}, 0);

Promise.resolve().then((value) => {
  console.log('two');
});

console.log('one');
// one
// two
// three

上面代碼中, setTimeout(fn, 0) 在下一輪“事件循環(huán)”開始時(shí)執(zhí)行, Promise.resolve() 在本輪“事件循環(huán)”結(jié)束時(shí)執(zhí)行, console.log('one')則是立即執(zhí)行,因此最先輸出。


9. Promise.reject()

Promise.reject(reason) 方法也返回一個(gè)新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為 rejected

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

p.then(null, s => console.log(s));
// 出錯(cuò)了

上面代碼生成了一個(gè) Promise 實(shí)例 p ,狀態(tài)為 rejected ,回調(diào)函數(shù)會(huì)立即執(zhí)行。

注意, Promise.reject() 方法的參數(shù),會(huì)原封不動(dòng)地作為 reject 的理由,變成后續(xù)方法的參數(shù)。這一點(diǎn)與 Promise.resolve 方法不一致。

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

Promise.reject(thenable)
  .catch(e => {
    console.log(e === thenable);
  })
// true

上面代碼中, Promise.reject 方法的參數(shù)是一個(gè) thenable 對(duì)象,執(zhí)行之后,后面 catch 方法的參數(shù)不是 reject 拋出的“出錯(cuò)了”這個(gè)字符串,而是 thenable 對(duì)象。


10. 應(yīng)用

加載圖片

我們可以將圖片的加載寫成一個(gè) Promise ,一旦加載完成, Promise 的狀態(tài)就發(fā)生變化。

const preloadImage = function(path) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
Generator 函數(shù)與 Promise 結(jié)合

使用 Generator 函數(shù)管理流程,遇到異步操作的時(shí)候,通常返回一個(gè) Promise 對(duì)象。

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

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

function run(generator) {
  const it = generator();

  function go(result) {
    if (result.done) {
      return result.value;
    }

    return result.value.then((value) => {
      return go(it.next(value));
    }, (error) => {
      return go(it.throw(error));
    })
  }

  go(it.next());
}

run(g);

上面代碼的 Generator 函數(shù) g 之中,有一個(gè)異步操作 getFoo ,它返回的就是一個(gè) Promise 對(duì)象。函數(shù) run 用來(lái)處理這個(gè) Promise 對(duì)象,并調(diào)用下一個(gè) next 方法。


11. Promise.try()

實(shí)際開發(fā)中,經(jīng)常遇到一種情況:不知道或者不想?yún)^(qū)分,函數(shù) f 是同步函數(shù)還是異步操作,但是想用 Promise 來(lái)處理它。因?yàn)檫@樣就可以不管 f 是否包含異步操作,都用 then 方法指定下一步流程,用 catch 方法處理 f 拋出的錯(cuò)誤。一般就采用下面的寫法。

Promise.resolve().then(f);

上面的寫法有一個(gè)缺點(diǎn),就是如果 f 是同步函數(shù),那么它會(huì)在本輪事件循環(huán)的末尾執(zhí)行。

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

上面代碼中,函數(shù) f 是同步的,但是用 Promise 包裝了以后,就變成異步操作了。

那么有沒有一種方法,讓同步函數(shù)同步執(zhí)行,異步函數(shù)異步執(zhí)行,并且讓它們具有統(tǒng)一的 API 呢?回答是可以的,并且還有兩種寫法。第一種寫法是用 async 函數(shù)來(lái)寫。

const f = () => console.log('now');
(async () => f)();
console.log('next');
// now
// next

上面代碼中,第二行是一個(gè)立即執(zhí)行的匿名函數(shù),會(huì)立即執(zhí)行里面的 async 函數(shù),因此如果 f 是同步的,就會(huì)得到同步的結(jié)果;如果 f 是異步的,就可以用 then 指定下一步,就像下面的寫法。

(async () => f())
  .then(...);

需要注意的是, async () => f()會(huì)吃掉 f() 拋出的錯(cuò)誤。所以如果想要捕獲錯(cuò)誤,要是使用 promise.catch 方法。

(async () => f())
  .then(...)
  .catch(...);

第二種寫法是使用 new Promise()

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

上面的代碼也是使用立即執(zhí)行的匿名函數(shù),執(zhí)行 new Promise() 。這種情況下,同步函數(shù)也是同步執(zhí)行的。

鑒于這是很常見的需求,所以現(xiàn)在有一個(gè)提案,提供 Promise.try 方法代替上面的寫法。

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

事實(shí)上, Promise.try 存在已久,Promise 庫(kù) bluebird 、 Qwhen ,早就提供了這個(gè)方法。

由于 Promise.try 為所有操作提供統(tǒng)一的處理機(jī)制,所以如果想用 then 方法管理流程,最好都用 Promise.try 包裝一下。這樣有很多好處,其中一點(diǎn)就是可以更好地管理異常。

function getUsername(userId) {
  return database.users.get({id: userId})
    .then((user) => {
      return user.name;
    })
};

上面代碼中, database.users.get() 返回一個(gè) Promise 對(duì)象,如果拋出一場(chǎng)錯(cuò)誤,可以用 catch 方法捕獲,就像下面這樣寫。

database.users.get({id: userId})
  .then(...)
  .catch(...)

但是 database.users.get() 可能還會(huì)拋出同步錯(cuò)誤(比如數(shù)據(jù)庫(kù)連接錯(cuò)誤,具體要看實(shí)現(xiàn)方法),這時(shí)你就不得不用 try...catch 捕獲

try {
  database.users.get({id: userId})
    .then(...)
    .catch(...)
} catch (e) {
  // ...
}

上面這樣的寫法就很笨拙了,這時(shí)就可以統(tǒng)一用 promise.catch() 捕獲所有同步和異步的錯(cuò)誤。

Promise.try(database.users.get({id: userId}))
  .then(...)
  .catch(...);

事實(shí)上, Promise.try 就是模擬 try 代碼塊,就像 promise.catch 模擬的是 catch 代碼塊。

摘抄自:http://es6.ruanyifeng.com/#docs/promise

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Promise 對(duì)象 Promise 的含義 Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,823評(píng)論 1 56
  • 00、前言Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它由社區(qū)...
    夜幕小草閱讀 2,217評(píng)論 0 12
  • Promise的含義: ??Promise是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和...
    呼呼哥閱讀 2,262評(píng)論 0 16
  • 我以為我是風(fēng)箏 你以為你牽著我的線 我以為我在自由的飛翔 你以為你在掌握著我的方向 如果 我一直努力 而你一直在原...
    兮云閱讀 518評(píng)論 4 4
  • 前幾天,接到一個(gè)久違的朋友電話。她知道我很喜歡讀書,閑聊之中突然問我:“讀書給你帶來(lái)什么?”當(dāng)時(shí)我脫口而出:”讀書...
    米素文閱讀 515評(píng)論 2 7

友情鏈接更多精彩內(nèi)容