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