Promise 對象

目錄:

  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. 應用
  11. Promise.try()

1. Promise 的含義

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

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

Promise 對象有以下兩個特點。

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

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

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

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

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

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


2. 基本用法

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.log('出錯了', 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 是一個 Rromise,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);
});

3. Promise.prototype.then()

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

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

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

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

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

getJSON("/posts.json")
  .then((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('/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) 的別名,用于指定發生錯誤時的回調函數。

getJSON('/posts.json')
  .then(posts => {
    // ...
  })
  .catch((err) => {
    // 處理 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((resolve, reject) => {
  throw new Error('test');
});

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

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

// 寫法一
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));

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

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

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 語句后面,再拋出錯誤,不會被捕獲,等于沒有拋出。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

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

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

上面的代碼中,一共有三個 Promise 對象:一個由 getJSON 產生,兩個由 then 產生。它們之中任何一個拋出的錯誤,都會被最后一個 catch 捕獲。

一般來說,不要在 then 方法里面定義 Reject 狀態的回調函數( then 的第二個參數),總是使用 catch 方法。

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

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

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

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

const someAsyncThing = function() {
  return new Promise((resolve, reject) => {
    // 下面一行會報錯,因為 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 函數產生的 Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示 ReferenceError: x is not defined ,但是不會退出進程、終止腳本執行,2 秒之后還是會輸出 123 。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法是“Promise 會吃掉錯誤”。

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

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

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

注意,Node 有計劃在未來廢除 unhandleRejection 事件。如果 Promise 內部有未捕獲的錯誤,會直接終止進程,并且進程的退出碼不為 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 指定在下一輪“事件循環”再拋出錯誤。到了那個時候,Promise 的運行已經結束了,所以這個錯誤是在 Promise 函數體外拋出的,會冒泡到最外層,成了未捕獲的錯誤。

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

const someAsyncThing = function() {
  return new Promise((resolve, reject) => {
    // 下面一行會報錯,因為 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

上面代碼運行完 catch 方法指定的回調函數,會接著運行后面那個 then 方法指定的回調函數。如果沒有報錯,則會跳過 catch 方法。

Promise
  .resolve()
  .catch((err) => {
    console.log('oh on', error);
  })
  .then(() => {
    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 方法拋出的錯誤。


5. Promise.prototype.finally()

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

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

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

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

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

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

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

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

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

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

它的實現也很簡單

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 ,都會執行回調函數 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(() => {})

6. 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 的狀態都變成 fulfilled ,此時 p1p2p3 的返回值組成一個數組,傳遞給 p 的回調函數。

(2)只要 p1p2p3 之中有一個被 rejectedp 的狀態就變成 rejected ,此時第一個被 reject 的實例的返回值,會傳遞給 p 的回調函數。

下面是一個具體的例子。

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

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

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

下面是另一個例子。

const databasePromise = connectDatabase();

const bookPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

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

上面代碼中, bookPromiseuserPromise 是兩個異步操作,只有等它們的結果都返回了,才會觸發 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: 報錯了]

上面代碼中, p1resolved ,但是 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: 報錯了

7. 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((resolve, reject) => {
    setTimeout(() => reject(new Error('request timeout')), 5000);
  })
]);

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

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

題外話:這個例子可以用來解決 fetch 沒有 timeout API 的問題。


8. 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((value) => {
  // ...
})

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

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

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

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

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

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


9. Promise.reject()

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

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

p.then(null, 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 對象。


10. 應用

加載圖片

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

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

使用 Generator 函數管理流程,遇到異步操作的時候,通常返回一個 Promise 對象。

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 函數 g 之中,有一個異步操作 getFoo ,它返回的就是一個 Promise 對象。函數 run 用來處理這個 Promise 對象,并調用下一個 next 方法。


11. 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((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 代碼塊。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函...
    neromous閱讀 8,719評論 1 56
  • 00、前言Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區...
    夜幕小草閱讀 2,137評論 0 12
  • Promise的含義: ??Promise是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和...
    呼呼哥閱讀 2,182評論 0 16
  • 我以為我是風箏 你以為你牽著我的線 我以為我在自由的飛翔 你以為你在掌握著我的方向 如果 我一直努力 而你一直在原...
    兮云閱讀 449評論 4 4
  • 前幾天,接到一個久違的朋友電話。她知道我很喜歡讀書,閑聊之中突然問我:“讀書給你帶來什么?”當時我脫口而出:”讀書...
    米素文閱讀 458評論 2 7