/ jQuery

JQuery那点事 -- Deferred(Promise的实现)

注:由于 Deferred 需要用到 Callback 因此需要先了解 Callback 的实现以及用处后才能进一步了解 Deferred 的实现及其作用。
附:JQuery那点事 -- Callback

Deferred (延迟对象)解决了什么

在写 javascript 代码时,难免的回写到异步回调。
当一个回调中含有另一个回调时,假设我们要获取一个坐标点,x 需要异步获取,而 y 轴坐标需要依赖 x 轴,z 轴需要依赖 y 轴,那么写起来的代码会是这样:

var x = 10;

function getX(cb) {
    setTimeout(function() {
        cb && cb(10);
    })
}

// 假设对X做的操作仅仅是 +1
function getY(x, cb) {
    setTimeout(function() {
        cb && cb(x + 1);
    })
}

// 假设对Y做的操作仅仅是 -5
function getZ(y, cb) {
    setTimeout(function() {
        cb && cb(y - 5);
    })
}

getX(function(result1) {
    var x = result1;
    getY(x, function(result2) {
        var y = result2;
        getZ(y, function(result3) {
            var z = result3;
            var point = { x: x, y: y, z: z };
            doSomeThing(point);
        })
    })
})

这种结构有缩进看着好像也就那么回事,但是没缩进呢?

getX(function(result1){
var a = result1;
getY(a.x, function(result2){
var b = result2;
getZ(b.y, function(result3){
var c = result3;
var point = {x: a.x, y: b.y, z: c.z};
doSomeThing(point);
})
})
})

真是一坨啊,而且这还是仅仅3个回调,不仅语义不明确,而且可读性极低。而 Deferred 的出现就解决了这个问题。

下面就是同样的例子,使用 Deferred 实现:

var x = 10;
var point = {};

function getX() {
    var defer = $.Deferred();
    setTimeout(function() {
        point.x = x;
        defer.resolve(x);
    })
    return defer.promise();
}

function getY(x) {
    var defer = $.Deferred();
    setTimeout(function() {
        point.y = x + 1;
        defer.resolve(x + 1);
    })
    return defer.promise();
}

function getZ(y) {
    var defer = $.Deferred();
    setTimeout(function() {
        point.z = y - 5;
        defer.resolve(y - 5);
    })
    return defer.promise();
}

getX().then(getY).then(getZ).then(function() {
    doSomeThing(point);
});

逻辑写在了函数的内部,不需要更多的回调来处理数据,而且即使在缩进错误的情况下,也毫无问题。

Deferred 都做了什么?

Deferred

Deferred 的3个重要的方法在图上都显现了,下面介绍下这3个方法都做了什么

  • promise.then(fun1,fun2)

fun1 处理后添加到 resolveCallbacks 中,将 fun2 处理后添加到 rejectCallbacks 中,当 promise 状态改变时,也就是 resolve 或是 reject 方法执行后,执行对应的 Callbacks,一般 reject 处理错误数据, resolve 处理正确数据。

  • defer.resolve(obj)

通知 promise 处理 obj 数据,使用 fun1 处理

  • defer.reject(err)

通知 promise 处理 err 数据,使用 fun2 处理

简单实现

以下代码为 jQuery 3.1.0 中,关于 Deferred 源码中的一部分。经过我的一些修改,仅仅实现了主要的逻辑。
本着足够精简的态度去提取,但经过经过了一遍又一遍的考虑后,也还剩下100多行的代码。

由于代码量感觉略大,这里就不放上来了,具体的精简代码 点击该链接查看

其实看看还是能在精简点的,但是在精简下去就要改变 JQuery 中源码的编写方式,不利于理解 JQuery 源码了 。

具体的代码执行过程,在代码里都有,这里上个图来说明下,一个 Deferred 从创建到 then 执行,到返回新的 Deferred 的过程:

Deferred流程图

配合思维导图来看,应该更能理解:

Deferred思维导图

JQ 中的实现

当然在 JQueryDeferred 的实现远比上面的简单实现要来的复杂。但主体的逻辑和上面已经没有区别了。

下面说说主要区别有哪些地方

  1. resolvereject 各自分别实现了两个 Callbacks,这两个 Callback 其中一个用于 Deferred 内部,一个用于 then 方法调用时,这样的结构使得代码管理起来更容易。
  2. 功能性函数的添加,这个在下面一节中细说。
  3. 实现了 3 种状态,除了 resolvereject 外还实现了 notify 的状态。
  4. 处理了 Deferred 实际允许过程种,由于特殊的写法而导致的一些意料之外的 BUG

notify 的作用

notify 用于提供 Deferred 的一些状态信息,比如当前的 Deferred 的完成百分比,以及 ajax 的状态码的改变··· 但是 notify 和其他两种状态并不互斥,而其他两种的改变也不会影响 notify 状态。

功能性函数

  • .done(fun) | .fail(fun) | .progress(fun)

这3个方法分别对应 resolve | reject | notify 这3种状态下在 Deferred 内部使用的 Callbacks.add 方法,用于在相应状态时,执行我们传入的函数。相当于对外暴露了 Callbacks.add 方法。

  • .state()

用于获取 Deferred 的状态。

  • .always()

.done(arguments).fail(arguments) 的变体,表示 Deferred 变为 resolve | reject 状态都要执行的函数。

  • .catch()

.then(null, fn) 的变体,表示仅仅配置 reject 需要执行的函数。(通常用于捕获异常)

  • $.when(promises)

当传入的 promises 全部运行成功后,才调用 resolved 状态的回调函数,如果其中一个 promise 失败,就调用 rejected 状态的回调函数。

JQuery 源码(含注释)

点击查看