lodash受欢迎的一个缘由,是其优异的计算性能。而其性能能有这么突出的表现,很大部分就来源于其使用的算法——惰性求值。
本文将讲述lodash源码中,惰性求值的原理和实现。javascript
惰性求值(Lazy Evaluation),又译为惰性计算、懒惰求值,也称为传需求调用(call-by-need),是计算机编程中的一个概念,它的目的是要 最小化计算机要作的工做。
惰性求值中的参数直到须要时才会进行计算。这种程序其实是 从末尾开始反向执行的。它会判断本身须要返回什么,并继续向后执行来肯定要这样作须要哪些值。
如下是How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation.(如何提高Lo-Dash百倍算力?惰性计算的简介)文中的示例,形象地展现惰性求值。java
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的数据。git
若是抛开lodash
这个工具库,让你用普通的方式实现var chosen = _(gems).filter(priceLt(10)).take(3)
;那么,能够用如下方式: _(gems)
拿到数据集,缓存起来。
再执行filter
方法,遍历gems
数组(长度为10),取出符合条件的数据:github
[ { 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
。
执行的示例图以下:编程
普通的作法存在一个问题:每一个方法各作各的事,没有协调起来浪费了不少资源。
若是能先把要作的事,用小本本记下来😎,而后等到真正要出数据时,再用最少的次数达到目的,岂不是更好。
惰性计算就是这么作的。
如下是实现的思路:数组
_(gems)
拿到数据集,缓存起来filter
方法,先记下来take
方法,先记下来value
方法,说明时机到了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次,就把结果拿到。
执行的示例图以下:缓存
从上面的例子能够获得惰性计算的特色:数据结构
value
方法,通知真正开始计算依据上述的特色,我将lodash的惰性求值实现进行抽离为如下几个部分:app
实现_(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__
记录须要拿的符合要求的数据集个数这样,一个基本的结构就完成了。
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
,对工具内部,是没有任何影响的。
take
方法// 截取n个数据 function take(n){ this.__takeCount__ = n; return this; }; LazyWrapper.prototype.take = take;
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;
仍是会被执行,这是咱们不但愿看到的。
应该一次性跳出内外两层循环,而且继续外层循环,才是正确的。
标签语句,恰好能够知足这个要求。
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]
整个惰性求值的实现,重点仍是在数据管道这块。以及,标签语句在这里的妙用。其实实现的方式,不仅当前这种。可是,要点仍是前面讲到的三个。掌握精髓,变通就很容易了。
惰性求值,是我在阅读lodash
源码中,发现的最大闪光点。
当初对惰性求值不甚理解,想看下javascript的实现,但网上也只找到上文提到的一篇文献。
那剩下的选择,就是对lodash进行剖离分析。也由于这,才有本文的诞生。
但愿这篇文章能对你有所帮助。若是能够的话,给个star :)
最后,附上本文实现的简易版lazy.js
完整源码:
https://github.com/wall-wxk/blogDemo/blob/master/lodash/lazy.js
喜欢我文章的朋友,能够经过如下方式关注我: