- 摘要
vue和axios都可以使用es6-promise來實現f1().then(f2).then(f3)這樣的連寫形式,es6-promise其實現代瀏覽器已經支持,無需加載外部文件。由於promise寫法明顯由於傳統寫法,已經越來越被高級程序采用,不懂promise就沒法看高級程序。
- es6-promise源代碼重點難點分析
本文以axios中的http request源代碼為例來分析es6-promise源代碼中最復雜深奧難懂的環節,當異步過程嵌套時,代碼還是很復雜的,有點超出想象,如果用ajax來實現,還真不太好寫。
通常用promise寫代碼是這樣寫的,比如:
function show(){
return new Promise((resolve) => {
bus.$on('optionClickedEvent', (data) => {
resolve(data.optionIndex) //resolve的目的是要執行then(fn)
this._hide()
})
})
show().then(function(index){
});
這就是一個異步過程完成之后就執行下一個callback回調並傳遞參數,這是典型的最簡單的寫法。
首先來看promise構造函數代碼:
function Promise(resolver) {
initializePromise(this, resolver)
function initializePromise(promise, resolver) {
try {
resolver(function resolvePromise(value) {
_resolve(promise, value);
}, function rejectPromise(reason) {
_reject(promise, reason);
調promise時傳遞一個resolve方法,它會執行resolve方法,傳遞兩個fn,resolve方法是綁定一個事件,事件觸發handler函數執行,
handler函數調用fn,傳遞事件數據,fn再調用內部_resolve方法,繼續傳遞數據value(data.optionIndex)。
function _resolve(promise, value) { //這個就是es6-promise提供的resolve()方法
if (promise === value) {
_reject(promise, selfFulfillment());
} else if (objectOrFunction(value)) {
handleMaybeThenable(promise, value, getThen(value));
} else {
fulfill(promise, value);
}
function fulfill(promise, value) {
promise._result = value; //傳遞的數據保存在promise實例
asap(publish, promise);
}
resolve調用asap:
var asap = function asap(callback, arg) {
queue[len] = callback; //傳遞的方法保存在queue
queue[len + 1] = arg; //promise實例保存在queue,里面有傳遞的數據value
len += 2;
if (len === 2) {
if (customSchedulerFn) {
customSchedulerFn(flush);
} else {
scheduleFlush();
異步延遲方法有以下幾種:
if (isNode) { //debug看是false
scheduleFlush = useNextTick();
} else if (BrowserMutationObserver) { //debug看有此方法,類似setTimeout,是異步延遲調度
scheduleFlush = useMutationObserver(); //執行seMutationObserver()會返回一個方法
} else if (isWorker) { //debug看是false
scheduleFlush = useMessageChannel();
} else if (browserWindow === undefined && typeof require === 'function') { //debug看都有
scheduleFlush = attemptVertx();
} else {
scheduleFlush = useSetTimeout(); //就是setTimeout方法
}
function useSetTimeout() {
var globalSetTimeout = setTimeout;
return function () {
return globalSetTimeout(flush, 1);
};
function useMutationObserver() {
var iterations = 0;
var observer = new BrowserMutationObserver(flush); //flush就是callback,用observer調度執行
var node = document.createTextNode('');
observer.observe(node, { characterData: true }); //告訴observer觀察屬性
return function () { //這就是scheduleFlush方法
node.data = iterations = ++iterations % 2; //人為修改屬性觸發observer執行callback
};
}
var queue = new Array(1000);
function flush() { //讓observer異步調度執行的callback方法
for (var i = 0; i < len; i += 2) {
var callback = queue[i];
var arg = queue[i + 1];
callback(arg); //執行隊列里面的方法,參數也從隊列里面取,就是publish(promise),傳遞的數據已經保存在promise實例中
queue[i] = undefined;
queue[i + 1] = undefined;
}
len = 0;
}
執行scheduleFlush方法就是修改屬性觸發observer調度執行callback,相關數據對象之前已經准備好了。
另外一種寫法是:new MutationObserver(callback);
所以異步調度執行除了setTimeout之外,還有observer,意思是一樣的,但內部實現機制不同,setTimeout是延遲機制,
observer是DOM元素變化事件觸發機制,一般用不着observer,因為一般都是數據變化要同步更新到DOM,而不是DOM有變化
要同步更新到數據,DOM一般不會主動變化,DOM的變化一般都是數據變化同步更新過去的。
再回頭看傳遞給asap存儲在queue中要調度執行的callback方法如下:
function publish(promise) {
var subscribers = promise._subscribers;
var settled = promise._state;
if (subscribers.length === 0) {
return;
}
var child = undefined,
callback = undefined,
detail = promise._result; //_result就是執行resolve()時傳遞的數據(保存在promise實例中)
for (var i = 0; i < subscribers.length; i += 3) {
child = subscribers[i];
callback = subscribers[i + settled];
if (child) {
invokeCallback(settled, child, callback, detail);
} else {
callback(detail); //這是執行then(handler)方法並且傳遞數據,數據是之前保存在promise實例中的
}
}
promise._subscribers.length = 0;
}
是從promise實例中取subscribers[],再從中取數據方法執行,由於執行resolve就是為了執行then(fn),因此執行then(fn)
時會調用subscribe方法把fn存儲在subscribers[]中,subscribers[]相當於events[],存儲handler。
下面看subscribers[]是如何創建的;
function then(onFulfillment, onRejection) { // 傳入f1/f2兩個handler
var parent = this;
var child = new this.constructor(noop);
subscribe(parent, child, onFulfillment, onRejection); //調subscribe存儲handler。
return child;
可見then會返回一個promise實例,因此可以連寫比如show().then(fn).then(fn),因為可以層層嵌套,parent就是then所在的
promise實例,child是返回的promise實例,也就是下一級then所在的promise實例。
function subscribe(parent, child, onFulfillment, onRejection) {
var _subscribers = parent._subscribers;
var length = _subscribers.length;
parent._onerror = null;
_subscribers[length] = child;
_subscribers[length + FULFILLED] = onFulfillment;
_subscribers[length + REJECTED] = onRejection;
if (length === 0 && parent._state) {
asap(publish, parent);
}
}
可見會把handler存儲在then所在的promis實例中的_subscribers[]中,事件訂閱者與handler是一類意思。
可見promise就是形式上寫了一個事件機制,實際上幾乎就是順序執行,show() -> then(handler) -> 事件觸發 -> resolve -> handler 應用代碼綁定了一個事件,事件觸發resolve執行。
如果show()是一個axios.get過程,那么事件就是http響應事件,handler就是http回調。
axios.get().then(function(res){
//http request有響應有返回
},function(){
//http request無響應/網絡異常
});
axios.get方法:
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
Axios.prototype.request = function request(config) {
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config); //相當於new promise實例而且會執行resolve傳遞config數據給dispatchrequest
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift()); //then(fn)會立即執行
}
return promise; //只有resolve這個promise才能傳遞response數據到axios.get.then(callback)
};
chain[0][1]是request攔截函數,[2]是dispatchrequest,[3][4]是response攔截函數。
每次axios.get請求都會執行一遍這段代碼,把chain里面的handler都執行一遍,其中有dispatchrequest,因此會執行
http request過程,promise.then會反復執行,每次執行都會返回一個promise實例,最后一次執行時返回的promise實例做為
axios.get.then的promise實例,那么http request過程如何resolve這個promise實例,執行then()回調函數?
function dispatchRequest(config) {
return adapter(config).then(function onAdapterResolution(response) {
return response; //執行handler返回response數據如何返回到axios.get.then(fn)?
}, function onAdapterRejection(reason) {
return Promise.reject(reason);
});
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
request[loadEvent] = function handleLoad() {
settle(resolve, reject, response); //http request請求響應返回之后執行settle
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response); //resolve new promise實例,傳遞response data
} else {
then把handler存儲起來,resolve執行存儲的handler,並傳遞數據,問題是執行handler返回response數據有何用?
實際上是promise嵌套寫法:
Promise.resolve(config).then(function dispatchrequest(config){
return adapter(config).then(function onAdapterResolution(response) {
return response;
}, function onAdapterRejection(reason) {
return Promise.reject(reason);
});
}
).then(function(res){});
里面那層promise本身的resolve沒問題,問題是里面那層promise的handler返回response如何能返回到外層promise的handler?
測試:
Promise.resolve().then(function(){
return Promise.resolve('hello').then(function (response) {
return response;
});
}
).then(function(res){
console.log(res);
});
結果response數據能傳遞給最后一層handler。
為了能debug,直接運行es6-promise.js文件覆蓋瀏覽器缺省的es6-promise,在es6-promise.js文件末尾加一句執行
polyfill()即可。
從代碼看,要new創建promise實例5次,debug看到的也是5個promise實例。
再看then代碼,看第一次執行then的流程,第一次執行then執行里面的callback時是返回一個promise實例,而執行里層then的
情況不一樣,此時執行里面的callback是返回response數據:
function then(onFulfillment, onRejection) {
var parent = this; // then是promise實例的內置方法,this就是then所在的promise實例
var child = new this.constructor(noop); //新建一個promise實例返回,是下一個then所在的promise實例
if (child[PROMISE_ID] === undefined) {
makePromise(child);
}
var _state = parent._state; //then所在的promise實例的狀態,對於第一個then,它的promise實例是完成狀態
if (_state) {
(function () {
var callback = _arguments[_state - 1];
asap(function () {
return invokeCallback(_state, child, callback, parent._result); //注意傳遞的是要返回的child實例
});
})();
} else {
subscribe(parent, child, onFulfillment, onRejection);
}
return child;
}
function invokeCallback(settled, promise, callback, detail) { //注意promise是then要返回的新建的child實例
if (hasCallback) {
value = tryCatch(callback, detail); //執行外層then里面的callback並獲取callback的返回值(promise實例2),
但當執行里層then里面的callback時返回值是response。
else if (hasCallback && succeeded) { //如果then里面的callback執行成功
_resolve(promise, value); //將要返回的promise實例設置成完成狀態並傳遞callback返回值(promise實例2)
function _resolve(promise, value) {
} else if (objectOrFunction(value)) {
handleMaybeThenable(promise, value, getThen(value));
} else {
fulfill(promise, value);
}
function handleMaybeThenable(promise, maybeThenable, then$$) {
if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
handleOwnThenable(promise, maybeThenable);
function handleOwnThenable(promise, thenable) {
if (thenable._state === FULFILLED) {
fulfill(promise, thenable._result);
promise是外層then要返回的promise實例,在此解決它,傳遞值是里層promise實例2的result值,
也就是執行外層下一個then里面的handler並傳遞數據。
因此執行Promise.resolve().then(callback1)時,一是要返回一個promise實例,因為有可能連寫.then(),二是要resolve返回的
promise實例才能執行后面可能連寫的then(callback2),resolve情況如何取決於callback1的代碼。
如果callback1的代碼是return Promise.resolve().then(callback11),這就嵌套了,就非常復雜,首先,執行這個里層then
會執行callback11,取返回值response,然后resolve(promise,value)解決當前promise實例,會把value保存在當前promise
實例的_result中,因為后面沒有再連寫.then(),所以從這點來說返回當前promise實例其實沒有用處,但對於外層promise是
有用的,里層then返回當前promise實例,按callback1的代碼,這個callback執行結果就是返回這個promise實例,那么就回到
外層第一個then繼續執行,外層then執行callback1獲取到返回值之后,又會把then代碼流程走一遍,但此時由於callback1
返回值是一個promise實例,處理流程有所不同,會取這個promise實例的_result值,再resolve(promise,value),其中promise
就是then本身返回的promise實例(then總是新建一個promise實例返回,再resolve這個實例,從而執行下一個then),這就會
執行下一個then里面的callback2並且傳遞value,因此最后一個then(function(res){}里面的callback能獲取到'hello'數據。
如果寫new Promise(callback1).then(callback2),意思是一樣的,callback1代碼決定第一個promise實例如何解決,
callback2代碼決定如何解決then返回的promise實例,如果后面沒有再連寫then,就無需再寫解決當前promise實例
(then返回的promise實例)的代碼,反之就要寫,連寫then不復雜,嵌套比連寫復雜。
promise代碼的關鍵和難點在於如何resolve返回的promise實例,then需要resolve自己返回的promise實例,依此類推,
如果有嵌套,就更復雜了。
還有一點,就是執行順序/異步問題,then是把callback存儲起來,resolve時會找callback執行,一般是這個邏輯,很顯然,
不能上來就執行then里面的callback。但執行then時會判斷,如果then所在的promise實例已經完成,則會執行callback,
解決then本身返回的promise實例,以便執行到后面可能還有的then。所以then(callback)有可能在執行到resolve時執行,
也可能在執行then本身時就立即執行,取決於then所在的promise實例的狀態,注意then本身返回的promise實例是下一個then
所在的promise實例,換句話說下一個連寫的then就是then本身返回的promise實例的內置方法then,以http為例,.then寫法
超越了jquery的$.ajax寫法,邏輯上非常簡單直觀,但.then寫法的代碼原理其實非常復雜抽象深奧。
再回顧一下axios.get的寫法:
Axios.prototype.request = function request(config) {
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
其實就是Promise.resolve(config).then(攔截函數/request函數).then(function(res){
//http returned
},function(){
//http failed
});
創建promise實例傳遞config,它是用while循環把攔截函數都執行一遍,最后執行request,返回promise實例,
request代碼又寫了一層同樣的嵌套,先完成http,再取response,再返回到外層繼續執行下一個then()里面的
callback,也就是http最終的回調處理函數,代碼設計非常高級精彩。
- 結語
是不是有點暈?
promise從某種程度來說把事情搞復雜了,ajax寫法多簡單,人人分分鍾就會寫,前端框架其實從某種程度來說也是把事情搞得非常復雜,但它們有非常高的價值,還是應該使用它們,什么價值呢?就是應用代碼可以寫得更簡潔更直觀更高級更有檔次,實現應用項目編程的模塊化組件化層次化可復用,相比之下傳統寫法確實太low了,編程技術確實在進步,固守傳統簡單的編程技術是沒有前途的,我們還是要勇於學習進步,這些源代碼的作者他們是真正的編程高手大師。