摘要:同時還定義了接口,使得其下級可以從這里得到一個迭代器,對于該進行遍歷。迭代器在中也是一個約定的協議,實現該協議的對象要支持和兩個接口方法。從迭代器的邏輯中,可以看到,當對象作為其他的上級時,如果實現上傳下達。
背景:惰性求值?
來看一個 lazy.js 主頁提供的示例:
var people = getBigArrayOfPeople(); var results = _.chain(people) .pluck("lastName") .filter(function(name) { return name.startsWith("Smith"); }) .take(5) .value();
上例中,要在非常非常多的人里面,找出 5 個以 Smith 開頭的 lastName。如果在上面的 pluck() 和 filter() 的過程中,每一步都產生了臨時數組,也就是說對上一步返回的數據執行了一次循環、處理的過程,那么整個查找的過程可能會花費很長的時間。
不采用上面的這種寫法,單純為了性能考慮,可以這樣處理:
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 個結果,就終止循環。
采取這種處理方法,對于比較大的數組,在計算性能上應該會有明顯的提升。
不過,如果每次遇到這種類似的情形,為了性能,都要手寫這樣的代碼,有點麻煩,而且代碼不清晰,不便于理解、維護。第一個例子中的寫法要好些,明顯更易讀一些,但是對于性能方面有些擔憂。
所以,如果可以結合第一個例子中的寫法,但又能有第二個例子中的性能提升,豈不是很好?
接下來再說說“惰性求值”了。
Lazy evaluation - wikipedia
https://en.wikipedia.org/wiki...
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 中的惰性求值實現,可以總結為:
收集計算需求
延遲并優化計算的執行過程
分析: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.jsLazy.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 的惰性求值的分析僅是我個人的見解,如果錯漏,歡迎指正!
最后,感謝閱讀!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91241.html
摘要:本文不深入分析惰性計算的內部原理后面打算單獨做一次分享,而是介紹下我是如何實現上面的回調函數執行計數。問題明確下需求或者說要解決的問題,針對如下的代碼能夠統計代碼執行過程中傳入的回調函數和的實際執行次數。 背景 最近在做一個簡化版的 Lazy.js:simply-lazy,目的是深入分析 Lazy.js 中惰性求值的實現,同時由于簡化了實現過程,便于在分享(計劃近期分享)時作為 dem...
摘要:我用替換已經有一段時間了。更快,支持,并且擁有所缺乏的特性。這真是太棒了同樣聲稱類似,但是使用惰性求值,并發布了一些令人印象深刻的速度比較。如果你使用,不管在哪里使用包括,你應該花上幾分鐘切換到。 我用Lo-Dash替換Underscore已經有一段時間了。Lo-Dash更快,支持AMD,并且擁有Underscore所缺乏的特性。同時,Lo-Dash和Underscore是100%兼容...
摘要:本文將講述源碼中,惰性求值的原理和實現。惰性求值中的參數直到需要時才會進行計算。執行的示例圖如下惰性求值做法普通的做法存在一個問題每個方法各做各的事,沒有協調起來浪費了很多資源。 前言 lodash受歡迎的一個原因,是其優異的計算性能。而其性能能有這么突出的表現,很大部分就來源于其使用的算法——惰性求值。本文將講述lodash源碼中,惰性求值的原理和實現。 一、惰性求值的原理分析 惰性...
摘要:本文是函數式編程第三章的讀書筆記,章名為流。正確使用表達式明確要達成什么轉化,而不是說明如何轉化沒有副作用只通過函數的返回值就能充分理解函數的全部作用函數不會修改程序或外界的狀態獲取值而不是變量避免使用數組逃過的追殺,應該考慮優化邏輯 本文是「Java 8 函數式編程」第三章的讀書筆記,章名為流。本章主要介紹了外部迭代與內部迭代以及常用的高階函數。 外部迭代與內部迭代 外部迭代 過去我...
摘要:純函數式狀態隨機數生成器很明顯,原有的函數不是引用透明的,這意味著它難以被測試組合并行化。售貨機在輸出糖果時忽略所有輸入本章知識點惰性求值函數式狀態 第二節?惰性求值與函數式狀態 在下面的代碼中我們對List數據進行了一些處理 List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3) 考慮一下這段程序是如何求值的,如果我們跟蹤一下...
閱讀 1249·2023-04-26 02:38
閱讀 928·2023-04-25 20:13
閱讀 3589·2021-11-19 11:31
閱讀 2396·2019-08-30 15:55
閱讀 2717·2019-08-30 14:11
閱讀 3157·2019-08-30 13:45
閱讀 1371·2019-08-29 18:41
閱讀 1147·2019-08-29 16:18