前言
在介绍了 jQuery.Callbacks()
后就可以来探索社区版本的 Promise
的实现了,在了解了 jQuery
中的 Promise
实现后,我们对 ES6
的 Promise
规范再使用的时候就会更加的有自己的见解。
但是学习 jQuery
中的实现之前我们需要对照着 ES6
中 promise
的基本使用来学习,这样就可以事半功倍,ES6
中 promise
可以参考阮一峰老师的文档:Promise 对象。
这里强调两个 promise
中需要注意的点:
- promise 有三种状态:
pending
、fulfilled
、rejected
。只有异步操作的结果可以决定是哪一种状态。 - 一旦
promise
的状态绑定就不会再变了。
接下来我们进入源码。
源码中的基础搭建
因为我们要给 jQuery
本身扩展 Deferred
方法,所以就可以使用到之前实现好的 jQuery.extend()
方法进行扩展:
jQuery.extend( { Deferred: function( func ) { // ... } });
之后就可以再该方法内部写代码啦,首先看一下其方法内部的基础搭建,我会将其分解来讲解:
Deferred: function (func) { var tuples = [ // 用元组的形式存储信息:动作、add 方法的语法糖、函数队列容器,状态 ["notify", "progress", jQuery.Callbacks("memory"), jQuery.Callbacks("memory"), 2 ], ["resolve", "done", jQuery.Callbacks("once memory"), jQuery.Callbacks("once memory"), 0, "resolved" ], ["reject", "fail", jQuery.Callbacks("once memory"), jQuery.Callbacks("once memory"), 1, "rejected" ] ], // 状态 [ pending | resolved | rejected ] state = "pending", promise = { state: function () { return state; }, always: function () { }, catch: function () { }, pipe: function () { }, then: function () { }, promise: function (obj) { return obj != null ? jQuery.extend(obj, promise) : promise; } }, deferred = {}; // 遍历 tuples,i 是索引,tuple 是二维数组中的每一个数组,储存每一项的信息 jQuery.each(tuples, function (i, tuple) { // 拿到 每一个容器,一共有三个容器 var list = tuple[2], // 只有两种状态 [ resolved, rejected ] stateString = tuple[5]; // promise.[ progress | done | fail ] = list.add // 也就是说 promise 的 progress done fail 是 list.add 方法的语法糖 promise[tuple[1]] = list.add; // 对成功管理的容器和失败管理的容器添加方法 if (stateString) { list.add( function () { // 添加绑定状态的方法 state = stateString; }, // rejected_callbacks.disable // fulfilled_callbacks.disable tuples[3 - i][2].disable, // rejected_handlers.disable // fulfilled_handlers.disable tuples[3 - i][3].disable, // progress_callbacks.lock tuples[0][2].lock, // progress_handlers.lock tuples[0][3].lock ); } // progress_handlers.fire // fulfilled_handlers.fire // rejected_handlers.fire list.add(tuple[3].fire); // 为 deferred 对象扩展 [ notify | resolve | reject ] 三个方法 deferred[tuple[0]] = function () { // 下面一步的传参是为了进行权限控制 deferred[tuple[0] + "With"](this === deferred ? undefined : this, arguments); return this; }; // 为 deferred 对象扩展 [ notifyWith | resolveWith | rejectWith ] 三个方法,直接拿到 list.fireWith 的引用 deferred[tuple[0] + "With"] = list.fireWith; }); // Make the deferred a promise promise.promise(deferred); // Call given func if any if (func) { func.call(deferred, deferred); } // All done! return deferred; }
看到这里其实基础的 promise
已经搭建好了,这里我们还没有阅读并实现 promise
的 then
、catch
等方法。
分布讲解
首先定义一个 tuples
二维数组和状态 state
:
var tuples = [ ["notify", "progress", jQuery.Callbacks("memory"), jQuery.Callbacks("memory"), 2 ], ["resolve", "done", jQuery.Callbacks("once memory"), jQuery.Callbacks("once memory"), 0, "resolved" ], ["reject", "fail", jQuery.Callbacks("once memory"), jQuery.Callbacks("once memory"), 1, "rejected" ] ], state = "pending";
这一初始化操作中 state
表示的就是当前 promise
的状态。对于 tuples
来说,里面有三个数组,对应管理着三个容器,并且每个数组中记录了管理该容器使用的方法名。
这时候你可能会有疑问:初始化 tuples
为什么 notify
对应的容器和 resolve
、reject
对应的容器的特性不同?
我们可以想到,notify
对应的容器表示进行中,很显然进行中状态时我们可以任意给容器中添加函数。
而对于 resolve
或者 reject
对应的容器来说,由于这时候已经需要执行容器中的所有函数了,重复的执行同一个函数是没有意义的,所以添加特性 once
。
那么为什么它们对应的容器都需要添加特性 memory
呢?原因也很简单,我们的 promise
是可以进行链式的书写回调的,我们可能会发送一个 ajax
请求,但是这个请求必须基于之前的请求的成功才可以发起。那么我们当收到第一个请求的成功时,就会继续向容器中添加第二次请求的函数,这个函数并不需要我们手动再次去执行,而是添加后立即执行,所以三个容器都有 memory
特性。
接下来继续分析源码:
promise = { state: function () { return state; }, always: function () { }, catch: function () { }, pipe: function () { }, then: function () { }, promise: function () { } }, deferred = {};
基础搭建中先不具体给出 always
、catch
、then
这些方法的实现,不过我们已经可以猜想到这个 promise
对象就是我们通常操作的那个 promise
,因为它上面有这些方法。
那么 deferred
对象是用来做什么的呢?我们已经猜不下去了,继续来到源码后发现,我们需要用到之前 tuples
中的容器就必须对其进行遍历:
jQuery.each(tuples, function (i, tuple) { var list = tuple[2], stateString = tuple[5]; promise[tuple[1]] = list.add; if (stateString) { list.add( function () { state = stateString; }, // rejected_callbacks.disable // fulfilled_callbacks.disable tuples[3 - i][2].disable, // rejected_handlers.disable // fulfilled_handlers.disable tuples[3 - i][3].disable, // progress_callbacks.lock tuples[0][2].lock, // progress_handlers.lock tuples[0][3].lock ); } list.add(tuple[3].fire); deferred[tuple[0]] = function () { deferred[tuple[0] + "With"](this === deferred ? undefined : this, arguments); return this; }; deferred[tuple[0] + "With"] = list.fireWith; });
这段源码的开始,我们遍历了 tuples
数组,并且让 list
拿到每一个容器, stateString
拿到容器所对应的状态,只有 resolved
和 reject
两种。
接着我们为 promise
对象扩展了三个方法 progress
、done
、fail
。这三个方法直接拿到为容器中添加方法的函数的引用,所以这三个方法其实是语法糖,目的就是为它们所对应的容器中添加回调函数。
之后我们向成功和失败所对应的容器添加一个回调函数用于状态的绑定。
之后我们为 deferred
对象分别添加了 notify
、resolve
、reject
这三个方法,内部其实是调用了 deferred
对象的 notifyWith
、resolveWith
、rejectWith
这三个方法并且传递了 this
指向调用者和对应的参数。最终返回 this
用于链式调用。
最后我们为 deferred
对象添加 notifyWith
、resolveWith
、rejectWith
这三个方法,让它们拿到依次执行对应容器中函数的方法的引用。这里其实就可以看作是一个语法糖。
我们的核心代码就要来了:
promise.promise( deferred );
这里调用了 promise
对象上的 promise
方法并传入当前的 deferred
对象。所以我们回到 promise
对象中看 promise
方法的实现:
promise = { promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }
在源码内部,默认这个方法就会执行,此时的参数obj就是对象 deferred
,方法内部会执行jQuery.extend( obj, promise )
并且返回 deferred
对象。 所以这时候我们知道 deferred
对象身上不仅拥有遍历时的 resolve
、resolveWith
等六个执行对应容器中方法的方法,还具有 promise
对象身上的为对应容器中添加回调函数的方法 done
、fail
、progress
。
promise
对象和 deferred
对象的区别
看到这里,我们已经知道了:
- promise 对象拥有为对应容器添加回调函数的操作。
- deferred 对象拥有执行对应容器中所有回调函数和向容器中添加回调函数的操作。
那么我们为什么要有 promise
对象呢?直接都使用 deferred
不好吗?它的功能不是已经够了吗?
实际上多一个 promise
对象是为了做一层权限管理。我们试想如果用户这么使用这个接口:
var def = jQuery.Deferred(); def.resolve(); def.reject();
如果没有 promise
做权限控制,那么这样使用状态不就绑定后可以修改了吗?本文的一开始就已经说明一旦 promise
的状态绑定就不会再变了。所以就要求我们的调用者这样使用接口:
function wait() { var def = jQuery.Deferred(); setTimeout(() => { def.resolve(); }, 1000); return def.promise(); // 这里没有传参,所以会返回源码内部的 promise 对象 } const promise = wait(); promise.done(eat); function eat() { console.log('eat'); // eat console.log(promise.state()); // resolved }
浏览器执行的时候就会在一秒后打印出 eat
。这样在 wait
函数的外部我们只有 promise
对象,而这个对象本身就没有可以直接调用容器中回调函数的方法,也就不可能去改变当前的状态。这就是权限管理。
我们可以打印出源码中的 deferred
和 promise
对象 :
function wait() { var def = jQuery.Deferred(); console.log(def); def.resolve(); // 状态绑定 return def.promise(); } console.log(wait());
可以看到运行结果:
到这里 promise 的基础搭建就已经完成了。我们可以简单使用一下:
function wait() { var def = jQuery.Deferred(); setTimeout(() => { def.reject(); }, 1000); return def.promise(); } const promise = wait(); promise.done(function () { console.log('resolved'); }).fail(function () { console.log('rejected'); });
这里只会打印出 rejected
,分析一下它的原理:
- 首先执行
jQuery.Deferred()
返回deferred
对象,一秒后绑定状态为rejected
,wait
函数内部返回promise
对象。 - 调用
promise.done(fn1).fail(fn2)
后,会向成功的容器中添加回调函数fn1
,向失败的容器中添加回调函数fn2
。并且应当立即执行,因为创建的容器具有memory
这一特性。 - 由于调用了
def.reject()
所以会依次执行失败所管理的容器中的回调函数,这里有两个函数,一个用于状态绑定为rejected
另一个则是新添加的fn2
函数。
所以最终只执行了 fail
中的函数打印出了 rejected
。