我的開源項目 — Vchat 正式上線了,歡迎各位小哥哥小姐姐體驗。若是以爲還行的話,記得給個star喲 ^_^。javascript
衆所周知,js是單線程異步機制的。這樣就會致使不少異步處理會嵌套不少的回調函數,最爲常見的就是ajax請求,咱們須要等請求結果返回後再進行某些操做。如:html
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,不就什麼都懂了。java
其實網絡上關於Promise的文章有不少,我也查閱了一些相關文章,文末有給出相關原文連接。因此本文側重點是我在實現Promise過程當中的思路以及我的的一些理解,有感興趣的小夥伴能夠一塊兒交流。ios
若是用promise實現上面的ajax,大概是這個效果:git
ajax().success().fail();
複製代碼
那麼什麼是Promise呢?es6
基本用法:github
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
複製代碼
先定個小目標,而後一步步實現它。面試
基礎構造ajax
首先須要瞭解一下基本原理。我第一次接觸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,歡迎各類技術交流,期待你的加入
歡迎關注公衆號 前端發動機,江三瘋的前端二三事,專一技術,也會時常迷糊。但願在將來的前端路上,與你一同成長。