zoukankan      html  css  js  c++  java
  • jQuery.Deferred(jQuery1.5-2.1)源码剖析

    jQuery.Deferred作为1.5的新特性出现在jQuery上,而jQuery.ajax函数也做了相应的调整。因此我们能如下的使用xhr请求调用,并实现事件处理函数晚绑定。

    var promise = $.getJSON('dummy.js')
    // 其他逻辑处理
    promise.then(function(){
      alert('late binding')
    })

      我还一度以为这就是Promises/A+规范的实现,但其实jQuery.Deferred应该与jsDeferred归为一类,我称之为Before Promises/A。虽然jQuery.Deferred的出现会导致初接触Promise的朋友产生不少的误解,但同时证明了Promises/A+规范的实现已成为开发过程中必不可少的利器了。

      接下来我们会踏上从1.5到2.1版本的jQuery.Deferred实现的剖析之旅,有兴趣的朋友们请坐稳扶好哦!!!

      由于篇幅较长,特设目录一坨!

      二、启程——1.5

      三、觉醒——1.6

      四、全局重构,但本质不变——1.7

      五、又一次靠近Promise/A+规范——1.8

      六、保持现状——1.9&2.1

      七、总结

      八、参考

    二、启程——1.5                        

      jQuery.Deferred 中主要包含三个对象类型Deferred、EnhancedDeferred和Promise,Deferred作为基础类型用于构建更复杂的EnhancedDeferred类型,EnhancedDeferred实例则是用户直接操作的对象,而Promise则是EnhancedDeferred的功能子集,仅提供成功/失败回调函数的订阅、关联的EnhancedDeferred实例的状态查询功能。

      Deferred实例的状态:initialized 、firedcancelled。而状态间的转换关系如下:

         initialized -> fired

         initalized -> cancelled

      EnhancedDeferred实例的状态:initializedresolvedrejected。而状态间的转换关系如下:

      initialized -> resolved

      initialized -> rejected

    (注意:上述类型和类型状态均根据源码分析得出,源码中并没有明确注明)

      1.5的jQuery.Deferred实现位于core.js文件中的,下面我将相关代码抽取并分组来分析。

      1. Deferred实例工厂

    复制代码
    /**
     * Deferred实例工厂
     * Deferred实例实际上就是对一堆回调函数的管理
     */
    $._Deferred = function(){
      // Deferred实例私有属性
       var callbacks = [] // 回调函数队列
       /**
        * 状态标识
        * fired和firing,均用于标识状态"fired"
        *     fired还用于保存调用回调函数队列元素时的this指针和入参,内容格式为:[ctx, args]
        *     firing表示是否正在执行回调函数, 防止并发执行resovleWith函数(主页面和同域或子域iframe子页面可并发调用)
        * cancelled,用于标识状态"cancelled"
        */
       var fired
           ,firing,
           ,cancelled
    
       // Deferred实例
       var deferred  = {
         // 添加回调函数到队列
          done: function(/* args{0,} */) {
            if ( !cancelled ) {
               var args = arguments
                  ,length
                   ,elem
                   ,type
                   ,_fired
                   // 若当前Deferred实例状态为"fired"(曾经调用过resolveWith或resolve方法)
                   // 则使用第一次调用resolveWith或resolve的参数作为入参执行新添加的回调函数
                   if (fired) {
                     _fired = fired;
                      // 将Deferred实例的状态重置为"initialized",后面通过resolveWith函数实现"initialized"->"fired"的状态转换
                      fired = 0;
                   }
                   for (var i = 0, length = args.length; i < length; i++) {
                     elem = args[i]
                      type = $.type(elem)
                      if (type === "array") {
                       // 若该入参为数组则递归添加回调函数
                          deferred.done.apply(deferred, elem)
                      } else if (type === "function") {
                          // 添加回调函数到队列
                          callbacks.push(elem)
                      }
                   }
                   if (_fired) {
                     // 实现"initialized"->"fired"的状态转换
                      // 注意:递归添加回调函数时并不会执行该代码
                      deferred.resolveWith(_fired[0], _fired[1])
                   }
            }
            // 返回当前Deferred对象,形成链式操作
            return this
        },
        /**
         * 发起实现"initialized"->"fired"的状态转换请求
         */
        resolveWith: function(context, /* args{0,} */) {
          if (!cancelled && !fired && !firing) {
             firing = 1 // 状态转换"initialized"->"fired"
             try {
               while(callbacks[0]) {
                  // 以resolveWith的参数作为入参同步调用所有回调函数
                   callbacks.shift().apply(context, args)
                }
             }
             finally {
               fired = [context, args] // 状态转换"initialized"->"fired"
                firing = 0
             }
            }
            return this
        },
        resolve: function() {
          // 当this为deferred时采用Promise实例
           // 当this为failDeferred时采用Deferred实例
           deferred.resolveWith($.isFunction(this.promise) ? this.promise() : this, arguments)
           return this
        },
        isResolved: function() {
          return !!( firing || fired );
        },
        /**
         * 私有方法
         * 将当前Deferred对象的状态设置为"cancelled",并清空回调函数队列
         */
        cancel: function() {
          cancelled = 1;
           callbacks = [];
           return this;
        }}
    
        return deferred
    }
    复制代码

        Deferred实例内部维护着名为callbacks的回调函数队列(而不是Promises/A+规范中的成功/失败事件处理函数和Deferred单向链表)。然后将目光移到done方法,透过其实现可知jQuery.Deferred是支持回调函数晚绑定的(jsDeferred不支持,Promises/A+规范支持),但均以resovleWith的参数作为回调函数的入参,而不是上一个回调函数的返回值作为下一个回调函数的入参来处理,无法形成责任链模式(Promises/A+规范支持)。

      2. 对外API——jQuery.Deferred

    复制代码
    /**
     * 用户使用的jQuery.Deferred API
     * 返回EnhancedDeferred类型实例(加工后的Deferred实例)
     */
    $.Deferred = function(func) {
      /**
        * EnhancedDeferred实例有两个Deferred实例构成
        * 其中deferred代表成功回调函数,failDeferred代表失败回调函数
        * 好玩之处:EnhancedDeferred实例并不是由新类型构建而成,
        *           而是以deferred实例为基础,并将failDeferred融入deferred的扩展方法中构建所得
        */
       var deferred = jQuery._Deferred(), failDeferred = jQuery._Deferred(), promise;
            
       // 将failDeferred融入deferred的扩展方法中
       deferred.fail = failDeferred.done
       deferred.rejectWith = failDeferred.resolveWith
       deferred.reject = failDeferred.resolve
       deferred.isRejected = failDeferred.isResolved
    
       // 辅助方法,一次性添加成功/失败处理函数到各自的Deferred实例的回调函数队列中
       deferred.then = function(doneCallbacks, failCallbacks) {
         deferred.done(doneCallbacks).fail(failCallbacks)
          return this
       }
            
       // 向入参obj添加Deferred实例的方法,使其成为Promise实例
       // 精妙之处:由于这些方法内部均通过闭包特性操作EnhancedDeferred实例的私有属性和方法(而不是通过this指针)
       //           因此即使this指针改变为其他对象依然有效。
       //           也就是promise函数不会产生新的Deferred对象,而是作为另一个操作原EnhancedDeferred实例的视图。
       deferred.promise = function(obj, i /* internal */) {
         if (obj == null) {
            if (promise) return promise
             promise = obj = {}
          }
          i = promiseMethods.length
          while (i--) {
            obj[promiseMethods[i]] = deferred[promiseMethods[i]]
          }
          return obj
      }
            
      // 当调用resolve后,failDeferred的状态从"initialized"转换为"cancelled"
      // 当调用reject后,deferred的状态从"initialized"转换为"cancelled"
      // 因此resolve和reject仅能调用其中一个,同时调用和重复调用均无效
      deferred.then(failDeferred.cancel, deferred.cancel)
      // 将cancel函数转换为私有函数
      delete deferred.cancel
      // 调用工厂方法
      if (func) {
        func.call(deferred, deferred)
      }
      return deferred
    }
    复制代码

       jQuery.Deferred函数返回一个EnhancedDeferred实例,而EnhancedDeferred是以一个管理成功回调函数队列的Deferred实例为基础,并将另一个用于管理失败回调函数队列的Deferred实例作为EnhancedDeferred实例扩展功能的实现提供者,很明显成功、失败回调函数队列是独立管理和执行。

      3. 辅助方法——jQuery.when

        功能就是等待所有入参均返回值后,以这些返回值为入参调用回调队列的函数

    复制代码
    $.when = function(object) {
      var args = arguments, length = args.length, deferred = length <= 1
         && object && $.isFunction(object.promise) ? object
          : jQuery.Deferred(), promise = deferred.promise(), resolveArray;
       if (length > 1) {
         resolveArray = new Array(length);
          $.each(args, function(index, element) {
            // 递归产生多个EnhancedDeferred实例
             $.when(element).then(
               function() {
                  resolveArray[index] = arguments.length > 1 
                     ? slice.call(arguments, 0) 
                      : arguments[0]
                    if (!--length) {
                      // 当入参均有返回值时,则修改顶层EnhancedDeferred实例状态为"resolved"
                       deferred.resolveWith(promise, resolveArray);
                    }
                 }
                 // 修改顶层EnhancedDeferred实例状态为"rejected"
                 , deferred.reject);
             });
        } else if (deferred !== object) {
          // 当object不是Deferred实例或Promise实例时,将当前的EnhancedDeferred实例状态设置为"resolved"
           deferred.resolve(object);
        }
        // 将设置当前EnhancedDeferred实例状态的操作,交还给object自身
        return promise;
    };
    复制代码

      jQuery.Deferred中的Deferred实例和EnhancedDeferred实例均设计了隐式的状态标识,因此支持回调函数晚绑定的功能,但由于其采用两个Deferred实例分类管理所有成功/失败回调函数,而不是采用Deferred实例单向链表的结构,因此无法实现成功和失败回调函数之间的数据传递,并且没有对回调函数的抛异常的情况作处理。并且resolveWith的遍历调用回调函数队列中没有采用责任链模式,与Promises/A+规范截然不同。另外回调函数均为同步调用,而不是Promises/A+中的异步调用。因此我们只能将其列入Before Promises/A的队列中了!

      jQuery1.5除了新增jQuery.Deferred特性,还以jQuery.Deferred为基础对ajax模块进行增强,相关代码如下:

    复制代码
    function done( status, statusText, responses, headers) {
      ......................
      // Success/Error
      if ( isSuccess ) {
        deferred.resolveWith( callbackContext, [ success, statusText, jXHR ] );
      } else {
        deferred.rejectWith( callbackContext, [ jXHR, statusText, error ] );
      }
      ......................
      // Complete
      completeDeferred.resolveWith( callbackContext, [ jXHR, statusText ] );
      ......................
      // Attach deferreds
      deferred.promise( jXHR );
      jXHR.success = jXHR.done;
      jXHR.error = jXHR.fail;
      jXHR.complete = completeDeferred.done
      ...................
    }
    复制代码

    三、觉醒——1.6                        

      可能是jQuery的开发团队意识到jQuery.Deferred的实现与Promises/A+规范相距甚远,于是在1.6版本上补丁式地为EnhancedDeferred增加了一个 pipe方法 ,从而实现回调函数的责任链。另外jQueyr.Deferred已经成为一个独立的模块deferred.js了(《JavaScript框架设计》中的示例就是1.6的)。

    复制代码
    /**
     * fnDone和fnFail作为当前EnhancedDeferred实例的回调函数,
     * 而不是pipe函数中新创建的EnhancedDeferred实例的回调函数。
     */
    pipe = function(fnDone, fnFail) {
      // 创建一个新的EnhancedDeferred实例
       return jQuery.Deferred(function(newDefer) {
         jQuery.each({
            done: [fnDone, "resolve"]
             ,fail: [fnFail, "reject"]
           }, function(handler, data) {
             var fn = data[0]
                ,action = data[1]
                  ,returned
               if ($.isFunction(fn)) {
                 deferred[handler](function() {
                    // fnDone, fnFail作为原有EnhancedDeferred实例的回调函数被执行
                     returned = fn.apply(this, arguments)
                     if (jQuery.isFunction(returned.promise)) {
                       // 若返回值为EnhancedDeferred或Promise实例,由它们来修改新EnhancedDeferred实例的状态
                        returned.promise().then(newDefer.resolve, newDefer.reject)
                     } else {
                        // 将原有EnhancedDeferred实例的回调函数的执行结果作为新EnhancedDeferred实例回调函数的入参,
                        // 并将新EnhancedDeferred实例的状态设置为"resolved"
                         newDefer[action](returned)
                     }
                  })
                } else {
                   // 将新EnhancedDeferred实例的resolve/reject添加到旧EnhancedDeferred相应的回调函数队列中
                   deferred[handler](newDefer[action])
                }
           })
       }).promise()
    } 
    复制代码

      除了pipe函数外,1.6还为EnhancedDeferred实例新增了 always函数 ,通过它添加的回调函数,无论EnhancedDeferred实例状态为"resolved"还是"rejected"均会被执行。

    always = function() {
      return deferred.done.apply(deferred, arguments).fail.apply(this, arguments)
    }

       另外1.6对$.when进行了重构使代码更容易理解。并且effectes和queue模块可以开始以jQuery.Deferred作为基础提供then方法等API了。

    四、全局重构,但本质不变——1.7                 

       由于VS2012新建Asp.Net项目时默认自带jQuery1.7,我想Asp.Net的攻城狮们对它应该不陌生了。而1.7版本的jQuery.Deferred相对于以前的版本新增了 progress 、 notify 和 notifyWith 的API,但到底有什么用呢?1.7版本的jQuery.Deferred是否更接近Promises/A+规范呢?答案是否定的。

       新版的jQuery.Deferred内部新增一个回调函数队列,该队列不像1.6版本中的deferred和failDeferred那样只能触发一次"initialized"->"fired"的状态转换,而是可以进行多次并且与deferred和failDeferred一样支持回调函数晚绑定。而 progress 、 notify 和 notifyWith 则与这个新的回调函数队列相关。

       另外1.7版本中对jQuery.Deferred进行全局重构,不再由原来的 $._Deferred 来构建Deferred实例,而是通过 jQuery.Callbacks函数 来生成回调函数队列管理器来代替(作用是一样的,但回调函数队列管理器更具有通用性),而上文提到的EnhancedDeferred则由三个回调函数队列管理器组成。

       在陷入源码前再次强调一点——1.7与1.6版本在本质上是一点都没变!!

       1. 首先我们一起来看看重构的重心—— jQuery.Callbacks函数 (位于callbacks.js文件中)

           作用:创建回调函数队列管理器实例。

           回调函数队列管理器存在以下状态:

        initialized: 管理器实例初始状态;

         firing: 正在遍历回调函数队列并按FIFO顺序调用回调函数;

         fired: 遍历完回调函数队列,等待接受下一次遍历请求;

         locked: 锁定管理器,无法再接受遍历回调函数的请求;

         dying: 管理器进入临死状态,只要此时状态转换为fired或locked,则会直接跳转为disabled状态;

         disabled: 管理器将被废弃,无法再使用了

          状态间的转换关系如下:

      ①. initialized -> firing <-> fired [-> disabled|locked]

      ②. initialized <-> firing <-> fired [-> disabled|locked]

      ③. initialized -> locked -> disabled

      ④. initialized -> dying -> locked -> disabled

      ⑤. initialized -> dying -> fired -> disabled

      ⑥. initialized -> dying -> fired -> firing

          在调用jQuery.Callbacks时可以通过可选入参来配置管理器的一些特性,分别为:

            unique,是否确保队列中的回调函数的唯一性。

            stopOnFalse,是否当某个回调函数返回值为false时,将配置管理器的状态设置为dying。

            once,是否仅能执行一次队列遍历操作。若不限制仅能执行一次队列遍历(默认值),则状态转换关系为②、③和⑥。

            memory,是否支持函数晚绑定。若不支持晚绑定且仅能执行一次队列遍历操作,则状态转换关系为③、④和⑤。若支持晚绑定则为①和③。

    复制代码
    (function( jQuery ) {
    
    // String to Object flags format cache
    var flagsCache = {};
    
    // Convert String-formatted flags into Object-formatted ones and store in cache
    function createFlags( flags ) {
        var object = flagsCache[ flags ] = {},
            i, length;
        flags = flags.split( /s+/ );
        for ( i = 0, length = flags.length; i < length; i++ ) {
            object[ flags[i] ] = true;
        }
        return object;
    }
    
     /** 特性说明
      * once: 启动仅遍历执行回调函数队列一次特性,遍历结束后废弃该管理器
      * memory: 启动回调函数晚绑定特性
      * unique: 启动回调函数唯一性特性
      * stopOnFalse: 启动回调函数返回false,则废弃该管理器
      */
    jQuery.Callbacks = function( flags ) {
        // 特性标识
        flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
    
        var // 回调函数队列
            list = [],
            // 请求队列(不要被变量名欺骗,不是栈结构而是队列结构),用于暂存发起遍历执行回调函数队列的请求,元素数据结构为[ctx, args]
            stack = [],
            // 标识是否支持回调函数晚绑定, 不支持则为true,支持则为[ctx, args]
            memory,
            // 表示是否正在遍历回调函数队列
            firing,
            // 初始化回调函数队列遍历的起始索引
            firingStart,
            // 回调函数队列遍历的上限
            firingLength,
            // 回调函数队列遍历的起始索引
            firingIndex,
            // 私有方法:添加回调函数到队列
            add = function( args ) {
                var i,
                    length,
                    elem,
                    type;
                for (i = 0, length = args.length; i < length; i++) {
                    elem = args[i];
                    type = jQuery.type(elem);
                    if (type === "array") {
                        // 递归添加到回调函数队列
                        add(elem);
                    } else if (type === "function") {
                        // 开启唯一性特性,且队列中已经有相同的函数则不入队
                        if (!flags.unique || !self.has( elem )) {
                            list.push(elem);
                        }
                    }
                }
            },
            // 私有方法:遍历队列执行队列中的函数
            fire = function(context, args) {
                args = args || [];
                // 标识是否支持回调函数晚绑定, 不支持则为true,支持则为[ctx, args]
                memory = !flags.memory || [context, args];
                firing = true;
                firingIndex = firingStart || 0;
                firingStart = 0;
                firingLength = list.length;
                // 由于在循环期间有可能管理器会被废弃,因此需要在循环条件中检查list的有效性
                for (;list && firingIndex < firingLength; firingIndex++) {
                    if (list[firingIndex].apply(context, args) === false && flags.stopOnFalse) {
                        memory = true; // 标识中止遍历队列操作,效果和不支持回调函数晚绑定一致
                        break;
                    }
                }
                firing = false;
                if (list) {
                    if (!flags.once) {
                        if (stack && stack.length) {
                            // 关闭仅遍历一次回调函数队列特性时
                            // 请求队列首元素出队,再次遍历执行回调函数队列
                            memory = stack.shift();
                            self.fireWith(memory[0], memory[1]);
                        }
                    } else if (memory === true) {
                        // 当开启仅遍历一次回调函数队列特性,且发生了中止遍历队列操作或不支持回调函数晚绑定,
                        // 则废弃当前回调函数队列管理器
                        self.disable();
                    } else {
                        list = [];
                    }
                }
            },
            // 回调函数队列管理器
            self = {
                // 添加回调函数到队列中
                add: function() {
                    if (list) {
                        var length = list.length;
                        // 如果正在遍历执行回调函数队列,那么添加函数到队列后马上更新遍历上限,从而执行新加入的回调函数
                        add(arguments);
                        if (firing) {
                            firingLength = list.length;
                        } else if (memory && memory !== true) {
                            // 遍历执行回调函数已结束,并且支持函数晚绑定则从上次遍历结束时的索引位开始继续遍历回调函数队列
                            firingStart = length;
                            fire(memory[0], memory[1]);
                        }
                    }
                    return this;
                },
                // 从队列中删除回调函数
                remove: function() {
                    if (list) {
                        var args = arguments,
                            argIndex = 0,
                            argLength = args.length;
                        for (; argIndex < argLength; argIndex++) {
                            for ( var i = 0; i < list.length; i++) {
                                if (args[argIndex] === list[i]) {
                                    // 由于删除队列的一个元素,因此若此时正在遍历执行回调函数队列,
                                    // 则需要调整当前遍历索引和遍历上限
                                    if (firing) {
                                        if (i <= firingLength) {
                                            firingLength--;
                                            if (i <= firingIndex) {
                                                firingIndex--;
                                            }
                                        }
                                    }
                                    // 删除回调函数
                                    list.splice(i--, 1);
                                    // 如果开启了回调函数唯一性的特性,则只需删除一次就够了
                                    if (flags.unique) {
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    return this;
                },
                // 对回调函数作唯一性检查
                has: function( fn ) {
                    if ( list ) {
                        var i = 0,
                            length = list.length;
                        for ( ; i < length; i++ ) {
                            if ( fn === list[ i ] ) {
                                return true;
                            }
                        }
                    }
                    return false;
                },
                // 清空回调函数队列
                empty: function() {
                    list = [];
                    return this;
                },
                // 废除该回调函数队列
                disable: function() {
                    list = stack = memory = undefined;
                    return this;
                },
                // 状态:是否已废弃
                disabled: function() {
                    return !list;
                },
                // 不在处理遍历执行回调函数队列的请求
                lock: function() {
                    // 不再处理遍历执行回调函数队列的请求
                    stack = undefined;
                    if (!memory || memory === true) {
                        // 当未遍历过回调函数队列
                        // 或关闭晚绑定特性则马上废弃该管理器
                        self.disable();
                    }
                    return this;
                },
                // 状态:是否已被锁定
                locked: function() {
                    return !stack;
                },
                // 发起遍历队列执行队列函数的请求
                fireWith: function(context,args) {
                    if (stack) {
                        if (firing) {
                            if (!flags.once) {
                                // 若正在遍历队列,并且关闭仅遍历一次队列的特性时,将此请求入队
                                stack.push([context, args]);
                            }
                        } else if (!flags.once || !memory) {
                            // 关闭仅遍历一次队列的特性
                            // 或从未遍历过回调函数队列时,执行遍历过回调函数队列操作
                            fire(context, args);
                        }
                    }
                    return this;
                },
                // 发起遍历队列执行队列函数的请求
                fire: function() {
                    self.fireWith(this, arguments);
                    return this;
                },
                // 状态:是否已遍历过回调函数队列
                fired: function() {
                    return !!memory;
                }
            };
    
        return self;
    };
    })( jQuery );
    复制代码

       2. 然后就是jQuery.Deferred的改造

    复制代码
    $.Deferred = function(){
      // 对原来的Deferred实例改造为两个不可重复遍历函数队列的回调函数队列管理器
      var doneList = jQuery.Callbacks("once memory"),
        failList = jQuery.Callbacks("once memory");
       // 新增的回调函数队列管理器,可多次遍历其函数队列
      var progressList = jQuery.Callbacks("memory"); 
      ...........................................
    }
    复制代码

      1.7中通过 私有属性state 明确标识Deferred实例的状态(pendingresolvedrejected),但可惜的是这些属性对Deferred实例的行为没有任何作用,感觉有没有这些状态都没有所谓。

      经过这样一改,就更明确Deferred实例其实对三个回调函数队列的统一管理入口而已了。

    五、又一次靠近Promise/A+规范——1.8               

      jQuery1.8的jQuery.Deferred依然依靠jQuery.Callbacks函数生成的三个回调函数队列管理器作为Deferred的构建基础,该版本大部分均为对jQuery.Deferred和jQuery.Callbacks代码结构、语义层面的局部重构,使得更容易理解和维护,尤其是对jQuery.Callbacks代码重构后,回调函数队列管理器实例的状态关系转换清晰不少。

      而比较大的局部功能重构是jQuery.Deferred的then方法被重构成为pipe方法的别名,而pipe函数的实现为Promise/A规范中的then方法,因此1.8的then方法与旧版本的then方法不完全兼容。

    六、 保持现状——1.9&2.1                       

      jQuery1.9和2.1并没重构或为jQuery.Deferred添加新功能,可以直接跳过。

    七、总结                               

      通过上述内容大家已经清楚jQuery.Deferred并不是Promise/A+规范的完整实现(甚至可以说是相距甚远),且jQuery1.8中then函数的实现方式与旧版本的不同,埋下了兼容陷阱,但由于jQuery.Deferred受众面少(直接使用Ajax、effects和queue模块的Promise形式的API较多),因此影响范围不大,庆幸庆幸啊!

      尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4158939.html ^_^肥子John

    八、参考                               

      《JavaScript架构设计》

  • 相关阅读:
    压缩脚本文件
    通用的访问非公有字段(Field)和属性组件
    调用加了SSL签名的WebService
    [译作]Class in Jscript Part I
    在UpdatePanel中使用Menu出错的解决方案
    到处遍是ASP.NET AJAX Released
    也谈约定胜于配置
    如何使用Orcas的CSS功能
    Ext js 2.0 Overview(3) 组件生命周期
    一些非常有用的备忘录文档
  • 原文地址:https://www.cnblogs.com/zhuyang/p/4321974.html
Copyright © 2011-2022 走看看