【譯】JavaScript Promise API


原文地址:JavaScript Promise API

在 JavaScript 中,同步的代碼更容易書寫和 debug,但是有時候出於性能考慮,我們會寫一些異步的代碼(代替同步代碼)。思考這樣一個場景,同時觸發幾個異步請求,當所有請求到位時我們需要觸發一個回調,怎么做?Promise 讓一切變的簡單,越來越多的原生 API 基於 Promise 去實現。那么,什么是 Promise?Promise API 如何使用?

基於 Promise 的 原生 API

Promise 主要是為了解決異步的回調地獄。我們熟悉的 XMLHttpRequest API 可以異步使用,但是它沒有基於 Promise API。一些原生的 API 已經使用了 Promise,比如:

  • Battery API
  • fetch API(下一代 XHR)
  • ServiceWorker API

對於 Promise 的測試其實非常簡單,使用 SetTimeout 就能當做一個異步的事件來測試。

Promise 基本用法

Promise 本質其實是一個構造函數,其接受一個函數作為參數,而這個函數內部一般會寫一些異步事件處理的代碼,比如 SetTimeout 或者 XMLHttpRequest。異步事件我們一般都會有一個 "失敗" 的處理機制,我們還可以給這個作為參數的函數傳入兩個參數 resolve 和 reject,分別表示異步事件 "成功" 和 "失敗" 時的回調函數。

let p = new Promise((resolve, reject) => {
  // Do an async task
  setTimeout(() => {
    // good condition
    if (Math.random() > 0.5) {
      resolve('The number is bigger than 0.5');
    } else {
      reject('The number is smaller than 0.5');
    }
  }, 1000);
});

p.then(data => {
  // do something with the result
  console.log(data);
}, data => {
  // do something with the result
  console.log(data);
});

對於什么時候調用 resolve(可以粗略理解為異步操作成功)或者 reject(可以粗略理解為異步操作失敗)作為異步事件的回調函數,完全取決於開發者。

以下的代碼我們將 XMLHttpRequest 基於 Promise 去實現:

// From Jake Archibald's Promises and Back:
// http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promisifying-xmlhttprequest

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

// Use it!
get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
});

Promise 構造函數調用 new 操作符,傳入一些異步事件的代碼,會生成一個 Promise 實例對象,但是有的時候,我們需要生成一個 Promise 的實例對象,但是並不需要執行一些異步代碼,我們可以用 Promise.resolve() 和 Promise().reject() 來做這件事情。

var userCache = {};

function getUserDetail(username) {
  // In both cases, cached or not, a promise will be returned

  if (userCache[username]) {
    // Return a promise without the "new" keyword
    return Promise.resolve(userCache[username]);
  }

  // Use the fetch API to get the information
  // fetch returns a promise
  return fetch('users/' + username + '.json')
    .then(function(result) {
      userCache[username] = result;
      return result;
    })
    .catch(function() {
      throw new Error('Could not find user: ' + username);
    });
}

getUserDetail() 會始終返回一個 Promise 實例,所以 then 等方法可以在該函數返回值(即 Promise 實例)中使用。

then

所有 Promise 實例均擁有 then 方法,then 方法上可以定義兩個回調函數,第一個回調函數能接收到實例化 Promise 時通過 resolve 方法傳遞過來的參數(必須),而第二個回調函數對應 reject 方法(可選)。

new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { resolve(10); }, 1000);
})
.then(function(result) {
  console.log(result);
});

// From the console:
// 10

當這個 Promise 實例內部調用 resolve 方法時(Pending -> Resolved),then 中的第一個參數代表的回調函數被觸發。

then 也能被鏈式調用。

new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { resolve(10); }, 3000);
})
.then(function(num) { console.log('first then: ', num); return num * 2; })
.then(function(num) { console.log('second then: ', num); return num * 2; })
.then(function(num) { console.log('last then: ', num);});

// From the console:
// first then:  10
// second then:  20
// last then:  40

在 then 方法中,可以直接 return 數據而不是 Promise 對象,在后面的 then 中就可以接收到數據了。

catch

catch 方法有兩個作用。

第一個作用可以代替 then 方法的第二個參數。

let p = new Promise((resolve, reject) => {
  // Do an async task
  setTimeout(() => {
    // good condition
    if (Math.random() > 0.5) {
      resolve('The number is bigger than 0.5');
    } else {
      reject('The number is smaller than 0.5');
    }
  }, 1000);
});

p.then(data => {
  // do something with the result
  console.log(data);
}).catch(data => {
  // replace the second argument of then function
  console.log(data);
});

第二個作用有點像 try-catch,嘗試捕獲錯誤。在執行 resolve 的回調(也就是 then 中的第一個參數)時,如果拋出異常了(代碼出錯),那么會進入這個 catch 方法中。

let p = new Promise((resolve, reject) => {
  // Do an async task
  setTimeout(() => {
    // good condition
    if (Math.random() > 0) {
      resolve('The number is bigger than 0');
    }
  }, 1000);
});

p.then(data => {
  // do something with the result
  console.log(data);
  throw new Error();
}).catch(error => {
  console.log('There is an error.');
});

// The number is bigger than 0
// There is an error.

Promise.all

回到本文開頭說的應用場景,如果多個異步事件同時請求,我們需要在所有事件完成后觸發回調,Promise.all 方法可以滿足。該方法接收一個 Promise 實例組成的數組作為參數,當該數組中的所有 Promise 實例的狀態變為 resolved 時,觸發回調方法,回調方法的參數是由所有 Promise 實例的 resolve 函數的參數組成的數組。

Promise.all([promise1, promise2]).then(function(results) {
  // Both promises resolved
})
.catch(function(error) {
  // One or more promises was rejected
});

Promise.all 可以和 fetch 結合使用,因為 fetch 方法始終返回 Promise 實例。

var request1 = fetch('/users.json');
var request2 = fetch('/articles.json');

Promise.all([request1, request2]).then(function(results) {
  // Both promises done!
});

有任意的 Promise 實例拋出異常就會進入 catch 方法,但是需要注意的是,異步事件並不會停止執行

var req1 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() {
    resolve('First!');
    console.log('req1 ends!');
  }, 4000);
});
var req2 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { reject('Second!'); }, 3000);
});
Promise.all([req1, req2]).then(function(results) {
  console.log('Then: ', results);
}).catch(function(err) {
  console.log('Catch: ', err);
});

// From the console:
// Catch: Second!
// req1 ends!

2018/09/10 add: 以上代碼,有一個 Promise 拋出異常,Promise.all 里就得不到結果了,如果還是需要得到結果,可以稍作修改(req2 加了個 catch,這樣就不會進入 Promise.all 的 catch 里。然后異常的都會返回 undefined):

var req1 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() {
    resolve('First!');
    console.log('req1 ends!');
  }, 4000);
});

var req2 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { reject('Second!'); }, 3000);
}).catch(err => {console.log('err ' +  err)}) // 這里加個 catch

Promise.all([req1, req2]).then(function(results) {
  console.log('Then: ', results);
}).catch(function(err) {
  console.log('Catch: ', err);
});

// From the console:
// err Second!
// req1 ends!
// Then:  [ 'First!', undefined ]

Promise.race

Promise.race 接收參數和 Promise.all 類似,但是有任何實例狀態變為 resolved 或者 rejected 時,就會調用 then 或者 catch 中的回調。很顯然,它的回調的參數是一個值,並不是一個數組。

var req1 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { resolve('First!'); }, 8000);
});
var req2 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { reject('Second!'); }, 3000);
});
Promise.race([req1, req2]).then(function(one) {
  console.log('Then: ', one);
}).catch(function(one, two) {
  console.log('Catch: ', one);
});

// From the console:
// Then: Second!

對於 Promise.race 方法,需要注意的是,雖然有一個異步事件有了結果,便會執行 then 或者 catch 中的回調,但是其他的異步事件其實還會繼續執行。

var req1 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() {
    resolve('First!');
    console.log('req1 end!');
  }, 8000);
});
var req2 = new Promise(function(resolve, reject) {
  // A mock async action using setTimeout
  setTimeout(function() { reject('Second!'); }, 3000);
});
Promise.race([req1, req2]).then(function(one) {
  console.log('Then: ', one);
}).catch(function(one, two) {
  console.log('Catch: ', one);
});

// From the console:
// Then: Second!
// req1 end!

有個簡單的應用,有個文件的請求我們有多個地址,很顯然請求到了任意一個即可。

熟悉 Promise

我們有必要掌握 Promise,Promise 可以有效防止回調地獄,優化異步交互,使得異步代碼更加直觀。而且越來越多的原生 API 基於 Promise 去實現,我們有必要知其然,知其所以然。

譯者補充

Promise 的主要用法就是將各個異步操作封裝成好多 Promise,而一個 Promise 只處理一個異步邏輯。最后將各個 Promise 用鏈式調用寫法串聯,在這樣處理下,如果異步邏輯之間前后關系很重的話,你也不需要層層嵌套,只需要把每個異步邏輯封裝成 Promise 鏈式調用就可以了。

promise 模式在任何時刻都處於以下三種狀態之一:未完成(Pending)、已完成(Resolved)和拒絕(Rejected)。以 CommonJS Promise/A 標准為例,promise 對象上的 then 方法負責添加針對已完成和拒絕狀態下的處理函數。then 方法會返回另一個 promise 對象,以便於形成 promise 管道,這種返回 promise 對象的方式能夠支持開發人員把異步操作串聯起來,如 then(resolvedHandler, rejectedHandler); 。resolvedHandler 回調函數在 promise 對象進入完成狀態時會觸發,並傳遞結果;rejectedHandler 函數會在拒絕狀態下調用。

(2017.02.27)今天看到一道很有意思的關於 Promise 的題目,來自 http://www.cnblogs.com/libin-1/p/6443693.html,稍作修改:

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

new Promise((resolve) => {
  console.log(2);
  resolve();
  console.log(3);
}).then(() => {
  console.log(4);
});

console.log(5);

答案是 2 3 5 4 1


2019.03.09:

Promise 只能用 .catch 或者 .then 中的第二個函數去捕獲錯誤,是無法用 try..catch 捕獲到錯誤的;async/await 只能用 try..catch 去捕獲錯誤。推薦在 Promise 最后加上 .catch,如果 .catch 和 .then 第二個參數同時存在,會就近捕獲錯誤,.catch 可以放置好幾個,而不是只能放在最后,.catch 返回的也是一個 Promise,所以能鏈式

Promise 可以被當作隊列去執行:

let p = Promise.resolve();

[1, 2].forEach(item => {
  p.then(() => {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log(item)
        resolve()
      }, item * 1000)
    })
  })
})

以上代碼 1s 后輸出 1,2s 后輸出 2,是並行的,並不是隊列。

稍作修改:

let p = Promise.resolve();

[1, 2].forEach(item => {
  p = p.then(() => {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log(item)
        resolve()
      }, item * 1000)
    })
  })
})

這樣就能達成我們的目的,1s 后輸出 1,(1+2)s 后輸出 2,原理是將返回的 Promise 繼續賦值給 p,完成鏈式調用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM