前排佔樓
個人開源專案 — Vchat 正式上線了,歡迎各位小哥哥小姐姐體驗。如果覺得還行的話,記得給個star喲 ^_^。
言歸正傳,你經歷過絕望嗎?
眾所周知,js是單執行緒非同步機制的。這樣就會導致很多非同步處理會巢狀很多的回撥函式,最為常見的就是ajax請求,我們需要等請求結果返回後再進行某些操作。如:
function success(data, status) {
console.log(data)
}
function fail(err, status) {
console.log(err)
}
ajax({
url: myUrl,
type: 'get',
dataType: 'json',
timeout: 1000,
success: success(data, status),
fail: fail(err, status)
})
複製程式碼
乍一看還行啊,不夠絕望啊,讓絕望來的更猛烈一些吧!那麼試想一下,如果還有多個請求依賴於上一個請求的返回值呢?五個?六個?程式碼就會變得非常冗餘和不易維護。這種現象,我們一般親切地稱它為‘回撥地獄’。現在解決回撥地獄的手段有很多,比如非常方便的async/await、Promise等。
我們現在要講的是Promise。在如今的前端面試中,Promise簡直是考點般的存在啊,十個有九個會問。那麼我們如何真正的弄懂Promise呢?俗話說的好,‘想要了解它,先要接近它,再慢慢地實現它’。自己實現一個Promise,不就什麼都懂了。
其實網路上關於Promise的文章有很多,我也查閱了一些相關文章,文末有給出相關原文連結。所以本文側重點是我在實現Promise過程中的思路以及個人的一些理解,有感興趣的小夥伴可以一起交流。
如果用promise實現上面的ajax,大概是這個效果:
ajax().success().fail();
複製程式碼
何為 Promise
那麼什麼是Promise呢?
- Promise是為了解決非同步程式設計的弊端,使你的程式碼更有條理、更清晰、更易維護。
- Promise是一個建構函式(或者類),接受一個函式作為引數,該函式接受resolve,reject兩個引數。
- 它的內部有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗),其中pending可以轉化為fulfilled或者和rejected,但是不能逆向轉化,成功和失敗也不能相互轉化。
- value、reason成功的引數和失敗的錯誤資訊。
- then方法,實現鏈式呼叫,類似於jq。
基本用法:
let getInfo = new Promise((resolve, reject) => {
setTimeout(_ => {
let ran = Math.random();
console.log(ran);
if (ran > 0.5) {
resolve('success');
} else {
reject('fail');
}
}, 200);
});
getInfo.then(r => {
return r + ' ----> Vchat';
}).then(r => {
console.log(r);
}).catch(err => {
console.log(err);
})
// ran > 0.5輸出 success ----> Vchat
// ran <= 0.5輸出 fail
複製程式碼
先定個小目標,然後一步步實現它。
構建Promise
-
基礎構造
首先需要了解一下基本原理。我第一次接觸Promise的時候,還很懵懂(捂臉)。那會只知道這麼寫,不知道到底是個什麼流程走向。下面,我們來看看最基本的實現:
function Promise(Fn){ let resolveCall = function() {console.log('我是預設的');}; // 定義為函式是為了防止沒有then方法時報錯 this.then = (onFulfilled) => { resolveCall = onFulfilled; }; function resolve(v){ // 將resolve的引數傳給then中的回撥 resolveCall(v); } Fn(resolve); } new Promise((resolve, reject) => { setTimeout(_ => { resolve('success'); }, 200) }).then(r => { console.log(r); }); // success 複製程式碼
這裡需要注意的是,當我們new Promise 的時候Promise裡的函式會直接執行。所以如果你想定義一個Promise以待後用,比如axios封裝,需要用函式包裝。比如這樣:
function myPromise() { return new Promise((resolve, reject) => { setTimeout(_ => { resolve('success'); }, 200) }) } // myPromise().then() 複製程式碼
再回到上面,在new Promise 的時候會立即執行fn,遇到非同步方法,於是先執行then中的方法,將 onFulfilled 儲存到 resolveCall 中。非同步時間到了後,執行 resolve,從而執行 resolveCall即儲存的then方法。這是輸出的是我們傳入的‘success’
這裡會有一個問題,如果 Promise 接受的方法不是非同步的,則會導致 resolve 比 then 方法先執行。而此時 resolveCall 還沒有被賦值,得不到我們想要的結果。所以要給resolve加上非同步操作,從而保證then方法先執行。
// 直接resolve new Promise((resolve, reject) => { resolve('success'); }).then(r => { console.log(r); // 輸出為 ‘我是預設的’,因為此時then方法還沒有,then方法的回撥沒有賦值給resolveCall,執行的是預設定義的function() {}。 }); // 加上非同步處理,保證then方法先執行 function resolve(v){ setTimeout(_ => { resolveCall(v); }) } 複製程式碼
-
增加鏈式呼叫
鏈式呼叫是Promise非常重要的一個特徵,但是上面寫的那個函式顯然是不支援鏈式呼叫的,所以我們需要進行處理,在每一個then方法中return一下this。
function Promise(Fn){ this.resolves = []; // 方便儲存onFulfilled this.then = (onFulfilled) => { this.resolves.push(onFulfilled); return this; }; let resolve = (value) =>{ // 改用箭頭函式,這樣不用擔心this指標問題 setTimeout(_ => { this.resolves.forEach(fn => fn(value)); }); }; Fn(resolve); } 複製程式碼
可以看到,這裡將接收then回撥的方法改為了Promise的屬性resolves,而且是陣列。這是因為如果有多個then,依次push到陣列的方式才能儲存,否則後面的then會將之前儲存的覆蓋掉。這樣等到resolve被呼叫的時候,依次執行resolves中的函式就可以了。這樣可以進行簡單的鏈式呼叫。
new Promise((resolve, reject) => { resolve('success'); }).then(r => { console.log(r); // success }).then(r => { console.log(r); // success }); 複製程式碼
但是我們會有這樣的需求, 某一個then鏈想自己return一個引數供後面的then使用,如:
then(r => { console.log(r); return r + ' ---> Vchat'; }).then(); 複製程式碼
要做到這一步,需要再加一個處理。
let resolve = (value) =>{ setTimeout(_ => { // 每次執行then的回撥時判斷一下是否有返回值,有的話更新value this.resolves.forEach(fn => value = fn(value) || value); }); }; 複製程式碼
-
增加狀態
我們在文章開始說了Promise的三種狀態以及成功和失敗的引數,現在我們需要體現在自己寫的例項裡面。
function Promise(Fn){ this.resolves = []; this.status = 'PENDING'; // 初始為'PENDING'狀態 this.value; this.then = (onFulfilled) => { if (this.status === 'PENDING') { // 如果是'PENDING',則儲存到陣列中 this.resolves.push(onFulfilled); } else if (this.status === 'FULFILLED') { // 如果是'FULFILLED',則立即執行回撥 console.log('isFULFILLED'); onFulfilled(this.value); } return this; }; let resolve = (value) =>{ if (this.status === 'PENDING') { // 'PENDING' 狀態才執行resolve操作 setTimeout(_ => { //狀態轉換為FULFILLED //執行then時儲存到resolves裡的回撥 //如果回撥有返回值,更新當前value this.status = 'FULFILLED'; this.resolves.forEach(fn => value = fn(value) || value); // 這裡有一個問題 實際上Promise 並沒有判斷是否有fanhui返回值 // fn => value = fn(value),沒有返回值就是 undefined this.value = value; }); } }; Fn(resolve); } 複製程式碼
這裡可能會有同學覺得困惑,我們通過一個例子來說明增加的這些處理到底有什麼用。
let getInfo = new Promise((resolve, reject) => { resolve('success'); }).then(_ => { console.log('hahah'); }); setTimeout(_ => { getInfo.then(r => { console.log(r); // success }) }, 200); 複製程式碼
在resolve函式中,判斷了'PENDING' 狀態才執行setTimeout方法,並且在執行時更改了狀態為'FULFILLED'。這時,如果執行這個例子,只會輸出一個‘hahah’,因為接下來的非同步方法呼叫時狀態已經被改為‘FULFILLED’,所以不會再次執行。
這種情況要想它可以執行,就需要用到then方法裡的判斷,如果狀態是'FULFILLED',則立即執行回撥。此時的傳參是在resolve執行時儲存的this.value。這樣就符合Promise的狀態原則,PENDING不可逆,FULFILLED和REJECTED不能相互轉化。
-
增加失敗處理
可能有同學發現我一直沒有處理reject,不用著急。reject和resolve流程是一樣的,需要一個reason做為失敗的資訊返回。在鏈式呼叫中,只要有一處出現了reject,後續的resolve都不應該執行,而是直接返回reject。
this.reason; this.rejects = []; // 接收失敗的onRejected函式 if (this.status === 'PENDING') { this.rejects.push(onRejected); } // 如果狀態是'REJECTED',則立即執行onRejected。 if (this.status === 'REJECTED') { onRejected(this.reason); } // reject方法 let reject = (reason) =>{ if (this.status === 'PENDING') { setTimeout(_ => { //狀態轉換為REJECTED //執行then時儲存到rejects裡的回撥 //如果回撥有返回值,更新當前reason this.status = 'REJECTED'; this.rejects.forEach(fn => reason = fn(reason) || reason); this.reason = reason; }); } }; // 執行Fn出錯直接reject try { Fn(resolve, reject); } catch(err) { reject(err); } 複製程式碼
在執行儲存then中的回撥函式那一步有一個細節一直沒有處理,那就是判斷是否有onFulfilled或者onRejected方法,因為是允許不要其中一個的。現在如果then中缺少某個回撥,會直接push進undefined,如果執行的話就會出錯,所以要先判斷一下是否是函式。
this.then = (onFulfilled, onRejected) => { // 判斷是否是函式,是函式則執行 function success (value) { return typeof onFulfilled === 'function' && onFulfilled(value) || value; } function erro (reason) { return typeof onRejected === 'function' && onRejected(reason) || reason; } // 下面的處理也要換成新定義的函式 if (this.status === 'PENDING') { this.resolves.push(success); this.rejects.push(erro); } else if (this.status === 'FULFILLED') { success(this.value); } else if (this.status === 'REJECTED') { erro(this.reason); } return this; }; 複製程式碼
因為reject回撥執行時和resolve基本一樣,所以稍微優化一下部分程式碼。
if(this.status === 'PENDING') { let transition = (status, val) => { setTimeout(_ => { this.status = status; let f = status === 'FULFILLED', queue = this[f ? 'resolves' : 'rejects']; queue.forEach(fn => val = fn(val) || val); this[f ? 'value' : 'reason'] = val; }); }; function resolve(value) { transition('FULFILLED', value); } function reject(reason) { transition('REJECTED', reason); } } 複製程式碼
-
序列 Promise
假設有多個ajax請求串聯呼叫,即下一個需要上一個的返回值作為引數,並且要return一個新的Promise捕捉錯誤。這樣我們現在的寫法就不能實現了。
我的理解是之前的then返回的一直是this,但是如果某一個then方法出錯了,就無法跳出迴圈、丟擲異常。而且原則上一個Promise,只要狀態改變成‘FULFILLED’或者‘REJECTED’就不允許再次改變。
之前的例子可以執行是因為沒有在then中做異常的處理,即沒有reject,只是傳遞了資料。所以如果要做到每一步都可以獨立的丟擲異常,從而終止後面的方法執行,還需要再次改造,我們需要每個then方法中return一個新的Promise。
// 把then方法放到原型上,這樣在new一個新的Promise時會去引用prototype的then方法,而不是再複製一份。 Promise.prototype.then = function(onFulfilled, onRejected) { let promise = this; return new Promise((resolve, reject) => { function success (value) { let val = typeof onFulfilled === 'function' && onFulfilled(value) || value; resolve(val); // 執行完這個then方法的onFulfilled以後,resolve下一個then方法 } function erro (reason) { let rea = typeof onRejected === 'function' && onRejected(reason) || reason; reject(rea); // 同resolve } if (promise.status === 'PENDING') { promise.resolves.push(success); promise.rejects.push(erro); } else if (promise.status === 'FULFILLED') { success(promise.value); } else if (promise.status === 'REJECTED') { erro(promise.reason); } }); }; 複製程式碼
在成功的函式中還需要做一個處理,用以支援在then的回撥函式(onFulfilled)中return的Promise。如果onFulfilled方法return的是一個Promise,則直接執行它的then方法。如果成功了,就繼續執行後面的then鏈,失敗了直接呼叫reject。
function success(value) { let val = typeof onFulfilled === 'function' && onFulfilled(value) || value; if(val && typeof val['then'] === 'function'){ // 判斷是否有then方法 val.then(function(value){ // 如果返回的是Promise 則直接執行得到結果後再呼叫後面的then方法 resolve(value); },function(reason){ reject(reason); }); }else{ resolve(val); } } 複製程式碼
找個例子測試一下
function getInfo(success, fail) { return new Promise((resolve, reject) => { setTimeout(_ => { let ran = Math.random(); console.log(success, ran); if (ran > 0.5) { resolve(success); } else { reject(fail); } }, 200); }) } getInfo('Vchat', 'fail').then(res => { console.log(res); return getInfo('可以線上預覽了', 'erro'); }, rej => { console.log(rej); }).then(res => { console.log(res); }, rej => { console.log(rej); }); // 輸出 // Vchat 0.8914818954810422 // Vchat // 可以線上預覽了 0.03702367800412443 // erro 複製程式碼
總結
到這裡,Promise的主要功能基本上都實現了。還有很多實用的擴充套件,我們也可以新增。 比如 catch可以看做then的一個語法糖,只有onRejected回撥的then方法。其它Promise的方法,比如.all、.race 等等,感興趣的小夥伴可以自己實現一下。另外,文中如有不對之處,還請指出。
Promise.prototype.catch = function(onRejected){
return this.then(null, onRejected);
}
複製程式碼
相關文章
交流群
qq前端交流群:960807765,歡迎各種技術交流,期待你的加入
公眾號
歡迎關注公眾號 前端發動機,江三瘋的前端二三事,專注技術,也會時常迷糊。希望在未來的前端路上,與你一同成長。