- 摘要
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了,编程技术确实在进步,固守传统简单的编程技术是没有前途的,我们还是要勇于学习进步,这些源代码的作者他们是真正的编程高手大师。