JS魔法堂:mmDeferred源码剖析
一、前言
avalon.js的影响力愈发强劲,而作为子模块之一的mmDeferred必然成为异步调用模式学习之旅的又一站呢!本文将记录我对mmDeferred的认识,若有纰漏请各位指正,谢谢。项目请见:mmDeferred@github
二、API说明
{Deferred} Deferred({Function|Object} mixin?) ,创建一个Deferred实例,当mixin的类型为Object时,将mixin的属性和函数附加到Deferred实例的Promise实例上。
{String} state() ,获取当前Deferred实例的状态,含三种状态:pending,fulfilled,rejected;转换关系为:pending->fulfilled,pending-rejected。
{Promise} then({Function} resolvefn?, {Function} rejectfn?, {Function} notifyfn?, {Function} ensurefn?) ,向当前的Deferred实例添加四类回调函数,并返回一个新的Promise实例。其中resolvefn是实例状态转换为fulfilled时调用,而rejectfn是实例状态转换为rejected时调用,而notifyfn则相当于Promises/A+规范中的progressHandler一样与实例状态无关只需调用notify函数则调用notifyfn,ensurefn的作用为模拟当前Deferred实例执行resolvefn、rejectfn和notifyfn的finally语句块,无论执行前面三个函数的哪一个均会执行ensurefn。
{Promise} otherwise({Function} rejectfn?) ,向当前的Deferred实例添加rejectfn回调函数,并返回一个新的Promise实例。
{Promise} ensure({Function} ensurefn?) ,向当前的Deferred实例添加ensurefn回调函数,并返回一个新的Promise实例。
{undefined} resolve(...[*]) ,用于触发fulfill回调——也就是触发调用当前Deferred实例的resolvefn函数的请求,仅能调用一次。
{undefined} reject(...[*]) ,用于触发reject回调——也就是触发调用当前Deferred实例的rejectfn函数的请求,仅能调用一次。
{undefined} notify(...[*]) ,用于触发notify回调——也就是触发调用当前Deferred实例的notifyfn函数的请求,能调用多次。
{Promise} Deferred.all(...[Promise]) ,要求传入多个Promise对象,当它们都正常触发时,就执行它的resolve回调。相当于jQuery的when方法,但all更标准,是社区公认的函数。
{Promise} Deferred.any(...[Promise]) ,要求传入多个Promise对象,最先正常触发的Promise对象,将执行它的resolve回调。
三、源码剖析
首先要了解的是mmDeferred中存在Deferred和Promise两个操作集合(两者操作同一个的数据结构实例),Promise用于向实例添加四类回调函数,而Deferred用于发起实例状态变化或触发回调函数调用的操作,并且限制为仅通过Deferred函数返回的为Deferred操作集合,而其他API返回的均为Promise操作集合。
另外,值得注意的有以下几点:
1. mmDeferred在实例状态转换的实现方式上是采取先调用回调函数再修改实例状态的方式;
2. resolve、reject等的实现上并不是统一采用异步调用的方式在执行回调函数,而是当实例已经被添加了回调函数时同步执行回调函数,当未添加回调函数时则发起异步调用,让当前执行的代码块有机会向实例添加回调函数;
3. 不支持以下方式的回调函数晚绑定:
var deferred = Deferred() deferred.resolve() setTimeout(function(){ deferred.promise.then(function(){ console.log('hello world') }) }, 0)
在代码结构上值得注意的是:
1. 利用JS中变量声明自动提升(hoist)的特性,通过前置return语句将对外接口与具体实现的代码分离。
2. 提取resolve、reject等函数的共性到私有函数_fire中,提供then、otherwise等函数的共性到私有函数_post中,提供Deferred.all和Deferred.any的共性到私有函数some中,避免重复代码从而大大减少代码量。
待改进点我觉得应该将_fire和_post函数移出至Deferred函数之外,通过入参取代闭包引用外部变量的方式来获取和修改实例属性,那么每次调用Deferred函数时就不会重新声明新的_fire和_post函数了。
存在疑惑的地方为:
假设当前实例A状态为pending,那么执行notify回调函数后当前实例A的状态是不变的,当后续执行的ensure函数抛出异常,那么将调用链表中下一个实例B的reject方法导致实例B的状态为rejected,但实例A状态依然为pending。这时再次调用实例B的resolve或reject方法均不会触发执行相应的回调函数,但可通过调用实例A的resovle或reject方法执行实例A和实例B相应的回调函数。
下面是源码
define("mmDeferred", ["avalon"], function(avalon) { var noop = function() { } function Deferred(mixin) { var state = "pending" // 标识是否已经添加了回调函数 , dirty = false function ok(x) { state = "fulfilled" return x } function ng(e) { state = "rejected" // 将异常往后传递 throw e } // Deferred实例 var dfd = { callback: { resolve: ok, reject: ng, notify: noop, ensure: noop }, dirty: function() { return dirty }, state: function() { return state }, promise: { then: function() { return _post.apply(null, arguments) }, otherwise: function(onReject) { return _post(0, onReject) }, ensure: function(onEnsure) { return _post(0, 0, 0, onEnsure) }, _next: null } } if (typeof mixin === "function") { mixin(dfd.promise) } else if (mixin && typeof mixin === "object") { for (var i in mixin) { if (!dfd.promise[i]) { dfd.promise[i] = mixin[i] } } } "resolve,reject,notify".replace(/\w+/g, function(method) { dfd[method] = function() { var that = this, args = arguments if (that.dirty()) { // 若已经添加了回调函数,则马上同步调用 _fire.call(that, method, args) } else { // 若未添加回调函数,则发起异步调用,让当前代码块的后续部分有足够的时间添加回调函数 Deferred.nextTick(function() { _fire.call(that, method, args) }) } } }) return dfd /** 精彩之处: * 由于JS会将变量声明自动提升(hoist)到代码块的头部 * 因此这里将私有方法写在return语句之后从而更好地格式化代码结构 */ // 添加回调函数到当前Deferred实例上 function _post() { var index = -1, fns = arguments; "resolve,reject,notify,ensure".replace(/\w+/g, function(method) { var fn = fns[++index]; if (typeof fn === "function") { dirty = true if (method === "resolve" || method === "reject") { // 将修改Deferred实例状态的功能封装到回调函数中 // 也就是先调用回到函数再修改实例状态 dfd.callback[method] = function() { try { var value = fn.apply(this, arguments) state = "fulfilled" return value } catch (err) { state = "rejected" return err } } } else { dfd.callback[method] = fn; } } }) // 创建链表的下一个Deferred实例 var deferred = dfd.promise._next = Deferred(mixin) return deferred.promise; } function _fire(method, array) { var next = "resolve", value if (this.state() === "pending" || method === "notify") { var fn = this.callback[method] try { value = fn.apply(this, array); } catch (e) {//处理notify的异常 value = e } if (this.state() === "rejected") { next = "reject" } else if (method === "notify") { next = "notify" } array = [value] } var ensure = this.callback.ensure if (noop !== ensure) { try { ensure.call(this)//模拟finally } catch (e) { next = "reject"; array = [e]; } } var nextDeferred = this.promise._next if (Deferred.isPromise(value)) { // 如果回调函数返回值为Deferred实例,那么就将该实例插入nextDeferred之前 value._next = nextDeferred } else { if (nextDeferred) { _fire.call(nextDeferred, next, array); } } } } window.Deferred = Deferred; Deferred.isPromise = function(obj) { return !!(obj && typeof obj.then === "function"); }; function some(any, promises) { var deferred = Deferred(), n = 0, result = [], end function loop(promise, index) { promise.then(function(ret) { if (!end) { result[index] = ret//保证回调的顺序 n++; if (any || n >= promises.length) { deferred.resolve(any ? ret : result); end = true } } }, function(e) { end = true deferred.reject(e); }) } for (var i = 0, l = promises.length; i < l; i++) { loop(promises[i], i) } return deferred.promise; } Deferred.all = function() { return some(false, arguments) } Deferred.any = function() { return some(true, arguments) } Deferred.nextTick = avalon.nextTick return Deferred })
四、总结
源码中还提供了相关资料的链接,可以让我们更了解Promise/A+规范哦!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4162646.html 肥子John
五、参考
《JavaScript框架设计》
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!