文章出處

一、前言                           

  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實例的狀態:initializedfiredcancelled。而狀態間的轉換關系如下:

     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架構設計》


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()