1. 程式人生 > >ES6學習(四)

ES6學習(四)

寫在前面

好久沒看es6了,昨天遇到一個與promise相關的bug,記錄下promise的相關知識了

阮一峰老師的Promise

正文

Promise物件是一個建構函式,用來生成Promise例項。

基礎用法

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

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

####非同步載入圖片

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物件實現的 Ajax 操作 getJSON是對 XMLHttpRequest 物件的封裝,用於發出一個針對 JSON 資料的 HTTP 請求,並且返回一個Promise物件。需要注意的是,在getJSON內部,resolve函式和reject函式呼叫時,都帶有引數。

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);
});

####注意小點

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

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

方法

Promise.prototype.then()

Promise 例項具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的。它的作用是為

Promise 例項新增狀態改變時的回撥函式

then方法的第一個引數 resolved狀態的回撥函式, 第二個引數(可選) rejected狀態的回撥函式。

可以採用鏈式寫法 鏈式中後一個then的執行是要在前一個then返回的Promise物件變化時再執行。。

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

Promise.prototype.catch()

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

// 基本用法
getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
  console.log('發生錯誤!', error);
});

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));

第二個then第一個引數null,只有rejected處理,

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方法的作用,等同於丟擲錯誤。

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) });

Promise 在resolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為

** Promise 的狀態一旦改變,就永久保持該狀態**,

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

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

不會再變了。

Promise 物件的錯誤具有**“冒泡”性質**,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。 但是這種冒泡只有鏈條中可以處理

跟傳統的try/catch程式碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回撥函式,Promise 物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應

Promise 內部的錯誤不會影響到 Promise 外部的程式碼,通俗的說法就是“Promise 會吃掉錯誤”。


Promise.prototype.finally()

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

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

不管promise最後的狀態,在執行完then或catch指定的回撥函式以後,都會執行finally方法指定的回撥函式。

finally方法的回撥函式不接受任何引數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,

finally方法裡面的操作,應該是與狀態無關的,

不依賴於 Promise 的執行結果。

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

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

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

Promise.all()

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

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

p的狀態由p1、p2、p3決定,分成兩種情況。

(1)只有p1、p2、p3的狀態都變成fulfilled

p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回撥函式。

(2)只要p1、p2、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 例項,自己定義了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: 報錯了]

p1會resolved,p2首先會rejected,但是p2有自己的catch方法,該方法返回的是一個新的 Promise 例項,p2指向的實際上是這個例項。該例項執行完catch方法後,也會變成resolved,導致Promise.all()方法引數裡面的兩個例項都會resolved,因此會呼叫then方法指定的回撥函式,而不會呼叫catch方法指定的回撥函式。

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


Promise.race()

同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。

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


Promise.resolve()

將現有物件轉為 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')則是立即執行,因此最先輸出。