jQuery源码学习:Deferred Object

本文所有讨论均基于jQuery版本3.1.1,官网http://jquery.com/

一.Deferred Object

1. 简介和创建

详见API:http://api.jquery.com/jQuery.Deferred/jQuery Deferred是”based onCommonJS Promises/A design”,并不完全等同于ES6的Promise,或者浏览器/JS引擎实现的原生Promise,或是各类Promise库。

jQuery.Deferred()工厂函数创建一个新的deferred object并且返回这个deferred:

jQuery.extend({
    Deferred: function (func) {
        var deferred = {};
              // ...
             // 实现deferred object的对象方法
             // ...
         return deferred;
    }
})

2. deferred与闭包

由于deferred的每个对象方法method都访问了外部函数jQuery.Deferred()中的局部变量,因此jQuery.Deferred()在执行完毕后,其活动对象activation object不会被销毁,而是被保存在deferred的每个method的[[Scope]]属性中(仅保存指针)。这样deferred的method在执行时就可以通过作用域链scope chain访问到deferred的“私有变量”(参见JavaScript高级程序设计第三版7.4章)。简而言之,deferred的每个method都是jQuery.Deferred()的内部函数。

注意这里的“私有变量”不是静态的,即每次通过调用jQuery.Deferred()得到的deferred都有自己的一份“私有变量”。

注1:这是在jQuery“块级作用域”(参见Javascript高级程序设计第三版7.3章)里定义的局部变量:

var
    version = "3.1.1",
    // Define a local copy of jQuery
    jQuery = function (selector, context) {...}, 

注2:这是在jQuery“块级作用域”里,通过下面的语句expose到全局环境下的,注意注1和注2中的jQuery作用域不同,虽然它们有着同样的名字并指向同样的对象:

    window.jQuery = window.$ = jQuery;  

注3:jQuery.Deferred()返回的deferred object本身是jQuery.Deferred()的一个局部变量。

注4:deferred.promise()返回这个promise object。

3. deferred的私有变量

3.1. state

  • 可取三种值”pending”,”resolved”,”rejected”之一,分别对应Spec里的 pending,fulfilled, or rejected;
  • 可以通过.state()方法读取;
  • 初始状态是”pendinig”。

3.2 promise

可以通过.promise()方法读取。和deferred一样是jQuery.Deferred()的局部变量:

    var promise = {
        ...
        promise: function (obj) {
            return obj != null ? jQuery.extend(obj, promise) : promise;
        }
    },
    deferred = {};  

    ... // 向deferred添加resolve,reject,notify,resolveWith,rejectWith和notifyWith方法  

    // Make the deferred a promise
    promise.promise(deferred); 

从上面jQuery.Deferred()的代码可以看出,deferred对象copy了promise所有的方法;deferred和promise的方法指向相同的函数实例,因而deferred和deferred.promise()拥有同一份“私有变量”

promise没有可以改变state的方法,即promise没有.resolve()/.resolveWith()/.reject()/.rejectWith()/.notify()/.notifyWith()方法。这样返回deferred.promise()给其它代码就可以确保其它代码不会改变deferred的状态或触发回调函数的执行。

3.3 tuples

tuples数组存放了6个callback list object,为讨论方便,我们命名

progress_callbacks = tuples[0][2],   progress_handlers = tuples[0][3]

fulfilled_callbacks = tuples[1][2],       fulfilled_handlers = tuples[1][3]

rejected_callbacks = tuples[2][2],     rejected_handlers = tuples[2][3]

var tuples = [  

        // action, add listener, callbacks,
        // ... .then handlers, argument index, [final state]
        ["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"]
    ],  

3.3.1 callback list

以fulfilled_callbacks即tuples[1][2]为例,它是由jQuery.Callbacks(“once memory”)创建的一个callback list,这个callbacklist的.add()方法把回调函数加入到list中,而.fireWith()方法将导致list里的所有回调函数以类似于”先进先出”的顺序执行。

3.3.2 “once” flag

fulfilled_callbacks创建时带有两个flag:”once”和”memory”。“once”表示这个callback list只能通过.fireWith()或.fire()的调用被fire一次,以后再次调用这个callback list的.fireWith()或.fire()没有任何效果。

注意progress_callbacks和progress_handlers创建时没有”once”这个flag,也就是说这两个callback list可以被多次fire,每次fire时list里的所有回调函数顺序执行。

3.3.3 “memory” flag

callback list在fire时的context和arguments将被“记忆”下来,这样在.fireWith()之后调用.add()的话,新加入的回调函数将马上用“记住”的context和arguments执行。测试如下:

fulfilled_callbacks = jQuery.Callbacks("once memory");
fulfilled_callbacks.add(fn1);
fulfilled_callbacks.add(fn2);
fulfilled_callbacks.fireWith(context, args);   // fn1先执行,即fn1.apply(context,args);然后fn2执行fn2.apply(context,args)
fulfilled_callbacks.fireWith(context, args);   // 没有任何效果
fulfilled_callbacks.add(fn3);                  // fn3马上执行,即fn3.apply(context, args)  

3.4. 对象方法与私有变量


Deferred的对象方法


这个方法会调用


对状态的影响


效果


.done()


fulfilled_callbacks.add()


不改变state


把回调函数加入到fulfilled_callbacks中


.resolve()/.resolveWith()


fulfilled_callbacks.fireWith()


state = “resovled”


fulfilled_callbacks中的所有回调函数被执行注1

fulfilled_handlers中的所有回调函数被执行


.fail()


rejected_callbacks.add()


不改变state


把回调函数加入到rejected_callbacks中


.reject()/.rejectWith()


rejected_callbacks.fireWith()


state = “rejected”


rejected_callbacks中的所有回调函数被执行注1

rejected_handlers中的所有回调函数被执行


.progress()


progress_callbacks.add()


不改变state


把回调函数加入到progress_callbacks中


.notify()/.notifyWith()


progress_callbacks.fireWith()


不改变state


progress_callbacks中的所有回调函数被执行

progress_handlers中的所有回调函数被执行


.then()/.catch()


fulfilled_handlers.add()

rejected_handlers.add()

progress_handlers.add()


不改变state


把回调函数加入到fulfilled_handlers中

把回调函数加入到rejected_handlers中

把回调函数加入到progress_handlers中

注1:这两个方法如果重复执行的话,只有第一次的调用会有效果,原因见3.3.2。

4. 链式调用

除了.state()和.promise()外,deferred的其它方法均返回一个deferred object,这样deferred就可以支持链式调用,例如.always():

always: function () {
    deferred.done(arguments).fail(arguments);
    return this;
},  

调用.always()方法相当于同时向fulfilled_callbacks和rejected_callbacks中添加了一个同样的回调函数,这样无论这个deferred是被.resolve()/.resolveWith()还是.reject()/.rejectWith(),这个回调函数always将被执行。

需要注意的是大多数方法均返回this指针,也就是方法在哪个deferred上被调用,就返回哪个deferred。而.then()和.catch()方法返回的是一个新的deferred的promise

顺便一提,deferred.catch(fn)就是deferred.then(null, fn),所以下面就不单独讨论.catch()方法了。

二.Control Flow

1. 同步执行流程

这里的同步执行流程指的是在同一个event loop里被顺序执行的代码。由于是在一个event loop里单线程运行,这些代码是同步执行的。例如deferred.resolve()/.resolveWith()被调用时,代码总是按照下图中箭头指定的顺序执行:

注1:也就是说,在deferred.resolve()/.resolveWith()之后,再去调用deferred.reject()/.rejectWith()将没有任何效果。反之亦然。

注2:如果.notify()/.notifyWith()被调用过,那么.resolve()/.resolveWith()调用时不会disable掉progress_callbacks.add(),这样仍然可以通过progress()方法加入progress的回调函数,而且这样加入的回调函数会被立即执行。即:

  dfd1 = jQuery.Deferred();
  dfd1.progress(function () {
      console.log("progress1")
  });
  dfd1.notify();                    // console log "progress1"
  dfd1.resolve();
  dfd1.progress(function () {      // console log "progress2" immediately, i.e. before dfd1.progress() returns
      console.log("progress2")
  });  

当然如果.notify()/.notifyWith()没有被调用过,那么.resolve()/.resolveWith()调用后就无法通过.progress()方法加入progress的回调函数了。

2. .then()

2.1 .then()同步执行流程

jQuery.Deferred()调用时可以带一个参数,这个参数指向一个函数func,在jQuery.Deferred()最后返回deferred之前会调用这个函数func。

Deferred: function (func) {
    …
    if (func) {
        func.call(deferred, deferred);
    }  

    // All done!
    return deferred;
},  

.then()在最终返回新的deferredobject(其实是promise)之前,就调用了一个函数func:

then: function (onFulfilled, onRejected, onProgress) {
          …
return jQuery.Deferred(func).promises();
}  

.then()方法的同步执行流程如下:

用文字描述的话,deferred.then(onFulfilled)方法被调用时主要完成了两件“任务”:

  a. 把“包裹”了真正的回调函数onFulfilled的一个函数加入了deferred的“私有变量”fulfilled_handlers当中;

  b. 产生了一个新的deferred object即newDefer,并最终返回了newDefer.promise()。

当然这只是逻辑上的划分,实际运行时.then()是同步执行的:“任务”b的前半段先执行(得到了newDefer),接下来“任务”a执行(需要newDefer作为实参),最后“任务”b的后半段执行,就如上图所示。

2.2 .then()运行时加入到fulfilled_handlers中的函数

在上图右侧代码部分,tuples[1][3]就是fulfilled_handlers,它的.add()方法将这个方法的参数加入到fulfilled_handlers中,也就是把执行resolve()函数得到的返回值加入到fulfilled_handlers中。而resolve()函数返回一个匿名的内部函数,所以,加入到fulfilled_handlers中的函数是执行resolve()函数返回的(resolve()函数的)内部函数。

function resolve(depth, deferred, handler, special) {
    return function () {
            …
    };
}  

请注意这个resolve()函数本身是.then()方法的内部函数,并不是.resolve()方法。

换句话说,.then()运行后,deferred的”私有变量“fulfilled_handlers的“私有变量”list数组的第一个元素list[0]就指向上面这个resolve()返回的匿名函数。这个匿名函数的定义见下一节。

3. 异步执行

3.1 异步的触发

deferred.resolve()/resolveWith()执行时,会调用fulfilled_handlers.fire(),从而运行已加入到fulfilled_handlers中的函数。从上一节可知,此时运行的就是resolve()的内部匿名函数,如下:

function () {
        var that = this,
            args = arguments,
            mightThrow = function () {…},  

            // Only normal processors (resolve) catch and reject exceptions
            process = special ?
                mightThrow :
                function () {
                    try {
                        mightThrow();
                    } catch (e) {…}                  }
                };  

        // Support: Promises/A+ section 2.3.3.3.1
        // https://promisesaplus.com/#point-57
        // Re-resolve promises immediately to dodge false rejection from
        // subsequent errors
        if (depth) {
            process();
        } else {
…
            window.setTimeout(process);
        }
    };
}  

这个函数定义了两个(对于它而言的)内部函数process()和mightThrow(),并在最后调用了window.setTimeout(),这样process()作为定时器的回调函数被异步执行。process()最早也要在下一个event loop才会得到机会运行,而且开始它运行前execution stack是空的,this变量指向window(sloppy mode)或undefined(strict mode),这样process()只有通过闭包才能访问到需要的变量,详见下图。

3.2 异步执行时的作用域链

3.3 process()的执行

3.3.1 Evaluation of handler

deferred.resolve()/resolveWith()执行时,最终会导致process()被异步执行。

procss()首先运行先前.then()调用中传入的handler即上图中的onFulfilled,返回值赋给returned;

    returned = handler.apply(that, args); 

这里handler是.then()调用中传入的onFulfilled,that是.resolveWith()时传入的context,args是.resolveWith()时传入的args,参考上图。

3.3.2 Promise Resolution Procedure

3.3.2.1 Evalution of handler returns non-thenable

process()会执行下面的语句来完成状态传递:

    deferred.resolveWith(that, args);

这里deferred其实是最开始调用.then()所产生的newDefer,that是调用.resolveWith()时传入的context,args是evaluation of handler的结果即returned,参考上图中的作用域链。

例如下面的代码就会走这种分支执行:

    deferred = jQuery.Deferred();
    promise = deferred.then(function (state) {  // state is 1 when this handler is called
        return state + 1;                       // returns new state 2
    });
    deferred.resolve(1);            // process() runs async
    promise.done(function (newState) {    // newState is the value promise is resolved to, i.e. 2 in this case
        console.log(newState)
    });

3.3.2.2 Evaluation of handler returns thenable

这种情况下evalutation of handler返回了一个thenable,也就是有着then方法的对象returned,process()会执行下面的语句:

  then = returned.then;
  then.call(
      returned,
      resolve(maxDepth, deferred, Identity, special),   // Identity_wrapper_handler
      resolve(maxDepth, deferred, Thrower, special),
      resolve(maxDepth, deferred, Identity,
          deferred.notifyWith)
  );  

这里的deferred是最初的.then()调用所产生的newDefer。另外为方便起见,我们把上面这个call()调用中的第二个参数叫做Identity_wrapper_handler。接下去的流程如下。

3.3.2.2.1 当这个thenable即returned被resolve的时候,process()将被异步调用,这是整个流程中process()第二次运行。这次被evaluate的handler是Identity_wrapper_handler。

3.3.2.2.2 Identity_wrapper_handler是调用resolve(maxDepth, deferred, Identity, special)返回的函数,这里maxDepth=1,deferred就是最初的.then()调用所产生的newDefer,可参考上图中的作用域链。运行identity_wrapper_handler会导致process()再次被运行。注意在Identity_wrapper_handler中,process()函数是立即运行而不是通过window.setTimeout(process)异步运行的。

3.3.2.2.3 process()在整个流程中第三次运行,这次被evalutate的handler是Identity函数。Identity函数定义如下:

function Identity(v) {
    return v;
}  

Identity函数运行时的context是returned被resolve时的context,参数是returned被resolve时的值。这样Identity函数运行完毕之后,流程又走回到了3.3.2 Promise Resolution Procedure,这时evalution of handler返回的就是上一个thenalbe被resolve时的值。请注意在这个第三次运行的process()中,闭包中存放的deferred引用指向最初的deferred.then()所产生的newDefer,这样就保证了变化的状态能被传递到正确的deferred object上。

对照规范https://promisesaplus.com/来看,这就是规范中的2.3所描述的从[[Resolve]](promise,x)到[[Resolve]](promise,y)的过程

例如下面的代码就会走这个小节即3.3.2.2的流程:

    deferred = jQuery.Deferred();
    anotherDeferred = jQuery.Deferred();
    fulfillment_handler = function () {
        return anotherDeferred.promise();       // a thenable is returned
    };
    newDefer = deferred.then(fulfillment_handler);
    deferred.resolveWith(context1, [1]);   // process() runs async
    anotherDeferred.resolveWith(context2, [2]);  // another process() runs async, and during the execution of this process() a third process() will run synchronously

在上面代码运行的整个流程中,process()总共运行了三次,每次运行时的部分作用域如下:

scope chain 作用域 变量 process()第一次运行时 process()第二次运行时 process()第三次运行时
[4] jQuery.Deferred activation object deferred deferred** anotherDeferred deferred**
[3] .then activation object maxDepth 0* 0 1*
[2]   resolve activation object   depth 0 0 1
deferred newDefer anotherDeferred.promise().then()所生成并返回的一个deferred object newDefer
handler fulfillment_handler Identity_wrapper_handler Identity
[1]  anonymous activation object   that  context1 context2  context2 
args  [1] [2]  [2] 

* process()第一次和第三次运行时,scope chain[3]其实指向同一个activation object,只不过里面的变量maxDepth从0变成了1。

** 这两个scope chain指针也指向同一个activation object,因为scope chain[3]相同。

另外每次process()运行时,scope chain[5]指向同一个jQuery Closure activation object,scope chain[6]指向同一个Global variable object。

3.3.3 Exceptions

process()在运行handler(evalution of handler)或者调用thenable对象的then方法时,如果发生了异常或者异常被抛出,process()会catch exception并调用newDefer的.rejectWith()方法,这样newDefer就会变成rejected状态,确保了exception也会被传递下去。

3.3.4 Thenable self-resolution

下面的代码会导致newDefer要进入resolved状态就必须等待自己被resolve,这是一个逻辑上的死循环,相当于[[Resolve]](promise,promise)

    deferred = jQuery.Deferred();
    newPromise = deferred.then(function () {
        return newPromise;
    });
    deferred.resolve(1);  // newPromise is never resolved, and jQuery.Deferred exception will be thrown

jQuery对于这种情况在deferred.resolve()时会抛出异常jQuery.Deferred exception: Thenable self-resolution。

三.Deferred的应用

jQuery中ready方法还有ajax等等都用到了deferred object。

1. .ready()

使用jQuery时,类似$( handler )的调用会调用下面的.ready()方法把handler加入到一个名为readyList的deferred object中。

    var readyList = jQuery.Deferred();

    jQuery.fn.ready = function (fn) {

        readyList
            .then(fn)
            .catch(function (error) {
                jQuery.readyException(error);
            });

        return this;
    };

当document的"DOMContentLoaded"事件或者window的"load"事件发生时,jQuery注册的回调函数运行,最终会resolve readyList:

    readyList.resolveWith(document, [jQuery]);

这样readyList上的handler就会运行。一些细节的讨论:

  • ready handler运行时的context是document,参数是jQuery,这样可以把ready handler写成function ( $ ) { ... }来安全地使用$代替jQuery。
  • 如果jQuery的js文件是在"DOMContentLoaded"或”load"事件之后动态加载的,也就是说jQuery注册的回调函数错过了这两个事件怎么办呢?jQuery提供了一个工具方法jQuery.ready(),调用这个方法就可以resolve readyList。实际上jQuery注册的事件回调函数也是调用jQuery.ready()来resolve readyList的。
  • .ready()中之所以用.then()而不用.done()把handler加入到readList,是因为.then()不仅可以传递状态,而且handler运行中的exception会被catch,通过rejected deferred/promise传给后面的.catch()。.catch()调用了jQuery.readyException()异步抛出了handler抛出的error。也就是说,如果用户编写的ready handler在运行时发生了错误,那么jQuery将异步抛出exception,这样不会影响接下去其它ready handler或代码同步运行。

以上是.ready()的实现,对于jQuery的使用者来说,一般只要直接写$( handler )就可以了,简洁方便但又不失灵活。这也是我认为jQuery最突出的优点。随着ECMA Script的发展和浏览器的进化,十年前jQuery的其它优势比如浏览器的兼容,Sizzle引擎等等已经不那么明显了,但是它的设计和实现细节一直值得学习。

时间: 2024-10-16 22:12:24

jQuery源码学习:Deferred Object的相关文章

jquery源码学习

jQuery 源码学习是对js的能力提升很有帮助的一个方法,废话不说,我们来开始学习啦 我们学习的源码是jquery-2.0.3已经不支持IE6,7,8了,因为可以少学很多hack和兼容的方法. jquery-2.0.3的代码结构如下 首先最外层为一个闭包, 代码执行的最后一句为window.$ = window.jquery = jquery 让闭包中的变量暴露倒全局中. 传参传入window是为了便于压缩 传入undefined是为了undifined被修改,他是window的属性,可以被修

菜鸟的jQuery源码学习笔记(二)

jQuery对象是使用构造函数和原型模式相结合的方式创建的.现在来看看jQuery的原型对象jQuery.prototype: 1 jQuery.fn = jQuery.prototype = { 2 //成员变量和方法 3 } 这里给原型对象起了一个别名叫做jQuery.fn.要注意的是这个jQuery.fn可不是jQuery对象的属性,而是jQuery构造方法本身的属性,它是不会传给它所创建的对象的.如果你在控制台敲$().fn的话输出的结果会是undefined.接下来看看原型对象里面有些

jquery源码学习(一)core部分

这一部分是jquery的核心 jquery的构造器 jquery的核心工具函数 构造器 jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQu

jQuery源码学习笔记:总体架构

1.1.自调用匿名函数: (function( window, undefined ) { // jquery code })(window); 这是一个自调用匿名函数,第一个括号内是一个匿名函数,第二个括号立即执行,传参是window. 1.为什么有自调用匿名函数? 通过定义匿名函数,创建了一个"私有"空间,jQuery必须保证创建的变量不能和导入它的程序发生冲突. 2.为什么传入window? 传入window使得window由全局变量变成局部变量,jQuery访问window时,

jQuery源码学习笔记:构造jQuery对象

3.1源码结构: (function( window, undefined ) { var jQuery = (function() { // 构建jQuery对象 var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context, rootjQuery ); } // jQuery对象原型 jQuery.fn = jQuery.prototype = { constructor:

jQuery源码学习笔记:扩展工具函数

// 扩展工具函数 jQuery.extend({ // http://www.w3school.com.cn/jquery/core_noconflict.asp // 释放$的 jQuery 控制权 // 许多 JavaScript 库使用 $ 作为函数或变量名,jQuery 也一样. // 在 jQuery 中,$ 仅仅是 jQuery 的别名,因此即使不使用 $ 也能保证所有功能性. // 假如我们需要使用 jQuery 之外的另一 JavaScript 库,我们可以通过调用 $.noC

jQuery源码学习笔记五 六 七 八 转

jQuery源码学习笔记五 六 七 八 转 Js代码   <p>在正式深入jQuery的核心功能选择器之前,还有一些方法,基本都是数组方法,用于遴选更具体的需求,如获得某个元素的所有祖选元素啦,等等.接着是其缓存机制data.</p> <pre class="brush:javascript;gutter:false;toolbar:false"> //@author  司徒正美|なさみ|cheng http://www.cnblogs.com/ru

jquery源码学习(二)sizzle部分 【转】

一,sizzle的基本原理 sizzle是jquery选择器引擎模块的名称,早在1.3版本就独立出来,并且被许多其他的js库当做默认的选择器引擎.首先,sizzle最大的特点就是快.那么为什么sizzle当时其他引擎都快了,因为当时其他的引擎都是按照从左到右逐个匹配的方式来进行查找的,而sizzle刚好相反是从右到左找的. 举个简单的例子 “.a .b .c”来说明为什么sizzle比较快.这个例子如果按照从左到右的顺序查找,很明显需要三次遍历过程才能结束,即先在document中查找.a,然后

jquery源码学习-构造函数(2)

最近几天一直在研究jquery源码,由于水平太低看得昏头转向.本来理解的也不是很深刻,下面就用自己的想法来说下jquery是如何定义构造函数初始化的.如果有什么不对的地方,希望个位高手指出.  一般写构造函数如下 function Aaa(){} Aaa.prototype.init = function(){}; Aaa.prototype.css = function(){}; var a1 = new Aaa(); a1.init(); //初始化 a1.css(); jQuery写法如下