JS-Promise對象使用方法

文章目錄

1、Promise.prototype.then()
      定義在原型對象Promise.prototype上的。它的作用是爲 Promise 實例添加狀態改變時的回調函數。

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

3、Promise.prototype.finally()
      finally方法用於指定不管 Promise 對象最後狀態如何,都會執行的操作。

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

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

6、Promise.resolve()
      有時需要將現有對象轉爲 Promise 對象,Promise.resolve方法就起到這個作用。

7、Promise.reject()
      reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected。

8、Promise.try()
      不知道或者不想區分,函數f是同步函數還是異步操作,但是想用 Promise 來處理它

 

Promise 的含義

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

所謂Promise,簡單說就是一個容器,裏面保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。

Promis對象有以下兩個特點。

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

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

注意,爲了行文方便,本章後面的resolved統一隻指fulfilled狀態,不包含rejected狀態。

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

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

如果某些事件不斷地反覆發生,一般來說,使用 Stream 模式是比部署Promise更好的選擇。

基本用法

ES6 規定,Promise對象是一個構造函數,用來生成Promise實例。下面代碼創造了一個Promise實例。

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

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

Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolvereject。它們是兩個函數,由 JavaScript 引擎提供,不用自己部署。

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

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

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

then方法可以接受兩個回調函數作爲參數。第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。其中,第二個函數是可選的,不一定要提供。這兩個函數都接受Promise對象傳出的值作爲參數。

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

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

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

上面代碼中,timeout方法返回一個Promise實例,表示一段時間以後纔會發生的結果。過了指定的時間(ms參數)以後,Promise實例的狀態變爲resolved,就會觸發then方法綁定的回調函數。

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最後輸出。

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

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包裝了一個圖片加載的異步操作。如果加載成功,就調用resolve方法,否則就調用reject方法。

下面是一個用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);
});

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

如果調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。reject函數的參數通常是Error對象的實例,表示拋出的錯誤;resolve函數的參數除了正常的值以外,還可能是另一個 Promise 實例,比如像下面這樣。

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

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

上面代碼中,p1p2都是 Promise 的實例,但是p2resolve方法將p1作爲參數,即一個異步操作的結果是返回另一個異步操作。

注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。如果p1的狀態是pending,那麼p2的回調函數就會等待p1的狀態改變;如果p1的狀態已經是resolved或者rejected,那麼p2的回調函數將會立刻執行。

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是一個 Promise,3 秒之後變爲rejectedp2的狀態在 1 秒之後改變,resolve方法返回的是p1。由於p2返回的是另一個 Promise,導致p2自己的狀態無效了,由p1的狀態決定p2的狀態。所以,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變爲rejected,導致觸發catch方法指定的回調函數。

注意,調用resolvereject並不會終結 Promise 的參數函數的執行。

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

上面代碼中,調用resolve(1)以後,後面的console.log(2)還是會執行,並且會首先打印出來。這是因爲立即 resolved 的 Promise 是在本輪事件循環的末尾執行,總是晚於本輪循環的同步任務。

一般來說,調用resolvereject以後,Promise 的使命就完成了,後繼操作應該放到then方法裏面,而不應該直接寫在resolvereject的後面。所以,最好在它們前面加上return語句,這樣就不會有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 後面的語句不會執行
  console.log(2);
})

1、Promise.prototype.then()

Promise 實例具有then方法,也就是說,then方法是定義在原型對象Promise.prototype上的。它的作用是爲 Promise 實例添加狀態改變時的回調函數。前面說過,then方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。

then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以採用鏈式寫法,即then方法後面再調用另一個then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

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

採用鏈式的then,可以指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的還是一個Promise對象(即有異步操作),這時後一個回調函數,就會等待該Promise對象的狀態發生變化,纔會被調用。

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

上面代碼中,第一個then方法指定的回調函數,返回的是另一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。如果變爲resolved,就調用funcA,如果狀態變爲rejected,就調用funcB

如果採用箭頭函數,上面的代碼可以寫得更簡潔。

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

2、Promise.prototype.catch()

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

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

上面代碼中,getJSON方法返回一個 Promise 對象,如果該對象狀態變爲resolved,則會調用then方法指定的回調函數;如果異步操作拋出錯誤,狀態就會變爲rejected,就會調用catch方法指定的回調函數,處理這個錯誤。另外,then方法指定的回調函數,如果運行中拋出錯誤,也會被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));

下面是一個例子。

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

上面代碼中,promise拋出一個錯誤,就被catch方法指定的回調函數捕獲。注意,上面的寫法與下面兩種寫法是等價的。

// 寫法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 寫法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

比較上面兩種寫法,可以發現reject方法的作用,等同於拋出錯誤。

如果 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

上面代碼中,Promise 在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。因爲 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

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

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

上面代碼中,一共有三個 Promise 對象:一個由getJSON產生,兩個由then產生。它們之中任何一個拋出的錯誤,都會被最後一個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
  });

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

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

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,因爲x沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

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

上面代碼中,someAsyncThing函數產生的 Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined,但是不會退出進程、終止腳本執行,2 秒之後還是會輸出123。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是“Promise 會喫掉錯誤”。

這個腳本放在服務器執行,退出碼就是0(即表示執行成功)。不過,Node 有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤,上面的腳本會觸發這個事件的監聽函數,可以在監聽函數裏面拋出錯誤。

process.on('unhandledRejection', function (err, p) {
  throw err;
});

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

注意,Node 有計劃在未來廢除unhandledRejection事件。如果 Promise 內部有未捕獲的錯誤,會直接終止進程,並且進程的退出碼不爲 0。

再看下面的例子。

const 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 指定在下一輪“事件循環”再拋出錯誤。到了那個時候,Promise 的運行已經結束了,所以這個錯誤是在 Promise 函數體外拋出的,會冒泡到最外層,成了未捕獲的錯誤。

一般總是建議,Promise 對象後面要跟catch方法,這樣可以處理 Promise 內部發生的錯誤。catch方法返回的還是一個 Promise 對象,因此後面還可以接着調用then方法。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,因爲x沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing()
.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方法。

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

上面的代碼因爲沒有報錯,跳過了catch方法,直接執行後面的then方法。此時,要是then方法裏面報錯,就與前面的catch無關了。

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

上面代碼中,catch方法拋出一個錯誤,因爲後面沒有別的catch方法了,導致這個錯誤不會被捕獲,也不會傳遞到外層。如果改寫一下,結果就不一樣了。

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方法拋出的錯誤。

3、Promise.prototype.finally()

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

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

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

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

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

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

finally本質上是then方法的特例。

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

// 等同於
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

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

它的實現也很簡單。

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

上面代碼中,不管前面的 Promise 是fulfilled還是rejected,都會執行回調函數callback

從上面的實現還可以看到,finally方法總是會返回原來的值。

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

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

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

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

4、Promise.all()

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

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

上面代碼中,Promise.all方法接受一個數組作爲參數,p1p2p3都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。)

p的狀態由p1p2p3決定,分成兩種情況。

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

(2)只要p1p2p3之中有一個被rejectedp的狀態就變成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){
  // ...
});

上面代碼中,promises是包含 6 個 Promise 實例的數組,只有這 6 個實例的狀態都變成fulfilled,或者其中有一個變爲rejected,纔會調用Promise.all方法後面的回調函數。

下面是另一個例子。

const databasePromise = connectDatabase();

const booksPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

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

上面代碼中,booksPromiseuserPromise是兩個異步操作,只有等到它們的結果都返回了,纔會觸發pickTopRecommentations這個回調函數。

注意,如果作爲參數的 Promise 實例,自己定義了catch方法,那麼它一旦被rejected,並不會觸發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('報錯了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]

上面代碼中,p1resolvedp2首先會rejected,但是p2有自己的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的實際上是這個實例。該實例執行完catch方法後,也會變成resolved,導致Promise.all()方法參數裏面的兩個實例都會resolved,因此會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。

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

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

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

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

5、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 實例,再進一步處理。

下面是一個例子,如果指定時間內沒有獲得結果,就將 Promise 的狀態變爲reject,否則變爲resolve

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方法指定的回調函數。

6、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方法的參數分成四種情況。

(1)參數是一個 Promise 實例

如果參數是 Promise 實例,那麼Promise.resolve將不做任何修改、原封不動地返回這個實例。

(2)參數是一個thenable對象

thenable對象指的是具有then方法的對象,比如下面這個對象。

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

Promise.resolve方法會將這個對象轉爲 Promise 對象,然後就立即執行thenable對象的then方法。

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

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

上面代碼中,thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而立即執行最後那個then方法指定的回調函數,輸出 42。

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

如果參數是一個原始值,或者是一個不具有then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved

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

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

上面代碼生成一個新的 Promise 對象的實例p。由於字符串Hello不屬於異步操作(判斷方法是字符串對象不具有 then 方法),返回 Promise 實例的狀態從一生成就是resolved,所以回調函數會立即執行。Promise.resolve方法的參數,會同時傳給回調函數。

(4)不帶有任何參數

Promise.resolve方法允許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。

所以,如果希望得到一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});

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

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

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

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

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

7、Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected

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

上面代碼中,Promise.reject方法的參數是一個thenable對象,執行以後,後面catch方法的參數不是reject拋出的“出錯了”這個字符串,而是thenable對象。

加載圖片

我們可以將圖片的加載寫成一個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方法。

8、Promise.try()

實際開發中,經常遇到一種情況:不知道或者不想區分,函數f是同步函數還是異步操作,但是想用 Promise 來處理它。因爲這樣就可以不管f是否包含異步操作,都用then方法指定下一步流程,用catch方法處理f拋出的錯誤。一般就會採用下面的寫法。

Promise.resolve().then(f)

上面的寫法有一個缺點,就是如果f是同步函數,那麼它會在本輪事件循環的末尾執行。

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

上面代碼中,函數f是同步的,但是用 Promise 包裝了以後,就變成異步執行了。

那麼有沒有一種方法,讓同步函數同步執行,異步函數異步執行,並且讓它們具有統一的 API 呢?回答是可以的,並且還有兩種寫法。第一種寫法是用async函數來寫。

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

上面代碼中,第二行是一個立即執行的匿名函數,會立即執行裏面的async函數,因此如果f是同步的,就會得到同步的結果;如果f是異步的,就可以用then指定下一步,就像下面的寫法。

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

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

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

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

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

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

鑑於這是一個很常見的需求,所以現在有一個提案,提供Promise.try方法替代上面的寫法。

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

事實上,Promise.try存在已久,Promise 庫BluebirdQwhen,早就提供了這個方法。

由於Promise.try爲所有操作提供了統一的處理機制,所以如果想用then方法管理流程,最好都用Promise.try包裝一下。這樣有許多好處,其中一點就是可以更好地管理異常。

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

上面代碼中,database.users.get()返回一個 Promise 對象,如果拋出異步錯誤,可以用catch方法捕獲,就像下面這樣寫。

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

但是database.users.get()可能還會拋出同步錯誤(比如數據庫連接錯誤,具體要看實現方法),這時你就不得不用try...catch去捕獲。

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

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

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

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章