鏈式調用我們平常用到很多,比如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尤其是惰性調用稍加改造後,功能可以大大加強,但是這裡就不再討論了。