来看一个 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 的惰性求值的分析仅是我我的的看法,若是错漏,欢迎指正!
最后,感谢阅读!