lodash 笔记:惰性求值

962 阅读2分钟

理解惰性求值

惰性求值,又叫做惰性计算,与其相反的是及早求值。

大多数的编程语言默认的是及早求值。

于下例代码:

a = b + c
print a
  • 及早求值:add(b, c) 当即放回一个值,赋值给 a,在第二行打印 a 的值。
  • 惰性求值:add(b, c) 过程被赋值给 a,而不是返回值,在第二行需要 a 的时候执行其存储的过程打印返回值。

惰性求值在处理大量数据的时候,可以在性能和可读性上取得极好平衡。

用 lodash 示例:

_(_.range(10000))
  .map(i => console.log(i))
  .take(2).value()

代码运行时,只打印了两行,takemap 的顺序没有影响结果。maptake 都没有被立即执行,直到 value() 这种出口才被执行。

与之相反的是 js 自带的 map()

_.range(10000)
  .map(i => console.log(i))
  .slice(0, 2)

程序在 map() 的时候便完全执行了,打印了整整 1 万行。而事实上,绝大多数的数据是没有必要处理的,因为被 slice() 丢弃掉了。

惰性求值性能比及早求值好?

不一定。论性能肯定是汇编语言最好了,它是及早求值的。

看下面一个例子:

const data = _(_.range(4)).map(i => console.log(i))

data.take(3).value()
data.take(2).value()

上面这段代码输出了 5 行(比及早求值的方法输出 4 行还多 1 行),因为有重复的过程被执行了。

用 lodash 的时候要注意到链式调用的分叉,自行判断一下是否先用 value() 完成过程进行求值比较好。

我个人经验是,绝大多数时候,处理的数据必定是来自读写接口,去向读写接口。得益于惰性计算,不必一次将所有数据写入内存,而是依个处理的,像流一样。那么就算是重复了一些过程,也省下了内存,性能上孰优孰劣就不好说了。

const files = _(paths).map(p => fs.readFileSync(p)) // 此时没有读取文件

files.filter(isTypeA)
  .map(handlerA)
  .forEach(f => fs.writeFileSync(...)) // forEach 是出口,此时文件才被读取

files.filter(isTypeB)
   .map(handlerB)
   .forEach(f => fs.writeFileSync(...))

以上代码中,假定typeAtypeB 存在交集,即会有重复读取交集中的文件,但是其读进并处理后就立即被写出了。

若是写成及早求值的方式,在 paths.map() 中所有文件被读取到内存中,就可能造成内存不够。所以只能使用非函数式的循环判断,而这样嵌套的代码确实不如链式调用好阅读(当代码量上去的时候就更容易观察到):

for (const p of paths) {
    const f = fs.readFileSync(p)
    if(isTypeA(f)) {
      const result = handlerA(f)
      fs.writeSync(result)
    }
    if(isTypeB(f)) {
      const result = handlerB(f)
      fs.writeSync(result)
    }
})