es6-promise源代码重点难点分析
- 摘要
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了,编程技术确实在进步,固守传统简单的编程技术是没有前途的,我们还是要勇于学习进步,这些源代码的作者他们是真正的编程高手大师。
es6-promise源代码重点难点分析的更多相关文章
- AXIOS源代码重点难点分析
摘要 vue使用axios进行http通讯,类似jquery/ajax的作用,类似angular http的作用,axios功能强大,使用方便,是一个优秀的http软件,本文旨在分享axios源代码重 ...
- vue 2.0 路由切换以及组件缓存源代码重点难点分析
摘要 关于vue 2.0源代码分析,已经有不少文档分析功能代码段比如watcher,history,vnode等,但没有一个是分析重点难点的,没有一个是分析大命题的,比如执行router.push之后 ...
- react源代码重点难点分析
网上已经有不少react源码分析文档,但都是分析主流程和主要功能函数,没有一个是从reactDOM.render()入口开始分析源码把流程走通尤其是把复杂重要的细节环节走通直到把组件template编 ...
- apache 重点难点
apache 重点难点 在于难以理解其工作原理,因为它是c 写的:其模块众多:功能强大而复杂. 其配置也是格式不齐, 比如一下子是 key value , 一下子就成了 xml. 转载: http:/ ...
- Android开发重点难点1:RelativeLayout(相对布局)详解
前言 啦啦啦~博主又推出了一个新的系列啦~ 之前的Android开发系列主要以完成实验的过程为主,经常会综合许多知识来写,所以难免会有知识点的交杂,给人一种混乱的感觉. 所以博主推出“重点难点”系列, ...
- 通过 ES6 Promise 和 jQuery Deferred 的异同学习 Promise
Deferred 和 Promise ES6 和 jQuery 都有 Deffered 和 Promise,但是略有不同.不过它们的作用可以简单的用两句话来描述 Deffered 触发 resolve ...
- ES6 Promise 接口
构造函数 new Promise(function(resolve, reject){}); 构造函数接受一个函数(executor)作为参数,该函数在返回 Promise 实例之前被调用.函数的两个 ...
- Es6 Promise 用法详解
Promise是什么?? 打印出来看看 console.dir(Promise) 这么一看就明白了,Promise是一个构造函数,自己身上有all.reject.resolve这几个眼熟的方 ...
- 第213天:12个HTML和CSS必须知道的重点难点问题
12个HTML和CSS必须知道的重点难点问题 这12个问题,基本上就是HTML和CSS基础中的重点个难点了,也是必须要弄清楚的基本问题,其中定位的绝对定位和相对定位到底相对什么定位?这个还是容易被忽视 ...
随机推荐
- ZJOI2018游记
我是一只普及组的菜鸡,我很菜 我参加 \(ZJOI\) 只是来试试水(水好深啊~),看看大佬(差距好大啊~),以后要好好学习 \(day0\) 下午2:00,颁奖 还以为要到很晚,还是挺快的 \(da ...
- [福大软工] W班 团队第一次作业—团队展示成绩公布
作业地址 https://edu.cnblogs.com/campus/fzu/FZUSoftwareEngineering1715W/homework/906 作业要求 根据已经组队的队伍组成, 每 ...
- 网络1712--c语言字符数组作业总结..
---恢复内容开始--- 作业亮点 1.总体情况 1.大部分同学利用了流程图后,对于思路的理解有了提升. 2.很多同学在总结方面写的很不错,能够罗列问题贴出解决问题,我们能够看到你们的进步 2.作业发 ...
- hibernate框架学习错误集锦-org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL)
最近学习ssh框架,总是出现这问题,后查证是没有开启事务. 如果采用注解方式,直接在业务层加@Transactional 并引入import org.springframework.transacti ...
- 算法——算法时间复杂度的计算和大O阶的推导
在算法分析中,我们将语句总的执行次数记为T(n)进而分析T(n)随n的变化情况确认T(n)的数量级.一般情况下,T(n)随n增大变化最缓慢的算法为最优算法. 根据定义,T(n)的求法是很简单的,也就是 ...
- 开始 Python 之旅
开始 Python 之旅 课程来源 本课程基于 Python for you and me 教程翻译制作,其中参考了 Python tutorial 和 The Python Standard Lib ...
- Android类加载机制及热修复实现
Android类加载机制 Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中.而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进 ...
- sql 多条记录插入
--多条记录插入,用逗号分开值. INSERT dbo.studentinfor ( id, name, class, age, hpsw ) ', -- id - nvarchar(50) N'te ...
- javascript中的数组对象
1.创建数组的三种方式: 1.1 var 数组名=[元素1,元素2,元素3...]; 例如: var arr1=[1,2,3,4]; 1.2 var 数组名=new Array(元素1,元素2,元素3 ...
- c# 字符串的内存分配和驻留池( 转 )
刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: string s1 = "he ...