lodash受歡迎的一個緣由,是其優異的計算性能。而其性能能有這麼突出的表現,很大部分就來源於其使用的算法——惰性求值。
本文將講述lodash源碼中,惰性求值的原理和實現。javascript
惰性求值(Lazy Evaluation),又譯爲惰性計算、懶惰求值,也稱爲傳需求調用(call-by-need),是計算機編程中的一個概念,它的目的是要 最小化計算機要作的工做。
惰性求值中的參數直到須要時纔會進行計算。這種程序其實是 從末尾開始反向執行的。它會判斷本身須要返回什麼,並繼續向後執行來肯定要這樣作須要哪些值。
如下是How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation.(如何提高Lo-Dash百倍算力?惰性計算的簡介)文中的示例,形象地展現惰性求值。java
function priceLt(x) { return function(item) { return item.price < x; }; } var gems = [ { name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 }, { name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7 }, { name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 }, { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 } ]; var chosen = _(gems).filter(priceLt(10)).take(3).value();
程序的目的,是對數據集gems
進行篩選,選出3個price
小於10的數據。git
若是拋開lodash
這個工具庫,讓你用普通的方式實現var chosen = _(gems).filter(priceLt(10)).take(3)
;那麼,能夠用如下方式: _(gems)
拿到數據集,緩存起來。
再執行filter
方法,遍歷gems
數組(長度爲10),取出符合條件的數據:github
[ { name: 'Sunstone', price: 4 }, { name: 'Sugilite', price: 7 }, { name: 'Diopside', price: 3 }, { name: 'Dioptase', price: 2 } ]
而後,執行take
方法,提取前3個數據。算法
[ { name: 'Sunstone', price: 4 }, { name: 'Sugilite', price: 7 }, { name: 'Diopside', price: 3 } ]
總共遍歷的次數爲:10+3
。
執行的示例圖以下:編程
普通的作法存在一個問題:每一個方法各作各的事,沒有協調起來浪費了不少資源。
若是能先把要作的事,用小本本記下來😎,而後等到真正要出數據時,再用最少的次數達到目的,豈不是更好。
惰性計算就是這麼作的。
如下是實現的思路:數組
_(gems)
拿到數據集,緩存起來filter
方法,先記下來take
方法,先記下來value
方法,說明時機到了filter
方法裏的判斷方法priceLt
對數據進行逐個裁決[ { name: 'Sunstone', price: 4 }, => priceLt裁決 => 符合要求,經過 => 拿到1個 { name: 'Amethyst', price: 15 }, => priceLt裁決 => 不符合要求 { name: 'Prehnite', price: 20}, => priceLt裁決 => 不符合要求 { name: 'Sugilite', price: 7 }, => priceLt裁決 => 符合要求,經過 => 拿到2個 { name: 'Diopside', price: 3 }, => priceLt裁決 => 符合要求,經過 => 拿到3個 => 夠了,收工! { name: 'Feldspar', price: 13 }, { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 } ]
如上所示,一共只執行了5次,就把結果拿到。
執行的示例圖以下:緩存
從上面的例子能夠獲得惰性計算的特色:數據結構
value
方法,通知真正開始計算依據上述的特色,我將lodash的惰性求值實現進行抽離爲如下幾個部分:app
實現_(gems)
。我這裏爲了語義明確,採用lazy(gems)
代替。
var MAX_ARRAY_LENGTH = 4294967295; // 最大的數組長度 // 緩存數據結構體 function LazyWrapper(value){ this.__wrapped__ = value; this.__iteratees__ = []; this.__takeCount__ = MAX_ARRAY_LENGTH; } // 惰性求值的入口 function lazy(value){ return new LazyWrapper(value); }
this.__wrapped__
緩存數據this.__iteratees__
緩存數據管道中進行「裁決」的方法this.__takeCount__
記錄須要拿的符合要求的數據集個數這樣,一個基本的結構就完成了。
filter
方法var LAZY_FILTER_FLAG = 1; // filter方法的標記 // 根據 篩選方法iteratee 篩選數據 function filter(iteratee){ this.__iteratees__.push({ 'iteratee': iteratee, 'type': LAZY_FILTER_FLAG }); return this; } // 綁定方法到原型鏈上 LazyWrapper.prototype.filter = filter;
filter
方法,將裁決方法iteratee
緩存起來。這裏有一個重要的點,就是須要記錄iteratee
的類型type
。
由於在lodash
中,還有map
等篩選數據的方法,也是會傳入一個裁決方法iteratee
。因爲filter
方法和map
方法篩選方式不一樣,因此要用type
進行標記。
這裏還有一個技巧:
(function(){ // 私有方法 function filter(iteratee){ /* code */ } // 綁定方法到原型鏈上 LazyWrapper.prototype.filter = filter; })();
原型上的方法,先用普通的函數聲明,而後再綁定到原型上。若是工具內部須要使用filter
,則使用聲明好的私有方法。
這樣的好處是,外部若是改變LazyWrapper.prototype.filter
,對工具內部,是沒有任何影響的。
take
方法// 截取n個數據 function take(n){ this.__takeCount__ = n; return this; }; LazyWrapper.prototype.take = take;
value
方法// 惰性求值 function lazyValue(){ var array = this.__wrapped__; var length = array.length; var resIndex = 0; var takeCount = this.__takeCount__; var iteratees = this.__iteratees__; var iterLength = iteratees.length; var index = -1; var dir = 1; var result = []; // 標籤語句 outer: while(length-- && resIndex < takeCount){ // 外層循環待處理的數組 index += dir; var iterIndex = -1; var value = array[index]; while(++iterIndex < iterLength){ // 內層循環處理鏈上的方法 var data = iteratees[iterIndex]; var iteratee = data.iteratee; var type = data.type; var computed = iteratee(value); // 處理數據不符合要求的狀況 if(!computed){ if(type == LAZY_FILTER_FLAG){ continue outer; }else{ break outer; } } } // 通過內層循環,符合要求的數據 result[resIndex++] = value; } return result; } LazyWrapper.prototype.value = lazyValue;
這裏的一個重點就是:標籤語句
outer: while(length-- && resIndex < takeCount){ // 外層循環待處理的數組 index += dir; var iterIndex = -1; var value = array[index]; while(++iterIndex < iterLength){ // 內層循環處理鏈上的方法 var data = iteratees[iterIndex]; var iteratee = data.iteratee; var type = data.type; var computed = iteratee(value); // 處理數據不符合要求的狀況 if(!computed){ if(type == LAZY_FILTER_FLAG){ continue outer; }else{ break outer; } } } // 通過內層循環,符合要求的數據 result[resIndex++] = value; }
當前方法的數據管道實現,其實就是內層的while
循環。經過取出緩存在iteratees
中的裁決方法取出,對當前數據value
進行裁決。
若是裁決結果是不符合,也即爲false
。那麼這個時候,就不必用後續的裁決方法進行判斷了。而是應該跳出當前循環。
而若是用break
跳出內層循環後,外層循環中的result[resIndex++] = value;
仍是會被執行,這是咱們不但願看到的。
應該一次性跳出內外兩層循環,而且繼續外層循環,纔是正確的。
標籤語句,恰好能夠知足這個要求。
var testArr = [1, 19, 30, 2, 12, 5, 28, 4]; lazy(testArr) .filter(function(x){ console.log('check x='+x); return x < 10 }) .take(2) .value(); // 輸出以下: check x=1 check x=19 check x=30 check x=2 // 獲得結果: [1, 2]
整個惰性求值的實現,重點仍是在數據管道這塊。以及,標籤語句在這裏的妙用。其實實現的方式,不僅當前這種。可是,要點仍是前面講到的三個。掌握精髓,變通就很容易了。
惰性求值,是我在閱讀lodash
源碼中,發現的最大閃光點。
當初對惰性求值不甚理解,想看下javascript的實現,但網上也只找到上文提到的一篇文獻。
那剩下的選擇,就是對lodash進行剖離分析。也由於這,纔有本文的誕生。
但願這篇文章能對你有所幫助。若是能夠的話,給個star :)
最後,附上本文實現的簡易版lazy.js
完整源碼:
https://github.com/wall-wxk/blogDemo/blob/master/lodash/lazy.js
喜歡我文章的朋友,能夠經過如下方式關注我: