前言
Promise 的基本使用可以看阮一峰老師的 《ECMAScript 6 入門》。
我們來聊點(diǎn)其他的。
回調(diào)說起 Promise,我們一般都會(huì)從回調(diào)或者回調(diào)地獄說起,那么使用回調(diào)到底會(huì)導(dǎo)致哪些不好的地方呢?
1. 回調(diào)嵌套使用回調(diào),我們很有可能會(huì)將業(yè)務(wù)代碼寫成如下這種形式:
doA( function(){ doB(); doC( function(){ doD(); } ) doE(); } ); doF();
當(dāng)然這是一種簡化的形式,經(jīng)過一番簡單的思考,我們可以判斷出執(zhí)行的順序?yàn)椋?/p>
doA() doF() doB() doC() doE() doD()
然而在實(shí)際的項(xiàng)目中,代碼會(huì)更加雜亂,為了排查問題,我們需要繞過很多礙眼的內(nèi)容,不斷的在函數(shù)間進(jìn)行跳轉(zhuǎn),使得排查問題的難度也在成倍增加。
當(dāng)然之所以導(dǎo)致這個(gè)問題,其實(shí)是因?yàn)檫@種嵌套的書寫方式跟人線性的思考方式相違和,以至于我們要多花一些精力去思考真正的執(zhí)行順序,嵌套和縮進(jìn)只是這個(gè)思考過程中轉(zhuǎn)移注意力的細(xì)枝末節(jié)而已。
當(dāng)然了,與人線性的思考方式相違和,還不是最糟糕的,實(shí)際上,我們還會(huì)在代碼中加入各種各樣的邏輯判斷,就比如在上面這個(gè)例子中,doD() 必須在 doC() 完成后才能完成,萬一 doC() 執(zhí)行失敗了呢?我們是要重試 doC() 嗎?還是直接轉(zhuǎn)到其他錯(cuò)誤處理函數(shù)中?當(dāng)我們將這些判斷都加入到這個(gè)流程中,很快代碼就會(huì)變得非常復(fù)雜,以至于無法維護(hù)和更新。
2. 控制反轉(zhuǎn)正常書寫代碼的時(shí)候,我們理所當(dāng)然可以控制自己的代碼,然而當(dāng)我們使用回調(diào)的時(shí)候,這個(gè)回調(diào)函數(shù)是否能接著執(zhí)行,其實(shí)取決于使用回調(diào)的那個(gè) API,就比如:
// 回調(diào)函數(shù)是否被執(zhí)行取決于 buy 模塊 import {buy} from "./buy.js"; buy(itemData, function(res) { console.log(res) });
對于我們經(jīng)常會(huì)使用的 fetch 這種 API,一般是沒有什么問題的,但是如果我們使用的是第三方的 API 呢?
當(dāng)你調(diào)用了第三方的 API,對方是否會(huì)因?yàn)槟硞€(gè)錯(cuò)誤導(dǎo)致你傳入的回調(diào)函數(shù)執(zhí)行了多次呢?
為了避免出現(xiàn)這樣的問題,你可以在自己的回調(diào)函數(shù)中加入判斷,可是萬一又因?yàn)槟硞€(gè)錯(cuò)誤這個(gè)回調(diào)函數(shù)沒有執(zhí)行呢?
萬一這個(gè)回調(diào)函數(shù)有時(shí)同步執(zhí)行有時(shí)異步執(zhí)行呢?
我們總結(jié)一下這些情況:
回調(diào)函數(shù)執(zhí)行多次
回調(diào)函數(shù)沒有執(zhí)行
回調(diào)函數(shù)有時(shí)同步執(zhí)行有時(shí)異步執(zhí)行
對于這些情況,你可能都要在回調(diào)函數(shù)中做些處理,并且每次執(zhí)行回調(diào)函數(shù)的時(shí)候都要做些處理,這就帶來了很多重復(fù)的代碼。
回調(diào)地獄我們先看一個(gè)簡單的回調(diào)地獄的示例。
現(xiàn)在要找出一個(gè)目錄中最大的文件,處理步驟應(yīng)該是:
用 fs.readdir 獲取目錄中的文件列表;
循環(huán)遍歷文件,使用 fs.stat 獲取文件信息
比較找出最大文件;
以最大文件的文件名為參數(shù)調(diào)用回調(diào)。
代碼為:
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; // 事先算好有多少個(gè)文件,讀完 1 個(gè)文件信息,計(jì)數(shù)減 1,當(dāng)為 0 時(shí),說明讀取完畢,此時(shí)執(zhí)行最終的比較操作 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)]) } }) }) }) }
使用方式為:
// 查找當(dāng)前目錄最大的文件 findLargest("./", function(er, filename) { if (er) return console.error(er) console.log("largest file was:", filename) });
你可以將以上代碼復(fù)制到一個(gè)比如 index.js 文件,然后執(zhí)行 node index.js 就可以打印出最大的文件的名稱。
看完這個(gè)例子,我們再來聊聊回調(diào)地獄的其他問題:
1.難以復(fù)用
回調(diào)的順序確定下來之后,想對其中的某些環(huán)節(jié)進(jìn)行復(fù)用也很困難,牽一發(fā)而動(dòng)全身。
舉個(gè)例子,如果你想對 fs.stat 讀取文件信息這段代碼復(fù)用,因?yàn)榛卣{(diào)中引用了外層的變量,提取出來后還需要對外層的代碼進(jìn)行修改。
2.堆棧信息被斷開
我們知道,JavaScript 引擎維護(hù)了一個(gè)執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行的時(shí)候,會(huì)創(chuàng)建該函數(shù)的執(zhí)行上下文壓入棧中,當(dāng)函數(shù)執(zhí)行完畢后,會(huì)將該執(zhí)行上下文出棧。
如果 A 函數(shù)中調(diào)用了 B 函數(shù),JavaScript 會(huì)先將 A 函數(shù)的執(zhí)行上下文壓入棧中,再將 B 函數(shù)的執(zhí)行上下文壓入棧中,當(dāng) B 函數(shù)執(zhí)行完畢,將 B 函數(shù)執(zhí)行上下文出棧,當(dāng) A 函數(shù)執(zhí)行完畢后,將 A 函數(shù)執(zhí)行上下文出棧。
這樣的好處在于,我們?nèi)绻袛啻a執(zhí)行,可以檢索完整的堆棧信息,從中獲取任何我們想獲取的信息。
可是異步回調(diào)函數(shù)并非如此,比如執(zhí)行 fs.readdir 的時(shí)候,其實(shí)是將回調(diào)函數(shù)加入任務(wù)隊(duì)列中,代碼繼續(xù)執(zhí)行,直至主線程完成后,才會(huì)從任務(wù)隊(duì)列中選擇已經(jīng)完成的任務(wù),并將其加入棧中,此時(shí)棧中只有這一個(gè)執(zhí)行上下文,如果回調(diào)報(bào)錯(cuò),也無法獲取調(diào)用該異步操作時(shí)的棧中的信息,不容易判定哪里出現(xiàn)了錯(cuò)誤。
此外,因?yàn)槭钱惒降木壒?,使?try catch 語句也無法直接捕獲錯(cuò)誤。
(不過 Promise 并沒有解決這個(gè)問題)
3.借助外層變量
當(dāng)多個(gè)異步計(jì)算同時(shí)進(jìn)行,比如這里遍歷讀取文件信息,由于無法預(yù)期完成順序,必須借助外層作用域的變量,比如這里的 count、errored、stats 等,不僅寫起來麻煩,而且如果你忽略了文件讀取錯(cuò)誤時(shí)的情況,不記錄錯(cuò)誤狀態(tài),就會(huì)接著讀取其他文件,造成無謂的浪費(fèi)。此外外層的變量,也可能被其它同一作用域的函數(shù)訪問并且修改,容易造成誤操作。
之所以多帶帶講講回調(diào)地獄,其實(shí)是想說嵌套和縮進(jìn)只是回調(diào)地獄的一個(gè)梗而已,它導(dǎo)致的問題遠(yuǎn)非嵌套導(dǎo)致的可讀性降低而已。
PromisePromise 使得以上絕大部分的問題都得到了解決。
1. 嵌套問題舉個(gè)例子:
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) });
而對于讀取最大文件的那個(gè)例子,我們使用 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)] }) }2. 控制反轉(zhuǎn)再反轉(zhuǎn)
前面我們講到使用第三方回調(diào) API 的時(shí)候,可能會(huì)遇到如下問題:
回調(diào)函數(shù)執(zhí)行多次
回調(diào)函數(shù)沒有執(zhí)行
回調(diào)函數(shù)有時(shí)同步執(zhí)行有時(shí)異步執(zhí)行
對于第一個(gè)問題,Promise 只能 resolve 一次,剩下的調(diào)用都會(huì)被忽略。
對于第二個(gè)問題,我們可以使用 Promise.race 函數(shù)來解決:
function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( "Timeout!" ); }, delay ); } ); } Promise.race( [ foo(), timeoutPromise( 3000 ) ] ) .then(function(){}, function(err){});
對于第三個(gè)問題,為什么有的時(shí)候會(huì)同步執(zhí)行有的時(shí)候回異步執(zhí)行呢?
我們來看個(gè)例子:
var cache = {...}; function downloadFile(url) { if(cache.has(url)) { // 如果存在cache,這里為同步調(diào)用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 這里為異步調(diào)用 } console.log("1"); getValue.then(() => console.log("2")); console.log("3");
在這個(gè)例子中,有 cahce 的情況下,打印結(jié)果為 1 2 3,在沒有 cache 的時(shí)候,打印結(jié)果為 1 3 2。
然而如果將這種同步和異步混用的代碼作為內(nèi)部實(shí)現(xiàn),只暴露接口給外部調(diào)用,調(diào)用方由于無法判斷是到底是異步還是同步狀態(tài),影響程序的可維護(hù)性和可測試性。
簡單來說就是同步和異步共存的情況無法保證程序邏輯的一致性。
然而 Promise 解決了這個(gè)問題,我們來看個(gè)例子:
var promise = new Promise(function (resolve){ resolve(); console.log(1); }); promise.then(function(){ console.log(2); }); console.log(3); // 1 3 2
即使 promise 對象立刻進(jìn)入 resolved 狀態(tài),即同步調(diào)用 resolve 函數(shù),then 函數(shù)中指定的方法依然是異步進(jìn)行的。
PromiseA+ 規(guī)范也有明確的規(guī)定:
實(shí)踐中要確保 onFulfilled 和 onRejected 方法異步執(zhí)行,且應(yīng)該在 then 方法被調(diào)用的那一輪事件循環(huán)之后的新執(zhí)行棧中執(zhí)行。Promise 反模式
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); })); }
如果你非要以隊(duì)列的形式執(zhí)行,你可以寫成:
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 拋出錯(cuò)誤,是無法被捕獲的。你可以寫成:
// good somethingAsync .then(function() { return somethingElseAsync() }) .then(null, function(err) { handleMyError(err); });
// good somethingAsync() .then(function() { return somethingElseAsync(); }) .catch(function(err) { handleMyError(err); });紅綠燈問題
題目:紅燈三秒亮一次,綠燈一秒亮一次,黃燈2秒亮一次;如何讓三個(gè)燈不斷交替重復(fù)亮燈?(用 Promse 實(shí)現(xiàn))
三個(gè)亮燈函數(shù)已經(jīng)存在:
function red(){ console.log("red"); } function green(){ console.log("green"); } function yellow(){ console.log("yellow"); }
利用 then 和遞歸實(shí)現(xiàn):
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();promisify
有的時(shí)候,我們需要將 callback 語法的 API 改造成 Promise 語法,為此我們需要一個(gè) promisify 的方法。
因?yàn)?callback 語法傳參比較明確,最后一個(gè)參數(shù)傳入回調(diào)函數(shù),回調(diào)函數(shù)的第一個(gè)參數(shù)是一個(gè)錯(cuò)誤信息,如果沒有錯(cuò)誤,就是 null,所以我們可以直接寫出一個(gè)簡單的 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
Promise 的局限性 1. 錯(cuò)誤被吃掉首先我們要理解,什么是錯(cuò)誤被吃掉,是指錯(cuò)誤信息不被打印嗎?
并不是,舉個(gè)例子:
throw new Error("error"); console.log(233333);
在這種情況下,因?yàn)?throw error 的緣故,代碼被阻斷執(zhí)行,并不會(huì)打印 233333,再舉個(gè)例子:
const promise = new Promise(null); console.log(233333);
以上代碼依然會(huì)被阻斷執(zhí)行,這是因?yàn)槿绻ㄟ^無效的方式使用 Promise,并且出現(xiàn)了一個(gè)錯(cuò)誤阻礙了正常 Promise 的構(gòu)造,結(jié)果會(huì)得到一個(gè)立刻跑出的異常,而不是一個(gè)被拒絕的 Promise。
然而再舉個(gè)例子:
let promise = new Promise(() => { throw new Error("error") }); console.log(2333333);
這次會(huì)正常的打印 233333,說明 Promise 內(nèi)部的錯(cuò)誤不會(huì)影響到 Promise 外部的代碼,而這種情況我們就通常稱為 “吃掉錯(cuò)誤”。
其實(shí)這并不是 Promise 獨(dú)有的局限性,try..catch 也是這樣,同樣會(huì)捕獲一個(gè)異常并簡單的吃掉錯(cuò)誤。
而正是因?yàn)殄e(cuò)誤被吃掉,Promise 鏈中的錯(cuò)誤很容易被忽略掉,這也是為什么會(huì)一般推薦在 Promise 鏈的最后添加一個(gè) catch 函數(shù),因?yàn)閷τ谝粋€(gè)沒有錯(cuò)誤處理函數(shù)的 Promise 鏈,任何錯(cuò)誤都會(huì)在鏈中被傳播下去,直到你注冊了錯(cuò)誤處理函數(shù)。
2. 單一值Promise 只能有一個(gè)完成值或一個(gè)拒絕原因,然而在真實(shí)使用的時(shí)候,往往需要傳遞多個(gè)值,一般做法都是構(gòu)造一個(gè)對象或數(shù)組,然后再傳遞,then 中獲得這個(gè)值后,又會(huì)進(jìn)行取值賦值的操作,每次封裝和解封都無疑讓代碼變得笨重。
說真的,并沒有什么好的方法,建議是使用 ES6 的解構(gòu)賦值:
Promise.all([Promise.resolve(1), Promise.resolve(2)]) .then(([x, y]) => { console.log(x, y); });3. 無法取消
Promise 一旦新建它就會(huì)立即執(zhí)行,無法中途取消。
4. 無法得知 pending 狀態(tài)當(dāng)處于 pending 狀態(tài)時(shí),無法得知目前進(jìn)展到哪一個(gè)階段(剛剛開始還是即將完成)。
參考《你不知道的 JavaScript 中卷》
Promise 的 N 種用法
JavaScript Promise 迷你書
Promises/A+規(guī)范
Promise 如何使用
Promise Anti-patterns
一道關(guān)于Promise應(yīng)用的面試題
ES6 系列ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預(yù)計(jì)寫二十篇左右,旨在加深 ES6 部分知識點(diǎn)的理解,重點(diǎn)講解塊級作用域、標(biāo)簽?zāi)0?、箭頭函數(shù)、Symbol、Set、Map 以及 Promise 的模擬實(shí)現(xiàn)、模塊加載方案、異步處理等內(nèi)容。
如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤?,請?wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵(lì)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/98419.html
摘要:標(biāo)準(zhǔn)引入了函數(shù),使得異步操作變得更加方便。在異步處理上,函數(shù)就是函數(shù)的語法糖。在實(shí)際項(xiàng)目中,錯(cuò)誤處理邏輯可能會(huì)很復(fù)雜,這會(huì)導(dǎo)致冗余的代碼。的出現(xiàn)使得就可以捕獲同步和異步的錯(cuò)誤。如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤?,請?wù)必給予指正,十分感謝。 async ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便。 在異步處理上,async 函數(shù)就是 Generator 函數(shù)的語法糖。 ...
摘要:前言這里的泛指之后的新語法這里的完全是指本文會(huì)不斷更新這里的使用是指本文會(huì)展示很多的使用場景這里的手冊是指你可以參照本文將項(xiàng)目更多的重構(gòu)為語法此外還要注意這里不一定就是正式進(jìn)入規(guī)范的語法。 前言 這里的 ES6 泛指 ES5 之后的新語法 這里的 完全 是指本文會(huì)不斷更新 這里的 使用 是指本文會(huì)展示很多 ES6 的使用場景 這里的 手冊 是指你可以參照本文將項(xiàng)目更多的重構(gòu)為 ES6...
摘要:第二部分源碼解析接下是應(yīng)用多個(gè)第二部分對于一個(gè)方法應(yīng)用了多個(gè),比如會(huì)編譯為在第二部分的源碼中,執(zhí)行了和操作,由此我們也可以發(fā)現(xiàn),如果同一個(gè)方法有多個(gè)裝飾器,會(huì)由內(nèi)向外執(zhí)行。有了裝飾器,就可以改寫上面的代碼。 Decorator 裝飾器主要用于: 裝飾類 裝飾方法或?qū)傩? 裝飾類 @annotation class MyClass { } function annotation(ta...
摘要:前端進(jìn)階進(jìn)階構(gòu)建項(xiàng)目一配置最佳實(shí)踐狀態(tài)管理之痛點(diǎn)分析與改良開發(fā)中所謂狀態(tài)淺析從時(shí)間旅行的烏托邦,看狀態(tài)管理的設(shè)計(jì)誤區(qū)使用更好地處理數(shù)據(jù)愛彼迎房源詳情頁中的性能優(yōu)化從零開始,在中構(gòu)建時(shí)間旅行式調(diào)試用輕松管理復(fù)雜狀態(tài)如何把業(yè)務(wù)邏輯這個(gè)故事講好和 前端進(jìn)階 webpack webpack進(jìn)階構(gòu)建項(xiàng)目(一) Webpack 4 配置最佳實(shí)踐 react Redux狀態(tài)管理之痛點(diǎn)、分析與...
摘要:一個(gè)對象若只被弱引用所引用,則被認(rèn)為是不可訪問或弱可訪問的,并因此可能在任何時(shí)刻被回收。也就是說,一旦不再需要,里面的鍵名對象和所對應(yīng)的鍵值對會(huì)自動(dòng)消失,不用手動(dòng)刪除引用。如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤?,請?wù)必給予指正,十分感謝。 前言 我們先從 WeakMap 的特性說起,然后聊聊 WeakMap 的一些應(yīng)用場景。 特性 1. WeakMap 只接受對象作為鍵名 const map = ...
閱讀 4000·2021-11-24 09:38
閱讀 1465·2021-11-19 09:40
閱讀 2797·2021-11-18 10:02
閱讀 3736·2021-11-09 09:46
閱讀 1806·2021-09-22 15:27
閱讀 3139·2019-08-29 15:24
閱讀 1027·2019-08-29 12:40
閱讀 1707·2019-08-28 18:24