來看一個 lazy.js 主頁提供的示例:javascript
var people = getBigArrayOfPeople(); var results = _.chain(people) .pluck('lastName') .filter(function(name) { return name.startsWith('Smith'); }) .take(5) .value();
上例中,要在很是很是多的人裏面,找出 5 個以 Smith 開頭的 lastName。若是在上面的 pluck() 和 filter() 的過程當中,每一步都產生了臨時數組,也就是說對上一步返回的數據執行了一次循環、處理的過程,那麼整個查找的過程可能會花費很長的時間。html
不採用上面的這種寫法,單純爲了性能考慮,能夠這樣處理:java
var results = []; for (var i = 0; i < people.length; ++i) { var lastName = people[i].lastName; if (lastName.startsWith('Smith')) { results.push(value); if (results.length === 5) { break; } } }
首先,對於原始數據,只作一次循環,單次循環中完成全部的計算。其次,因爲只須要 5 個最終結果,因此,一旦獲得了 5 個結果,就終止循環。git
採起這種處理方法,對於比較大的數組,在計算性能上應該會有明顯的提高。github
不過,若是每次遇到這種相似的情形,爲了性能,都要手寫這樣的代碼,有點麻煩,並且代碼不清晰,不便於理解、維護。第一個例子中的寫法要好些,明顯更易讀一些,可是對於性能方面有些擔心。express
因此,若是能夠結合第一個例子中的寫法,但又能有第二個例子中的性能提高,豈不是很好?設計模式
接下來再說說「惰性求值」了。數組
Lazy evaluation - wikipedia
https://en.wikipedia.org/wiki...app
In programming language theory, lazy evaluation, or call-by-need is an evaluation strategy which delays the evaluation of an expression) until its value is needed (non-strict evaluation) and which also avoids repeated evaluations (sharing)).函數
用個人話來表達,惰性求值就是:對於一個表達式,在不須要值的時候不計算,須要的時候才計算。
JavaScript 並不是從語言層面就支持這樣的特性,而是要經過一些技術來模擬、實現。
首先,不能是表達式,表達式在 JS 中是當即求值的。因此,要像第一個例子中的那樣,將求值的過程包裝爲函數,只在須要求值的時候才調用該函數。
而後,延遲計算還須要經過「精妙的」設計和約定來實現。對於第一個例子,pluck()、filter()、take() 方法在調用時,不能直接對原始數據進行計算,而是要延遲到相似 value() 這樣的方法被調用時再進行。
在分析 Lazy.js 的惰性求值實現前,先總結下這裏要討論的藉助惰性求值技術來實現的目標:
良好的代碼可讀性
良好的代碼執行性能
而對於 Lazy.js 中的惰性求值實現,能夠總結爲:
收集計算需求
延遲並優化計算的執行過程
與 Underscore、Lo-Dash 不一樣,Lazy.js 只提供鏈式的、惰性求值的計算模式,這也使得其惰性求值實現比較「純粹」,接下來就進入 Lazy.js 的實現分析了。
先看下使用 Lazy.js 的代碼(來自 simply-lazy - demo):
Lazy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) .map(i => i * 2) .filter(i => i <= 10) .take(3) .each(i => print(i)) // output: // 2 // 4 // 6
注:爲了書寫方法,函數定義採用 ES6 的 「=>」 語法。
這是一個有點無聊的例子,對 1 - 10 先進行乘 2 運算,而後過濾出小於 10 的值,再取前 3 個結果值輸出。
若是對上述代碼的執行進行監測(參考:藉助 Proxy 實現回調函數執行計數),會獲得如下結果:
map() 和 filter() 的過程都只執行了 3 次。
先關注 map() 調用,顯然,這裏沒有當即執行計算,由於這裏的代碼還不能預知到後面的 filter() 和 take(3),因此不會聰明地知道本身只須要執行 3 次計算就能夠了。若是沒有執行計算,那麼這裏作了什麼,又返回了什麼呢?
先說答案:其實從 Lazy() 返回的是一個 Sequence 類型的對象,包含了原始的數據;map() 方法執行後,又生成了一個新的 Sequence 對象,該對象連接到上一個 Sequence 對象,同時記錄了當前步驟要執行的計算(i => i * 2),而後返回新的 Sequence 對象。後續的 filter() 和 take() 也是相似的過程。
上面的代碼也能夠寫成:
var seq0 = Lazy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) var seq1 = seq0.map(i => i * 2) var seq2 = seq1.filter(i => i <= 10) var seq3 = seq2.take(3) // 求值 seq3.each(i => print(i))
在最後一個求值時,已經有一個 Sequence 鏈了:
seq0 <- seq1 <- seq2 <- seq3
在 seq3 調用 each() 方法執行求值前,這些鏈上的 seq 都尚未執行計算。那麼計算的過程是怎樣的呢?其實就相似於最前面第二個例子那樣,在一個循環中,由鏈上的最後一個 seq 開始,依次向前面一個 seq 獲取值,再將值返回給調用方(也就是下一個 seq)前,會應用當前 seq 的計算。
得到第一個最終結果值的過程爲:
[1] seq3 調用 seq2 獲取第 1 個值 [2] seq2 調用 seq1 獲取第 1 個值 [3] seq1 直接從 seq0 的 source 屬性對應的原始數值取值(第 1 個值爲 1) [4] seq1 獲得 1,應用計算(i => i * 2),獲得 2,返回給調用方(seq2) [5] seq2 獲得 2,應用計算(i => i < 10),知足條件,將 2 返回給調用方(seq3) [6] seq3 獲得 2,應用計算(已返回值數目是否小於 3),知足條件,將 2 返回給調用方(seq3.each)
最終,seq3.each() 獲得第 1 個值(2),應用計算(i => print(i)),將其輸出。而後繼續獲取下一個值,直到在某個過程當中調用終止(這裏是 take() 計算中已返回 3 個值時終止)。
除了 map()、filter()、take(),Lazy.js 還提供了更多的計算模式,不過其惰性計算的過程就是這樣了,總結爲:
Lazy() 返回初始的 Sequence 對象
惰性計算方法返回新的 Sequence 對象,內部記錄上一個 Sequence 對象以及當前計算邏輯
求值計算方法從當前 Sequence 對象開始,依次向上一個 Sequence 對象獲取值
Sequence 對象在將從上一個 Sequence 對象得到的值返回給下一個 Sequence 前,應用自身的計算邏輯
Lazy.js 的 Sequence 的設計,使得取值和計算的過程造成一個長鏈,鏈條上的單個節點只須要完成上傳、下達,而且應用自身計算邏輯就能夠了,它不須要洞悉總體的執行過程。每一個節點各司其職,最終實現了惰性計算。
代碼有時候賽過萬語千言,下面經過對簡化版的 Lazy.js(simply-lazy)的源碼分析,來更進一步展現 Lazy.js 惰性計算的原理。
Lazy.js 能夠支持進行計算的數據,除了數組,還有字符串和對象。simply-lazy 只提供了數組的支持,並且只支持三種惰性求值方法:
map()
filter()
take()
分別看這個三個方法的實現。
(一)map
Lazy() 直接返回的 Sequence 對象是比較特殊的,和鏈上的其餘 Sequence 對象不一樣,它已是根節點,自身記錄了原始數據,也不包含計算邏輯。因此,對這個對象進行遍歷其實就是遍歷原始數據,也不涉及惰性計算。
simple-lazy 中保留了 Lazy.js 中的命名方式,儘管只支持數組,仍是給這個類型取名爲 ArrayWrapper:
function ArrayWrapper(source) { var seq = ArrayLikeSequence() seq.source = source seq.get = i => source[i] seq.length = () => source.length seq.each = fn => { var i = -1 var len = source.length while (++i < len) { if (fn(source[i], i) === false) { return false } } return true } seq.map = mapFn => MappedArrayWrapper(seq, mapFn) seq.filter = filterFn => FilteredArrayWrapper(seq, filterFn) return seq }
simple-lazy 爲了簡化代碼,並無採用 Lazy.js 那種爲不一樣類型的 Sequence 對象構造不一樣的類的模式,Sequence 能夠看做普通的對象,只是按照約定,須要支持幾個接口方法而已。像這裏的 ArrayWrapper(),返回的 seq 對象儘管來自 ArrayLikeSequence(),但自身已經實現了大多數的接口。
Lazy 函數其實就是返回了這樣的 ArrayWrapper 對象:
function Lazy(source) { if (source instanceof Array) { return ArrayWrapper(source); } throw new Error('Sorry, only array is supported in simply-lazy.') }
若是對於 Lazy() 返回的對象之間進行求值,能夠看到,其實就是執行了在 ArrayWrapper 中定義的遍歷原始數據的過程。
下面來看 seq.map()。ArrayWrapper 的 map() 返回的是 MappedArrayWrapper 類型的 Sequence 對象:
function MappedArrayWrapper(parent, mapFn) { var source = parent.source var length = source.length var seq = ArrayLikeSequence() seq.parent = parent seq.get = i => (i < 0 || i >= length) ? undefined : mapFn(source[i]) seq.length = () => length seq.each = fn => { var i = -1; while (++i < length) { if (fn(mapFn(source[i], i), i) === false) { return false } } return true } return seq }
這其實也是個特殊的 Sequence(因此名字上沒有 Sequence),由於它知道本身上一級 Sequence 對象是不包含計算邏輯的原始 Sequence 對象,因此它直接經過 parent.source 就能夠獲取到原始數據了。
此時執行的求值,則是直接在原始數據上應用傳入的 mapFn。
而若是是先執行了其餘的惰性計算方法,對於獲得的結果對象再應用 map() 呢, 這個時候就要看具體的狀況了,simply-lazy 中還有兩種相關的類型:
MappedSequence
IndexedMappedSequence
MappedSequence 是更通用的類型,對應 map() 獲得 Sequence 的類型:
function MappedSequence(parent, mapFn) { var seq = new Sequence() seq.getIterator = () => { var iterator = parent.getIterator() var index = -1 return { current() { return mapFn(iterator.current(), index) }, moveNext() { if (iterator.moveNext()) { ++index return true } return false } } } seq.each = fn => parent.each((e, i) => fn(mapFn(e, i), i)) return seq }
來看這裏的求值(each)過程,是間接調用了其上級 Sequence 的 each() 來完成的。同時還定義了 getIterator() 接口,使得其下級 Sequence 能夠從這裏獲得一個迭代器,對於該 Sequence 進行遍歷。迭代器在 Lazy.js 中也是一個約定的協議,實現該協議的對象要支持 current() 和 moveNext() 兩個接口方法。從迭代器的邏輯中,能夠看到,當 MappedSequence 對象做爲其餘 Sequence 的上級時,若是實現「上傳下達」。
而 IndexedMappedSequence 的實現要簡單些,它的主要功能都來源於「繼承」 :
function IndexedMappedSequence(parent, mapFn) { var seq = ArrayLikeSequence() seq.parent = parent seq.get = (i) => { if (i < 0 || i >= parent.length()) { return undefined; } return mapFn(parent.get(i), i); } return seq }
IndexedMappedSequence 只提供了獲取特定索引位置的元素的功能,其餘的處理交由 ArrayLikeSequence 來實現。
而 ArrayLikeSequence 則又「繼承」自 Sequence:
function ArrayLikeSequence() { var seq = new Sequence() seq.length = () => seq.parent.length() seq.map = mapFn => IndexedMappedSequence(seq, mapFn) seq.filter = filterFn => IndexedFilteredSequence(seq, filterFn) return seq }
Sequence 中實現的是更通常意義上的處理。
上面介紹的這些與 map 有關的 Sequence 類型,其實現各有不一樣,的確有些繞。但不管是怎樣進行實現,其核心的邏輯沒有變,都是要在其上級 Sequence 的值上應用其 mapFn,而後返回結果值給下級 Sequence 使用。這是 map 計算的特定模式。
(二)filter
filter 的計算模式與 map 不一樣,filter 對上級 Sequence 返回的值應用 filterFn 進行判斷,知足條件後再傳遞給下級 Sequence,不然繼續從上級 Sequence 獲取新一個值進行計算,直到沒有值或者找到一個知足條件的值。
filter 相關的 Sequence 類型也有多個,這裏只看其中一個,由於儘管實現的方式不一樣,其計算模式的本質是同樣的:
function FilteredSequence(parent, filterFn) { var seq = new Sequence() seq.getIterator = () => { var iterator = parent.getIterator() var index = 0 var value return { current() { return value }, moveNext() { var _val while (iterator.moveNext()) { _val = iterator.current() if (filterFn(_val, index++)) { value = _val return true } } value = undefined return false } } } seq.each = fn => { var j = 0; return parent.each((e, i) => { if (filterFn(e, i)) { return fn(e, j++); } }) } return seq }
FilteredSequence 的 getIterator() 和 each() 中均可以看到 filter 的計算模式,就是前面說的,判斷上級 Sequence 的值,根據結果決定是返回給下一級 Sequence 仍是繼續獲取。
再也不贅述。
(三)take
take 的計算模式是從上級 Sequence 中獲取值,達到指定數目就終止計算:
function TakeSequence(parent, count) { var seq = new Sequence() seq.getIterator = () => { var iterator = parent.getIterator() var _count = count return { current() { return iterator.current() }, moveNext() { return (--_count >= 0) && iterator.moveNext() } } } seq.each = (fn) => { var _count = count var i = 0 var result parent.each(e => { if (i < count) { result = fn(e, i++); } if (i >= count) { return false; } return result }) return i === count && result !== false } return seq }
只看 TakeSequence 類型,與 FilteredSequence 相似,其 getIterator() 和 each() 中都提現了其計算模式。一旦得到了指定數目的值,就終止計算(經過 return false)。
simply-lazy 中雖然只是實現了 Lazy.js 的三個惰性計算方法(map,filter、take),但從中已經能夠看出 Lazy.js 的設計模式了。不一樣的計算方法體現的是不一樣的計算模式,而這計算模式則是經過不一樣的 Sequence 類型來實現的。
具體的 Sequence 類型包含了特定的計算模式,這從其類型名稱上也能看出來。例如,MappedArrayWrapper、MappedSequence、IndexedMappedSequence 都是對應 map 計算模式。
而求值的過程,或者說求值的模式是統一的,都是藉助 Sequence 鏈,鏈條上的每一個 Sequence 節點只負責:
上傳:向上級 Sequence 獲取值
下達:向下級 Sequence 傳遞至
求值:應用類型對應的計算模式對數據進行計算或者過濾等操做
由內嵌了不一樣的計算模式的 Sequence 構成的鏈,就實現了惰性求值。
固然,這只是 Lazy.js 中的惰性求值的實現,並不意外這「惰性求值」就等於這裏的實現,或者說惰性求值的所有特性就是這裏 Lazy.js 的所有特性。更況且,本文主要分析的 simply-lazy 也只是從模式上對 Lazy.js 的惰性求值進行了說明,也並不包含 Lazy.js 的所有特性(例如生成無限長度的列表)。
無論怎樣,仍是閱讀事後可以給你帶來一點有價值的東西。哪怕只有一點,我也很高興。
文中對 Lazy.js 的惰性求值的分析僅是我我的的看法,若是錯漏,歡迎指正!
最後,感謝閱讀!