Promise 的基本使用能夠看阮一峯老師的 《ECMAScript 6 入門》。html
咱們來聊點其餘的。node
提及 Promise,咱們通常都會從回調或者回調地獄提及,那麼使用回調到底會致使哪些很差的地方呢?git
使用回調,咱們頗有可能會將業務代碼寫成以下這種形式:es6
doA( function(){ doB(); doC( function(){ doD(); } ) doE(); } ); doF();
固然這是一種簡化的形式,通過一番簡單的思考,咱們能夠判斷出執行的順序爲:github
doA() doF() doB() doC() doE() doD()
然而在實際的項目中,代碼會更加雜亂,爲了排查問題,咱們須要繞過不少礙眼的內容,不斷的在函數間進行跳轉,使得排查問題的難度也在成倍增長。面試
固然之因此致使這個問題,實際上是由於這種嵌套的書寫方式跟人線性的思考方式相違和,以致於咱們要多花一些精力去思考真正的執行順序,嵌套和縮進只是這個思考過程當中轉移注意力的細枝末節而已。segmentfault
固然了,與人線性的思考方式相違和,還不是最糟糕的,實際上,咱們還會在代碼中加入各類各樣的邏輯判斷,就好比在上面這個例子中,doD() 必須在 doC() 完成後才能完成,萬一 doC() 執行失敗了呢?咱們是要重試 doC() 嗎?仍是直接轉到其餘錯誤處理函數中?當咱們將這些判斷都加入到這個流程中,很快代碼就會變得很是複雜,以致於沒法維護和更新。數組
正常書寫代碼的時候,咱們理所固然能夠控制本身的代碼,然而當咱們使用回調的時候,這個回調函數是否能接着執行,其實取決於使用回調的那個 API,就好比:promise
// 回調函數是否被執行取決於 buy 模塊 import {buy} from './buy.js'; buy(itemData, function(res) { console.log(res) });
對於咱們常常會使用的 fetch 這種 API,通常是沒有什麼問題的,可是若是咱們使用的是第三方的 API 呢?異步
當你調用了第三方的 API,對方是否會由於某個錯誤致使你傳入的回調函數執行了屢次呢?
爲了不出現這樣的問題,你能夠在本身的回調函數中加入判斷,但是萬一又由於某個錯誤這個回調函數沒有執行呢?
萬一這個回調函數有時同步執行有時異步執行呢?
咱們總結一下這些狀況:
對於這些狀況,你可能都要在回調函數中作些處理,而且每次執行回調函數的時候都要作些處理,這就帶來了不少重複的代碼。
咱們先看一個簡單的回調地獄的示例。
如今要找出一個目錄中最大的文件,處理步驟應該是:
fs.readdir
獲取目錄中的文件列表;fs.stat
獲取文件信息代碼爲:
var fs = require('fs'); var path = require('path'); function findLargest(dir, cb) { // 讀取目錄下的全部文件 fs.readdir(dir, function(er, files) { if (er) return cb(er); var counter = files.length; var errored = false; var stats = []; files.forEach(function(file, index) { // 讀取文件信息 fs.stat(path.join(dir, file), function(er, stat) { if (errored) return; if (er) { errored = true; return cb(er); } stats[index] = stat; // 事先算好有多少個文件,讀完 1 個文件信息,計數減 1,當爲 0 時,說明讀取完畢,此時執行最終的比較操做 if (--counter == 0) { var largest = stats .filter(function(stat) { return stat.isFile() }) .reduce(function(prev, next) { if (prev.size > next.size) return prev return next }) cb(null, files[stats.indexOf(largest)]) } }) }) }) }
使用方式爲:
// 查找當前目錄最大的文件 findLargest('./', function(er, filename) { if (er) return console.error(er) console.log('largest file was:', filename) });
你能夠將以上代碼複製到一個好比 index.js
文件,而後執行 node index.js
就能夠打印出最大的文件的名稱。
看完這個例子,咱們再來聊聊回調地獄的其餘問題:
1.難以複用
回調的順序肯定下來以後,想對其中的某些環節進行復用也很困難,牽一髮而動全身。
舉個例子,若是你想對 fs.stat
讀取文件信息這段代碼複用,由於回調中引用了外層的變量,提取出來後還須要對外層的代碼進行修改。
2.堆棧信息被斷開
咱們知道,JavaScript 引擎維護了一個執行上下文棧,當函數執行的時候,會建立該函數的執行上下文壓入棧中,當函數執行完畢後,會將該執行上下文出棧。
若是 A 函數中調用了 B 函數,JavaScript 會先將 A 函數的執行上下文壓入棧中,再將 B 函數的執行上下文壓入棧中,當 B 函數執行完畢,將 B 函數執行上下文出棧,當 A 函數執行完畢後,將 A 函數執行上下文出棧。
這樣的好處在於,咱們若是中斷代碼執行,能夠檢索完整的堆棧信息,從中獲取任何咱們想獲取的信息。
但是異步回調函數並不是如此,好比執行 fs.readdir
的時候,實際上是將回調函數加入任務隊列中,代碼繼續執行,直至主線程完成後,纔會從任務隊列中選擇已經完成的任務,並將其加入棧中,此時棧中只有這一個執行上下文,若是回調報錯,也沒法獲取調用該異步操做時的棧中的信息,不容易斷定哪裏出現了錯誤。
此外,由於是異步的緣故,使用 try catch 語句也沒法直接捕獲錯誤。
(不過 Promise 並無解決這個問題)
3.藉助外層變量
當多個異步計算同時進行,好比這裏遍歷讀取文件信息,因爲沒法預期完成順序,必須藉助外層做用域的變量,好比這裏的 count、errored、stats 等,不只寫起來麻煩,並且若是你忽略了文件讀取錯誤時的狀況,不記錄錯誤狀態,就會接着讀取其餘文件,形成無謂的浪費。此外外層的變量,也可能被其它同一做用域的函數訪問而且修改,容易形成誤操做。
之因此單獨講講回調地獄,實際上是想說嵌套和縮進只是回調地獄的一個梗而已,它致使的問題遠非嵌套致使的可讀性下降而已。
Promise 使得以上絕大部分的問題都獲得瞭解決。
舉個例子:
request(url, function(err, res, body) { if (err) handleError(err); fs.writeFile('1.txt', body, function(err) { request(url2, function(err, res, body) { if (err) handleError(err) }) }) });
使用 Promise 後:
request(url) .then(function(result) { return writeFileAsynv('1.txt', result) }) .then(function(result) { return request(url2) }) .catch(function(e){ handleError(e) });
而對於讀取最大文件的那個例子,咱們使用 promise 能夠簡化爲:
var fs = require('fs'); var path = require('path'); var readDir = function(dir) { return new Promise(function(resolve, reject) { fs.readdir(dir, function(err, files) { if (err) reject(err); resolve(files) }) }) } var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stat) { if (err) reject(err) resolve(stat) }) }) } function findLargest(dir) { return readDir(dir) .then(function(files) { let promises = files.map(file => stat(path.join(dir, file))) return Promise.all(promises).then(function(stats) { return { stats, files } }) }) .then(data => { let largest = data.stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return data.files[data.stats.indexOf(largest)] }) }
前面咱們講到使用第三方回調 API 的時候,可能會遇到以下問題:
對於第一個問題,Promise 只能 resolve 一次,剩下的調用都會被忽略。
對於第二個問題,咱們可使用 Promise.race 函數來解決:
function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( "Timeout!" ); }, delay ); } ); } Promise.race( [ foo(), timeoutPromise( 3000 ) ] ) .then(function(){}, function(err){});
對於第三個問題,爲何有的時候會同步執行有的時候回異步執行呢?
咱們來看個例子:
var cache = {...}; function downloadFile(url) { if(cache.has(url)) { // 若是存在cache,這裏爲同步調用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 這裏爲異步調用 } console.log('1'); getValue.then(() => console.log('2')); console.log('3');
在這個例子中,有 cahce 的狀況下,打印結果爲 1 2 3,在沒有 cache 的時候,打印結果爲 1 3 2。
然而若是將這種同步和異步混用的代碼做爲內部實現,只暴露接口給外部調用,調用方因爲沒法判斷是究竟是異步仍是同步狀態,影響程序的可維護性和可測試性。
簡單來講就是同步和異步共存的狀況沒法保證程序邏輯的一致性。
然而 Promise 解決了這個問題,咱們來看個例子:
var promise = new Promise(function (resolve){ resolve(); console.log(1); }); promise.then(function(){ console.log(2); }); console.log(3); // 1 3 2
即便 promise 對象馬上進入 resolved 狀態,即同步調用 resolve 函數,then 函數中指定的方法依然是異步進行的。
PromiseA+ 規範也有明確的規定:
實踐中要確保 onFulfilled 和 onRejected 方法異步執行,且應該在 then 方法被調用的那一輪事件循環以後的新執行棧中執行。
1.Promise 嵌套
// bad loadSomething().then(function(something) { loadAnotherthing().then(function(another) { DoSomethingOnThem(something, another); }); });
// good Promise.all([loadSomething(), loadAnotherthing()]) .then(function ([something, another]) { DoSomethingOnThem(...[something, another]); });
2.斷開的 Promise 鏈
// bad function anAsyncCall() { var promise = doSomethingAsync(); promise.then(function() { somethingComplicated(); }); return promise; }
// good function anAsyncCall() { var promise = doSomethingAsync(); return promise.then(function() { somethingComplicated() }); }
3.混亂的集合
// bad function workMyCollection(arr) { var resultArr = []; function _recursive(idx) { if (idx >= resultArr.length) return resultArr; return doSomethingAsync(arr[idx]).then(function(res) { resultArr.push(res); return _recursive(idx + 1); }); } return _recursive(0); }
你能夠寫成:
function workMyCollection(arr) { return Promise.all(arr.map(function(item) { return doSomethingAsync(item); })); }
若是你非要以隊列的形式執行,你能夠寫成:
function workMyCollection(arr) { return arr.reduce(function(promise, item) { return promise.then(function(result) { return doSomethingAsyncWithResult(item, result); }); }, Promise.resolve()); }
4.catch
// bad somethingAync.then(function() { return somethingElseAsync(); }, function(err) { handleMyError(err); });
若是 somethingElseAsync 拋出錯誤,是沒法被捕獲的。你能夠寫成:
// good somethingAsync .then(function() { return somethingElseAsync() }) .then(null, function(err) { handleMyError(err); });
// good somethingAsync() .then(function() { return somethingElseAsync(); }) .catch(function(err) { handleMyError(err); });
題目:紅燈三秒亮一次,綠燈一秒亮一次,黃燈2秒亮一次;如何讓三個燈不斷交替重複亮燈?(用 Promse 實現)
三個亮燈函數已經存在:
function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); }
利用 then 和遞歸實現:
function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); } var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); }); }; var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(2000, green); }).then(function(){ return light(1000, yellow); }).then(function(){ step(); }); } step();
有的時候,咱們須要將 callback 語法的 API 改形成 Promise 語法,爲此咱們須要一個 promisify 的方法。
由於 callback 語法傳參比較明確,最後一個參數傳入回調函數,回調函數的第一個參數是一個錯誤信息,若是沒有錯誤,就是 null,因此咱們能夠直接寫出一個簡單的 promisify 方法:
function promisify(original) { return function (...args) { return new Promise((resolve, reject) => { args.push(function callback(err, ...values) { if (err) { return reject(err); } return resolve(...values) }); original.call(this, ...args); }); }; }
完整的能夠參考 es6-promisif
首先咱們要理解,什麼是錯誤被吃掉,是指錯誤信息不被打印嗎?
並非,舉個例子:
throw new Error('error'); console.log(233333);
在這種狀況下,由於 throw error 的緣故,代碼被阻斷執行,並不會打印 233333,再舉個例子:
const promise = new Promise(null); console.log(233333);
以上代碼依然會被阻斷執行,這是由於若是經過無效的方式使用 Promise,而且出現了一個錯誤阻礙了正常 Promise 的構造,結果會獲得一個馬上跑出的異常,而不是一個被拒絕的 Promise。
然而再舉個例子:
let promise = new Promise(() => { throw new Error('error') }); console.log(2333333);
此次會正常的打印 233333
,說明 Promise 內部的錯誤不會影響到 Promise 外部的代碼,而這種狀況咱們就一般稱爲 「吃掉錯誤」。
其實這並非 Promise 獨有的侷限性,try..catch 也是這樣,一樣會捕獲一個異常並簡單的吃掉錯誤。
而正是由於錯誤被吃掉,Promise 鏈中的錯誤很容易被忽略掉,這也是爲何會通常推薦在 Promise 鏈的最後添加一個 catch 函數,由於對於一個沒有錯誤處理函數的 Promise 鏈,任何錯誤都會在鏈中被傳播下去,直到你註冊了錯誤處理函數。
Promise 只能有一個完成值或一個拒絕緣由,然而在真實使用的時候,每每須要傳遞多個值,通常作法都是構造一個對象或數組,而後再傳遞,then 中得到這個值後,又會進行取值賦值的操做,每次封裝和解封都無疑讓代碼變得笨重。
說真的,並無什麼好的方法,建議是使用 ES6 的解構賦值:
Promise.all([Promise.resolve(1), Promise.resolve(2)]) .then(([x, y]) => { console.log(x, y); });
Promise 一旦新建它就會當即執行,沒法中途取消。
當處於 pending 狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。