$ 源码分析四:$.Deferred() 社区版 Promise

时间:2021-1-18 作者:admin

前言

在介绍了 jQuery.Callbacks() 后就可以来探索社区版本的 Promise 的实现了,在了解了 jQuery 中的 Promise 实现后,我们对 ES6Promise 规范再使用的时候就会更加的有自己的见解。

但是学习 jQuery 中的实现之前我们需要对照着 ES6promise 的基本使用来学习,这样就可以事半功倍,ES6promise 可以参考阮一峰老师的文档:Promise 对象

这里强调两个 promise 中需要注意的点:

  • promise 有三种状态:pendingfulfilledrejected。只有异步操作的结果可以决定是哪一种状态。
  • 一旦 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 已经搭建好了,这里我们还没有阅读并实现 promisethencatch 等方法。

分布讲解

首先定义一个 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 对应的容器和 resolvereject 对应的容器的特性不同?

我们可以想到,notify 对应的容器表示进行中,很显然进行中状态时我们可以任意给容器中添加函数。

而对于 resolve 或者 reject 对应的容器来说,由于这时候已经需要执行容器中的所有函数了,重复的执行同一个函数是没有意义的,所以添加特性 once

那么为什么它们对应的容器都需要添加特性 memory 呢?原因也很简单,我们的 promise 是可以进行链式的书写回调的,我们可能会发送一个 ajax 请求,但是这个请求必须基于之前的请求的成功才可以发起。那么我们当收到第一个请求的成功时,就会继续向容器中添加第二次请求的函数,这个函数并不需要我们手动再次去执行,而是添加后立即执行,所以三个容器都有 memory 特性。

接下来继续分析源码:

promise = {
  state: function () {
    return state;
  },
  always: function () {

  },
  catch: function () {

  },
  pipe: function () {

  },
  then: function () {

  },
  promise: function () {

  }
},
deferred = {};

基础搭建中先不具体给出 alwayscatchthen 这些方法的实现,不过我们已经可以猜想到这个 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 拿到容器所对应的状态,只有 resolvedreject 两种。

接着我们为 promise 对象扩展了三个方法 progressdonefail。这三个方法直接拿到为容器中添加方法的函数的引用,所以这三个方法其实是语法糖,目的就是为它们所对应的容器中添加回调函数。

之后我们向成功和失败所对应的容器添加一个回调函数用于状态的绑定。

之后我们为 deferred 对象分别添加了 notifyresolvereject 这三个方法,内部其实是调用了 deferred 对象的 notifyWithresolveWithrejectWith 这三个方法并且传递了 this 指向调用者和对应的参数。最终返回 this 用于链式调用。

最后我们为 deferred 对象添加 notifyWithresolveWithrejectWith这三个方法,让它们拿到依次执行对应容器中函数的方法的引用。这里其实就可以看作是一个语法糖。

我们的核心代码就要来了:

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 对象身上不仅拥有遍历时的 resolveresolveWith 等六个执行对应容器中方法的方法,还具有 promise 对象身上的为对应容器中添加回调函数的方法 donefailprogress

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 对象,而这个对象本身就没有可以直接调用容器中回调函数的方法,也就不可能去改变当前的状态。这就是权限管理

我们可以打印出源码中的 deferredpromise 对象 :

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 对象,一秒后绑定状态为 rejectedwait 函数内部返回 promise 对象。
  • 调用 promise.done(fn1).fail(fn2) 后,会向成功的容器中添加回调函数 fn1 ,向失败的容器中添加回调函数 fn2 。并且应当立即执行,因为创建的容器具有 memory 这一特性。
  • 由于调用了 def.reject() 所以会依次执行失败所管理的容器中的回调函数,这里有两个函数,一个用于状态绑定为 rejected 另一个则是新添加的 fn2 函数。

所以最终只执行了 fail 中的函数打印出了 rejected

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。