前端客棧 - Promise書生 (一)
前文
“前端客棧?想必前方就是寧安城了,此地為寧安城的郊區,”書生忖道,“時間不早了,今日不如在此停頓休整一日。”
進得店裡,書生吩咐店小二將馬匹安排妥當。訂罷客房,要了杯茶,便進房間休息。不到半個鐘頭,外面天色已黑,便下來吃晚飯。
入住的人並不多,十來張桌子都有些空曠。店小二靠牆邊站著,大概其他客人下來得早。
書生找了靠角落的位置坐下,店小二隨即跟來,遞上食單。“客官您看看想吃什麼,這上面除了排骨和鵝,其他的都有。”
書生望向店小二,心裡暗道:“這店小二雖是敝巾舊服,但生得腰圓背厚,面闊口方,更兼劍眉星眼,料想不會是久困之人。”
“一份小白菜,蔥拌豆腐,三兩牛肉,一碗米飯,加一瓶楊梅汁。”
“好叻,您稍等片刻。”
書生點點頭,隨即掏出手機開啟這個禮拜的JavaScript weekly,想看看有何新鮮。
無精彩之處。
不到一刻鐘,店小二端來托盤,全部上齊。隨即待在書生身後的牆邊。
書生左手劃手機,右手夾著小蔥拌豆腐,不時喝一口楊梅汁,腦子裡胡思亂想。想到今天一整天未曾好好說話,心生一種孤寂之感。想到前面的路似乎還很漫長,家鄉還有人等著,書生嘆了口氣,收起了手機,斷斷續續吃著。
想說說話。
見店小二目不轉睛,近似呆滯,書生便抬手示意,招來店小二。
“你們店為啥叫前端客棧呢?”
“客官,這店名啊,小的聽說掌櫃的以前是做前端的,後來轉行了,做過許多行當,但最難忘的還是前端的日子,店名也跟著叫了前端客棧。說來也怪,我來這不久也漸漸對前端生了興趣,目前自己也在學。只是基礎不太行,學得相當吃力。”店小二說完不好意思地笑了笑。
“這倒有趣,我還以為是因為這地離寧安城不遠。我對前端也略知一二,不知道你學得如何了?”
“吃力歸吃力,但學得還算有頭有尾。最近開始學es6的Promise了,進展有些慢,可能是小的比較笨吧。”
“Promise我也略知,不知你有何困惑之處?”
“小的還沒弄清Promise要解決什麼問題,只是鏈式呼叫嗎?”
Promise要解決什麼問題
“在Promise出現之前,瀏覽器中的回撥就已經很常見了。一個回撥還好,但如果回撥函式巢狀多了,則容易出現被稱為‘回撥地獄’的情況。這你知道,對吧?”
店小二點點頭,“回撥地獄就是回撥函式巢狀太多,如果邏輯的分支太多會讓函式難以理解,呼叫順序和程式碼順序對應不上。慢慢地可能難以理解難以維護。”
“是的。為了緩解回撥地獄的問題,大家做了不少嘗試,比如儘量恰當地拆分邏輯,然後用函式表示每一個步驟。
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
// 得益於函式變數提升,可以自由組織位置。
function doSomething (cb){
...
cb && cb(value)
}
function doSomethingElse (input,cb){
...
cb && cb(value)
}
function doThirdThing (input,cb){
...
cb && cb(value)
}
這樣邏輯上可看得清楚一些。
“是的。除此之外,回掉函式的錯誤處理也是一個問題。由於回撥函式不可控,我們編碼時需要主動假設回撥可能出錯,編碼時也要有相應處理。nodejs的風格是將回調函式返回的第一個引數作為錯誤。但這意味著需要處理每一個錯誤,流程將非常繁瑣,需要判斷很多分支。
此外,在某些情況中,人們可能使用釋出訂閱模式對程式碼邏輯進行處理。”
“釋出訂閱模式也被稱為觀察者模式嗎?”店小二問道。
書生微微點頭:“《headfirst設計模式》提到釋出訂閱模式只是觀察者模式的別名,但現在人們往往認為有些區別。觀察者模式的前提是被觀察者有自己的釋出機制,是一對多;而釋出訂閱模式中,釋出者和訂閱者都依賴一箇中間人,全交由中間人處理,可以處理多對多。不過大體上它們是一樣的,都是一種響應機制。”
“小的似懂非懂。”
但看到店小二的眼神仍舊不太明朗,書生打算慢慢地講。
“除此之外,回撥函式的執行沒有一個統一的規範。當你想在某個函式中執行一個回撥函式,那麼就必須以來一個前提:這個函式定義好了形參,並且會在函式中呼叫。
乍看這並不是個問題,只是要定義好就行。但是否有似乎成了一種約定,在程式碼層面沒法檢測。如果有了統一的規範,統一的檢測方式,事情是不是會變得更簡單呢?”說完,書生定定地看著店小二。
“Promise就是那個規範,對吧?”
“是的,另外Promise還讓非同步呼叫變得更加可控。它不只是檢測入參是否有回撥並且呼叫,它同時可以傳遞這個回撥到合適的時機去處理,這個是它最厲害的。如下面程式碼,cb在何時執行,就看resolveCallback要怎麼處理。”
const supportCallback = function(cb){
const resolveCallback() = ...
resolveCallback(cb)
}
“小的聽不太懂了,小的只知道Promise呼叫和那幾個api。”
“那你給我講講怎麼使用,如何?我看看你的瞭解多少。”
“好的,讓小的直接敲程式碼吧。”說完從裡屋拿出一臺電腦,坐在書生旁邊敲了起來。其他客人仍舊擺著原來的姿勢,似遊戲裡npc一般,不曾打擾這兩人。
Promise的使用
常規使用
//店小二在註釋中寫道:“以下是最基本的建立。“
const dianxiaoerPromise = new Promise(resolve => {
setTimeout(() => {
resolve('hello')
}, 2000)
})
//建立之後用then
dianxiaoerPromise.then(res => {
//1then
console.log(res)
})
//可以分開的多次then,then裡面的函式會按註冊順序執行
dianxiaoerPromise.then(res => {
//2then
console.log(res)
})
//還可以鏈式then, 鏈式then中傳遞各種型別資料,主要分為PromiseLike和非PromiseLike
//以下為非PromiseLike
dianxiaoerPromise.then(res => {
console.log(val) // 引數val = 'hello'
return 'world'
}).then(val => {
console.log(val) // 引數val = 'world'
})
//PromiseLike
//如果有非同步操作,則把操作放在Promise中,返回出去,外層的then則會在非同步操作之後執行
dianxiaoerPromise.then(res => {
return new Promise(resolve => {
setTimeout(() => {
resolve('world')
}, 2000)
})
}).then(val => {
console.log(val) // 引數val = 'world'
})
//裡面返回的裡層的promise也可以then,並且順序可控
dianxiaoerPromise.then(res => {
return new Promise(resolve => {
setTimeout(() => {
resolve('world')
}, 2000)
}).then(res=>{
return '調皮'
})
}).then(val => {
console.log(val) // 引數val = '調皮'
})
// 但不能像下面這樣,返回之前建立的Promise;這是個不合法的操作。
// 如果瀏覽器支援這麼執行,後面的then永遠無法執行
dianxiaoerPromise.then(res => {
return dianxiaoerPromise
}).then(val => {
console.log(val) // 引數val = 'world'
})
書生看著店小二敲程式碼,不時嗯兩聲,表示認可,同時細細地吃牛肉,喝楊梅汁。偶爾會一口吃上幾種食物,白菜、豆腐和牛肉加酸梅汁,混在一起。
敲到這裡,書生吃了6口雜燴,每一口都嚼了二十多下。
“不錯不錯,基本用法你掌握得相當準確了。你看你上面的寫法已經緩解了回撥地獄問題,非同步也變得更更加可控。這就是我之前說的resolveCallback的功能:如果傳遞過來的還在執行的非同步操作,則將自己的回撥轉交給那個非同步操作去觸發。”
店小二點點頭。
“那麼接下來你寫寫Promise中的錯誤處理吧。”書生又道。
店小二輕輕應了一聲,繼續敲程式碼。
錯誤處理
//Promise中的錯誤處理主要有兩種方式
//用onReject函式
const p1 = new Promise(function(resolve, reject) {
resolve('Success');
});
p1.then(function(value) {
console.log(value); // "Success!"
throw 'oh, no!';
}).then(function(res){
console.log('不會執行');
}, function (err) { //onReject函式
console.error(err); //'oh, no!';
});
// 或者用catch,catch是在內部呼叫then(undefined, onRejected))
p1.then(function(value) {
console.log(value); // "Success!"
throw 'oh, no!';
}).catch(function (err) {
console.error(err); //'oh, no!';
});
// 在非同步函式中丟擲的錯誤不會被catch捕獲到
const p2 = new Promise(function(resolve, reject) {
setTimeout(function() {
throw 'Uncaught Exception!';
}, 1000);
});
p2.catch(function(e) {
console.log(e); // 不會執行
});
//錯誤處理會根據promise的狀態處理,
//如果已經fullfilled,已經完成
Promise.resolve("calling next").catch(function (reason) {
//這個方法永遠不會呼叫
console.log(reason);
});
//如果是失敗狀態
Promise.reject("calling next").catch(function (reason) {
//則會和then一樣放在微觀任務(瀏覽器執行機制)中,適時執行
console.log(reason);
});
//只要丟擲的錯誤或reject在某個環節catch住,後面的流程則迴歸正常
Promise.reject("calling next").catch(function (reason) {
//則會和then一樣放在微觀任務(瀏覽器執行機制)中,適時執行
console.error(reason);
return '錯誤已處理'
}).then(res=>{
//執行
console.log(res) //'錯誤已處理'
},err=>{
//後面的錯誤處理不會執行
})
//如果catch中出現丟擲錯誤,則catch後面的catch才會起作用
Promise.reject("calling next").catch(function (reason) {
throw "error"
}).then(res=>{
//不會執行
console.log(res) //
},err=>{
//這裡才會執行
//用cacht也能執行
console.error(err)
})
//如果promise的reject不處理,則瀏覽器會觸發一個unhandledrejection,一般觸發源在window,也可以是Worker
//這時候進行事件監聽
window.addEventListener("unhandledrejection", (event) => {
// 可能要在開發服務下才能獲取到事件,直接開啟檔案獲取不到
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
“甚奇,當朝已出現電腦和手機這等精巧之物,為何老百姓仍依仗著馬匹遠行,”書生緩緩思索著。店小二兩手一攤,舒展了下腰身。
房裡穿過一陣風,帶著房簷上的燈籠微微晃動。風聲,蟋蟀叫,幾位客人在小聲說著什麼。“此刻愜意即可,”書生腦中響起這句話。
“小的已經寫完Promise的錯誤處理。”店小二把電腦稍稍移動,轉向書生。
書生看了幾眼:“非常不錯,你對promise的掌握已經相當熟練。那麼你可知道Promise的那些api?繼續寫吧。”
店小二寫了起來。
白菜和小蔥拌豆腐已吃大半,牛肉還剩不少,書生認真地看著店小二敲程式碼。
一些api
//Promise.all 的基本呼叫
//可以傳入promise例項和非promise,非promise會被保留
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});
//當all中傳入的可迭代物件為空時,all為同步
console.log("同步1");
const p1 = Promise.all([]);
console.log(p1); //resolved
console.log("非同步");
const p2 = Promise.all([1, 3, 4, 5]);
console.log(p2); //pending
//如果在所有都完成前有個失敗的,則all狀態為失敗,並且返回失敗值
var p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'one');
});
var p2 = new Promise((resolve, reject) => {
reject('reject');
});
Promise.all([p1, p2, ]).then(values => {
},error=>{
console.error(error) // reject
});
//Promise.race正常呼叫
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// Both resolve, but promise2 is faster
});
//如果為race傳入空的可迭代物件,則例項一直pending
const p = Promise.race([]);
console.log(p); //pending
setTimeout(() => {
console.log(p); //pending
}, 0);
//設x為race的引數中所能找到的第一個非pending狀態promise的值,如果存在x,則返回x
//如果不存在x,則返回第一個非pending的promise的返回值
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 500);
});
const p2 = Promise.resovle(2);
const race = Promise.race([p1, p2,3]).then((res) => {
console.log(res); //2
});
const race = Promise.race([p1, 3, p2]).then((res) => {
console.log(res); //3
});
//Promise.resolve正常呼叫
Promise.resolve("Success").then(function(value) {
console.log(value); // "Success"
}, function(value) {
// 不會被呼叫
});
//resolve一個promise,
//值得一提的時,當值為promise時,resolve返回的promise的狀態會跟隨傳入的promise
//就是說,它的狀態不一定是resolved
//而reject方法則一定是reject
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(15);
}, 1000);
// 或者reject('error')
});
Promise.resolve(p1).then(function (value) {
console.log("value: " + value);
},e=>{
console.error(e)
});
//resolve一個thenable物件也可以呼叫物件中的then
var thenable = {
then: function (resolve) {
resolve("Resolving");
},
};
Promise.resolve(thenable).then((res) => {
console.log(res);
});
//then中報錯也能處理
var thenable = {
then: function (resolve) {
throw new TypeError("Throwing");
},
};
Promise.resolve(thenable).catch((e) => {
console.error(e);
});
//reject正常呼叫,
const p = Promise.reject(Promise.resolve())
console.log(p) //狀態永遠是rejected
p.then(function() {
// not called
}, function(error) {
console.error(error); // Stacktrace
});
“小的已寫完,”店小二停下來說道。
書生的白菜和豆腐吃得差不多了,還剩下許多牛肉。從小便如此,喜歡的東西往往要留在後面。
“精彩,看得出來基礎非常紮實。那麼,你是打算深入理解Promise?”書生夾著牛肉說道。
“正是。小的聽聞Promise並不只是瀏覽器的原生物件,可以通過js程式碼來實現,但小的琢磨不透它該如何實現。嘗試看了Promise規範,卻於事無補,終究理解不能。”
“正好,當年我從西域的一個網站上找到一份Promise原始碼,潛心修煉之後已大概融合其心法。原始碼先傳授給你,你先自己過一遍,我們再交流,如何?”
店小二心領神會:“願先生賜教!”
“那好,你先看著吧,我先回房休息。”丟下原始碼,吃完幾口牛肉,灌了楊梅汁,書生便回房休息了。
燈火比剛才又暗了一些。店小二還坐在老位子,眉頭多半緊縮,偶爾舒放。
作為一個店小二,他確實有些蹊蹺,但在故事之外他仍然是個一本正經的店小二。至於其他客人,則都是尋常旅人,各個生得平平無奇,吃飯、休息,然後上路,沒有絲毫衝突值得訴諸筆墨。
Promise原始碼
class MyPromise {
constructor(exector) {
this.status = MyPromise.PENDING;
// 1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).
this.value = null;
// 1.5 “reason” is a value that indicates why a promise was rejected.
this.reason = null;
this.resolved = false;
/**
* 2.2.6 then may be called multiple times on the same promise
* 2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then
* 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
*/
this.onFulfilledCallback = [];
this.onRejectedCallback = [];
this.initBind();
this.init(exector);
}
initBind() {
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
init(exector) {
try {
exector(this.resolve, this.reject);
} catch (err) {
this.reject(err);
}
}
resolve(value) {
if (this.status === MyPromise.PENDING && this.resolved === false) {
this.resolved = true;
setTimeout(() => {
this.status = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallback.forEach((cb) => cb(this.value));
});
}
}
reject(reason) {
if (this.status === MyPromise.PENDING) {
setTimeout(() => {
this.status = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallback.forEach((cb) => cb(this.reason));
});
}
}
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
let promise2;
if (this.status === MyPromise.FULFILLED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.status === MyPromise.REJECTED) {
return (promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
MyPromise.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (this.status === MyPromise.PENDING) {
return (promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledCallback.push((value) => {
try {
const x = onFulfilled(value);
MyPromise.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
this.onRejectedCallback.push((reason) => {
try {
const x = onRejected(reason);
MyPromise.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
}
}
// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending";
MyPromise.FULFILLED = "fulfilled";
MyPromise.REJECTED = "rejected";
MyPromise.resolvePromise = (promise2, x, resolve, reject) => {
let called = false;
/**
* 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
*/
if (promise2 === x) {
const err = new TypeError(
"cannot return the same promise object from onfulfilled or on rejected callback."
);
console.error(err);
return reject(err);
}
if (x instanceof MyPromise) {
// 處理返回值是 Promise 物件的情況
/**
* new MyPromise(resolve => {
* resolve("Success")
* }).then(data => {
* return new MyPromise(resolve => {
* resolve("Success2")
* })
* })
*/
if (x.status === MyPromise.PENDING) {
/**
* 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
*/
x.then(
(y) => {
MyPromise.resolvePromise(promise2, y, resolve, reject);
},
(reason) => {
reject(reason);
}
);
} else {
/**
* 2.3 If x is a thenable, it attempts to make promise adopt the state of x,
* under the assumption that x behaves at least somewhat like a promise.
*
* 2.3.2 If x is a promise, adopt its state [3.4]:
* 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
* 2.3.2.4 If/when x is rejected, reject promise with the same reason.
*/
x.then(resolve, reject);
}
/**
* 2.3.3 Otherwise, if x is an object or function,
*/
} else if ((x !== null && typeof x === "object") || typeof x === "function") {
/**
* 2.3.3.1 Let then be x.then.
* 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
*/
try {
// then 方法可能設定了訪問限制(setter),因此這裡進行了錯誤捕獲處理
const then = x.then;
if (typeof then === "function") {
/**
* 2.3.3.2 If retrieving the property x.then results in a thrown exception e,
* reject promise with e as the reason.
*/
/**
* 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
* 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
*/
then.call(
x,
(y) => {
/**
* If both resolvePromise and rejectPromise are called,
* or multiple calls to the same argument are made,
* the first call takes precedence, and any further calls are ignored.
*/
if (called) return;
called = true;
MyPromise.resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
/**
* 2.3.3.3.4 If calling then throws an exception e,
* 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
* 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
*/
if (called) return;
called = true;
reject(e);
}
} else {
// If x is not an object or function, fulfill promise with x.
resolve(x);
}
};