前言

lodash受欢迎的一个原因,是其优异的计算性能。而其性能能有这么突出的表现,很大部分就来源于其使用的算法——惰性求值。
本文将讲述lodash源码中,惰性求值的原理和实现。

一、惰性求值的原理分析

惰性求值(Lazy Evaluation),又译为惰性计算、懒惰求值,也称为传需求调用(call-by-need),是计算机编程中的一个概念,它的目的是要最小化计算机要做的工作
惰性求值中的参数直到需要时才会进行计算。这种程序实际上是从末尾开始反向执行的。它会判断自己需要返回什么,并继续向后执行来确定要这样做需要哪些值。

以下是How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation.(如何提升Lo-Dash百倍算力?惰性计算的简介)文中的示例,形象地展示惰性求值。

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的数据。

1.1 一般的做法

如果抛开lodash这个工具库,让你用普通的方式实现var chosen = _(gems).filter(priceLt(10)).take(3);那么,可以用以下方式:
_(gems)拿到数据集,缓存起来。
再执行filter方法,遍历gems数组(长度为10),取出符合条件的数据:

[{ 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
执行的示例图如下:

1.2 惰性求值做法

普通的做法存在一个问题:每个方法各做各的事,没有协调起来浪费了很多资源。
如果能先把要做的事,用小本本记下来?,然后等到真正要出数据时,再用最少的次数达到目的,岂不是更好。
惰性计算就是这么做的。
以下是实现的思路:

  • _(gems)拿到数据集,缓存起来
  • 遇到filter方法,先记下来
  • 遇到take方法,先记下来
  • 遇到value方法,说明时机到了
  • 把小本本拿出来,看下要求:要取出3个数,price<10
  • 使用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次,就把结果拿到。
执行的示例图如下:

1.3 小结

从上面的例子可以得到惰性计算的特点:

  • 延迟计算,把要做的计算先缓存,不执行
  • 数据管道,逐个数据通过“裁决”方法,在这个类似安检的过程中,进行过关的操作,最后只留下符合要求的数据
  • 触发时机,方法缓存,那么就需要一个方法来触发执行。lodash就是使用value方法,通知真正开始计算

二、惰性求值的实现

依据上述的特点,我将lodash的惰性求值实现进行抽离为以下几个部分:

2.1 实现延迟计算的缓存

实现_(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__ 记录需要拿的符合要求的数据集个数

这样,一个基本的结构就完成了。

2.2 实现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,对工具内部,是没有任何影响的。

2.3 实现take方法

// 截取n个数据
function take(n){this.__takeCount__ = n;return this;
};LazyWrapper.prototype.take = take;

2.4 实现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;还是会被执行,这是我们不希望看到的。
应该一次性跳出内外两层循环,并且继续外层循环,才是正确的。
标签语句,刚好可以满足这个要求。

2.5 小检测


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]

2.6 小结

整个惰性求值的实现,重点还是在数据管道这块。以及,标签语句在这里的妙用。其实实现的方式,不只当前这种。但是,要点还是前面讲到的三个。掌握精髓,变通就很容易了。

结语

惰性求值,是我在阅读lodash源码中,发现的最大闪光点。
当初对惰性求值不甚理解,想看下javascript的实现,但网上也只找到上文提到的一篇文献。
那剩下的选择,就是对lodash进行剖离分析。也因为这,才有本文的诞生。
希望这篇文章能对你有所帮助。如果可以的话,给个star :)

最后,附上本文实现的简易版lazy.js完整源码:
https://github.com/wall-wxk/blogDemo/blob/master/lodash/lazy.js


喜欢我文章的朋友,可以通过以下方式关注我:

  • 「star」「watch」 我的GitHub blog
  • RSS订阅我的个人博客:王先生的基地

惰性求值——lodash源码解读相关推荐

  1. 惰性求值 php,惰性求值——lodash源码解读

    前言 lodash受欢迎的一个原因,是其优异的计算性能.而其性能能有这么突出的表现,很大部分就来源于其使用的算法--惰性求值. 本文将讲述lodash源码中,惰性求值的原理和实现. 一.惰性求值的原理 ...

  2. c语言中缀表达式求值_[源码和文档分享]基于C++的表达式计算求值

    一.使用说明 1.1 项目简介 表达式求值是程序设计语言编译中的一个最基本的问题,就是将一个表达式转化为逆波兰表达式并求值.具体要求是以字符序列的形式从终端输入语法正确的.不含变量的整数表达式,并利用 ...

  3. lodash 源码解读 _.concat()

    合并多个数组,形成一个简单数组 var other = concat([1], 2,[3],[[[4]]]); // other: [1,2,3,4] 1 function concat() { 2 ...

  4. lodash 源码解读 _.findIndex(obj_array, fn)

    _.findIndex(obj_array, fn), 从对象数组中返回满足条件的第一个对象,如果没有返回-1 var users = [{ 'user': 'barney', 'active': 1 ...

  5. 学习 lodash 源码整体架构,打造属于自己的函数式编程类库

    前言 这是 学习源码整体架构系列第三篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.文章学习的是打包整合后的代码,不是实际仓库中 ...

  6. html 源码知识,源码解读

    用Node.js写一个web服务器,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器 ...

  7. python中关系运算符惰性求值,lazy.js 惰性求值实现分析

    背景:惰性求值? 来看一个 lazy.js 主页提供的示例: var people = getBigArrayOfPeople(); var results = _.chain(people) .pl ...

  8. ThreadLocal源码解读 侵立删

    转自:http://www.cnblogs.com/micrari/p/6790229.html 1. 背景 ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很 ...

  9. 读Lodash源码——chunk.js

    The time is out of joint: O cursed spite, That ever I was born to set it right. --莎士比亚 最艰难的第一步 最近学习遇 ...

最新文章

  1. 人与人的差距在于认知
  2. 「后端小伙伴来学前端了」Vue中利用全局事件总线改造 TodoList 案例
  3. 我的新发现:AVL树旋转的一个特性
  4. CentOS经常使用文件操作命令[百度博客搬家]
  5. mysql in 按顺序排序_Mysql查询结果顺序按in()中ID的顺序排列的实例分析
  6. (单层)感知机学习规则
  7. 揭秘盒马鲜生,如何打破收益增长天花板!
  8. 【干货】--手把手教你完成文本情感分类
  9. 数字电子技术基础(十):SR锁存器
  10. 硬盘格式化了怎么恢复数据
  11. infercnv报错Error in base::rowMeans(x, na.rm = na.rm, dims = dims, ...) : ‘x‘ must be an array of a
  12. 用c语言实现基本数据结构(哈希表)
  13. 什么是绿色工厂?申报绿色工厂对企业有什么好处?
  14. 币圈神话的成就者,谷歌团队首发PlusFo
  15. 免费开放聚合的论文查询下载网站推荐:查询SCI、SSCI、EI、核心期刊、CCF会议论文列表,免费下载论文
  16. python打印实心正方形
  17. Markdown语法拓展 vscode
  18. zynq 维修调试记录 客服支持 工作记录
  19. python多子图导出至pdf_matplotlib保存为pdf,并将子图作为嵌入的矢量图像
  20. MicroPython入门指南

热门文章

  1. qt 5.0中HeaderView的setResiziMode无法使用的问题
  2. 将一个长度最多为30位数字的十进制非负整数转换为二进制数(Java)
  3. 为什么我认为现阶段HIDS处于攻防不对等的地位?(ids、nta、绕过)
  4. linux kvm 常用命令
  5. C++ WINDOWS下 wchar_t *和char * 相互转化总结篇
  6. java课程之团队开发冲刺阶段1.7
  7. 【LOJ】#2887. 「APIO2015」雅加达的摩天楼 Jakarta Skyscrapers
  8. c语言程序设计第四次作业——顺序结构
  9. 【hdu4010】 Query on The Trees
  10. 滑动cell的时候执行动画效果