DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> JavaScript基礎知識 >> JS的鏈式調用實現方法
JS的鏈式調用實現方法
編輯:JavaScript基礎知識     

  鏈式調用我們平常用到很多,比如jQuery中的$(ele).show().find(child).hide(),再比如angularjs中的$http.get(url).success(fn_s).error(fn_e)。但這都是已經包裝好的鏈式調用,我們只能體會鏈式調用帶來的方便,卻不知道形成這樣一條函數鏈的原理是什麼。

  隨著鏈式調用的普及,實現的方案也越來越多。最常見的,是jQuery直接返回this的方式,underscore的可選式的方式,和lodash惰性求值的方式。我們分別來了解,並逐個完成它們的demo。

  我們從最簡單的開始,直接返回this是最常見的方式,也是所有方式的基礎。我們實現一個簡單的鏈式運算類,首先它得有個字段保留結果。

function A(num) {
  this.value = num || 0;  //不做傳參校驗了
 }

  然後添加進行運算並返回this的方法。

A.prototype.add = function(a) {this.value += a; return this;}
 A.prototype.reduce = function(a) {this.value -= a; return this;}

  最後為了顯示正常修改兩個繼承的方法。

A.prototype.valueOf = function() {return this.value;}
 A.prototype.toString = function() {return this.value + '';}

  進行驗證。

var a = new A(2);
 alert(a.add(1).reduce(2))

  這個demo應該簡單到不用對任何代碼進行說明,我們快速來到第二個,就是underscore中用到chain。underscore規定了兩種調用方式,_.forEach(arr, fn);_.map(arr, fn);和_.chain(arr).forEach(fn).map(fn)。

  我們先實現前面一種調用方式,因為這裡不是講解underscore,所以我們只是簡單實現forEach和map的功能,不對對象而僅對數組進行處理。

var _ = {};
_.forEach = function(array, fn) {
    array.forEach(function(v, i, array) {
      fn.apply(v, [v, i, array]);
    })
  };
  _.map = function(array, fn) {
    return array.map(function(v, i, array) {
      return fn.apply(v, [v, i, array]);
    })
  };

  上面的代碼很簡單,直接調用ES5中數組原型的方法。接下來問題就來了,要實現鏈式調用,我們首先要做什麼?我們看到第二種調用方式中,所有的操作無論是forEach還是map都是在_.chain(arr)上調用的,所以_.chain(arr)應該是返回了一個對象,這個對象上有和_上相同的方法,只是實現上傳參由2個變成了1個,因為原來的第一個參數永遠是_.chain中傳入的參數的拷貝。

  好了,確定_.chain(arr)要返回一個對象了,那這個對象的構造函數怎麼寫呢?我們借用一個現成的變量來保存這個構造函數,就是_。函數也是對象,所以當_由對象變成函數,不會影響原來的邏輯,而這個函數要傳入一個array,並返回一個新的對象。所以上面的代碼應該改成這樣。

var _ = function(array) {
    this._value = Array.prototype.slice.apply(array);
  }
  _.forEach = function(array, fn) {
    array.forEach(function(v, i, array) {
      fn.apply(v, [v, i, array]);
    })
  };
  _.map = function(array, fn) {
    return array.map(function(v, i, array) {
      return fn.apply(v, [v, i, array]);
    })
  };
  _.chain = function(array) {
    return new _(array);
  }

  新的構造函數有了,但它生成的對象除了_value就是一片空白,我們要怎麼把原本_上的方法稍加修改的移植到_生成的對象上呢?代碼如下:

for(var i in _) { //首先我們要遍歷_
    if(i !== 'chain') { //然後要去除chain
      _.prototype[i] = (function(i) { //把其他的方法都經過處理賦給_.prototype
        return function() { //i是全局變量,我們要通過閉包轉化為局部變量
          var args = Array.prototype.slice.apply(arguments);  //取出新方法的參數,其實就fn一個
          args.unshift(this._value);  //把_value放入參數數組的第一位
          if(i === 'map') { //當方法是map的時候,需要修改_value的值
            this._value = _[i].apply(this, args);
          }else { //當方法是forEach的時候,不需要修改_value的值
            _[i].apply(this, args);
          }
          return this;
        }
      })(i);
    }
  }

  最後我們模仿underscore使用value返回當前的_value。

_.prototype.value = function() {
    return this._value;
  }

   進行驗證。

var a = [1, 2, 3];
  _.forEach(a, function(v){console.log(v);})
  alert(_.map(a, function(v){return ++v;}))
  alert(_.chain(a).map(function(v){return ++v;}).forEach(function(v){console.log(v);}).value())

  以上是underscore中用到的鏈式調用的簡化版,應該不難理解。那最復雜的來了,lodash惰性調用又是怎樣的呢?首先我來解釋下什麼是惰性調用,比如上面的_.chain(arr).forEach(fn).map(fn).value(),當執行到chain(arr)的時候,返回了一個對象,執行到forEach的時候開始輪詢,輪詢完再返回這個對象,執行到map的時候再次開始輪詢,輪詢完又返回這個對象,最後執行到value,返回對象中_value的值。其中每一步都是獨立的,依次進行的。而惰性調用就是,執行到forEach的時候不執行輪詢的操作,而是把這個操作塞進隊列,執行到map的時候,再把map的操作塞進隊列。那什麼時候執行呢?當某個特定的操作塞進隊列的時候開始執行之前隊列中所有的操作,比如當value被調用時,開始執行forEach、map和value。

  惰性調用有什麼好處呢,為什麼把一堆操作塞在一起反倒是更優秀的方案的?我們看傳統的鏈式操作都是這樣的格式,obj.job1().job2().job3(),沒錯整個函數鏈都是job鏈,如果這時候有一個簡單的需求,比如連續執行100遍job1-3,那麼我們就要寫100遍,或者用for把整個鏈條斷開100次。所以傳統鏈式操作的缺點很明顯,函數鏈中都是job,不存在controller。而一旦加上controller,比如上面的需求我們用簡單的惰性調用來實現,那就是obj.loop(100).job1().job2().job3().end().done()。其中loop是聲明開啟100次循環,end是結束當前這次循環,done是開始執行任務的標志,代碼多麼簡單!

  現在我們實現一下惰性鏈式調用,由於lodash就是underscore的威力加強版,大體架構都差不多,而上面已經有underscore的基本鏈式實現,所以我們脫離lodash和underscore的其他代碼,僅僅實現一個類似的惰性調用的demo。

  首先我們要有一個構造函數,生成可供鏈式調用的對象。之前提到的,任何controller或者job的調用都是把它塞入任務隊列,那麼這個構造函數自然要有一個隊列屬性。有了隊列,肯定要有索引指明當前執行的任務,所以要有隊列索引。那麼這個構造函數暫時就這樣了

function Task() {
    this.queen = [];
  this.queenIndex = 0; }

  如果我們要實現loop,那麼還要有個loop的總次數和當前loop的次數,而如果一次loop結束,我們要回到任務隊列哪裡呢?所以還要有個屬性記錄loop開始的地方。構造函數最終的形態如此:

function Task() {
    this.queen = [];
    this.queenIndex = 0;
    this.loopCount = 0;
    this.loopIndex = 0;
    this.loopStart = 0;
  }

  現在我們開始實現controller和job,比如上面這個例子中說到的:job()、loop()、end()、done()。它們應該都包含兩種形態,一種是本來的業務邏輯,比如job的業務就是do something,而loop的控制邏輯就是記錄loopCount和loopStart,end的控制邏輯就是loopIndex+1和檢查loopIndex看是否需要回到loopStart的位置再次遍歷。而另一種形態是不管業務邏輯是什麼,把業務邏輯對應的代碼統一塞進任務隊列,這種形態可以稱之為第一種形態的包裝器。

  如果我們最終的調用格式是new Task().loop(100).job().end().done(),那麼方法鏈上的方法肯定是包裝器,這些方法自然應該放在Task.prototype上,那第一種形態的方法何去何從呢?那就放在Task.prototype.__proto__上吧。我們這樣寫

var _task_proto = {
    loop: function(num) {
      this.loopStart = this.queenIndex;
      this.loopCount = num;
    },
    job: function(str) {
      console.log(str);
    },
    end: function() {
      this.loopIndex++;
      if(this.loopIndex < this.loopCount) {
        this.queenIndex = this.loopStart;
      }else {
        this.loopIndex = 0;
      }
    },
    done: function() {
      console.log('done');
    }
  };
  Task.prototype.__proto__ = _task_proto;

  然後在遍歷_task_proto在Task.prototype上生成包裝器,並讓每個包裝器返回this以供鏈式調用(看見沒,其實每一種鏈式調用的方式都要這麼做)

  for(var i in _task_proto) {
    (function(i) {
      var raw = Task.prototype[i];
      Task.prototype[i] = function() {
        this.queen.push({
          name: i,
          fn: raw,
          args: arguments
        }); //保存具體的實現方法、名字和參數到任務隊列
    return this; }; })(i); }

  現在問題來了,我們什麼時候開始執行具體的任務,又怎樣讓任務有條不紊的執行和跳轉呢?這時候我們要在Task.prototype上定義一個新的方法,這個方法專門用來控制任務的執行的,因為任務隊列是依次執行並由索引定位的,跟迭代器有那麼一點相像,我們定義這個新的方法叫next

Task.prototype.next = function() {
    var task = this.queen[this.queenIndex];  //取出新的任務
    task.fn.apply(this, task.args); //執行任務中指向的具體的實現方法,並傳入之前保存的參數
    if(task.name !== 'done') {
      this.queenIndex++;
      this.next();  //如果沒執行完,任務索引+1並再次調用next
    }else {
      this.queen = [];
      this.queenIndex = 0;  //如果執行完了,清空任務隊列,重置任務索引
    }
  }

  添加了next,我們需要在done的包裝器上加點東西以便讓任務隊列開始執行,修改之前生成包裝器的代碼

  for(var i in _task_proto) {
    (function(i) {
      var raw = Task.prototype[i];
      Task.prototype[i] = function() {
        this.queen.push({
          name: i,
          fn: raw,
          args: arguments
        }); //保存具體的實現方法、名字和參數到任務隊列
        if(i === 'done') {
          this.next();
        }
        return this;
      };
    })(i);
  }

  最後我們進行驗證。

var t = new Task();
  console.log('1')
  t.job('fuck').loop(3).job('world').end().loop(3).job('world').end().job('!').done();
  console.log('2')
  t.job('fuck').loop(3).job('world').job('!').end().done();
  console.log('3')
  t.job('fuck').loop(3).job('world').job('!').end().job('!');

  好了,鏈式調用玩到這裡了。這幾個demo尤其是惰性調用稍加改造後,功能可以大大加強,但是這裡就不再討論了。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved