查看原文
其他

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

若川视野 若川视野 2022-05-01

前言

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

上上篇文章写了 jQuery源码整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库

上一篇文章写了 underscore源码整体架构,学习 underscore 源码整体架构,打造属于自己的函数式编程类库

感兴趣的读者可以点击阅读。

underscore源码分析的文章比较多,而 lodash源码分析的文章比较少。原因之一可能是由于 lodash源码行数太多。注释加起来一万多行。

分析 lodash整体代码结构的文章比较少,笔者利用谷歌、必应、 github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用 lodash,而且都或多或少知道, lodash比 underscore性能好,性能好的主要原因是使用了惰性求值这一特性。

本文章学习的 lodash的版本是: v4.17.15。 unpkg.com地址 https://unpkg.com/lodash@4.17.15/lodash.js

文章篇幅可能比较长,可以先收藏再看。

导读:

文章主要学习了 runInContext() 导出 _ lodash函数使用 baseCreate方法原型继承 LodashWrapper和 LazyWrapper, mixin挂载方法到 lodash.prototype、后文用结合例子解释 lodash.prototype.value(wrapperValue)和 Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

匿名函数执行

  1. ;(function() {


  2. }.call(this));

暴露 lodash

  1. var _ = runInContext();

runInContext 函数

这里的简版源码,只关注函数入口和返回值。

  1. var runInContext = (function runInContext(context) {

  2. // 浏览器中处理context为window

  3. // ...

  4. function lodash(value) {}{

  5. // ...

  6. return new LodashWrapper(value);

  7. }

  8. // ...

  9. return lodash;

  10. });

可以看到申明了一个 runInContext函数。里面有一个 lodash函数,最后处理返回这个 lodash函数。

再看 lodash函数中的返回值 newLodashWrapper(value)

LodashWrapper 函数

  1. function LodashWrapper(value, chainAll) {

  2. this.__wrapped__ = value;

  3. this.__actions__ = [];

  4. this.__chain__ = !!chainAll;

  5. this.__index__ = 0;

  6. this.__values__ = undefined;

  7. }

设置了这些属性:

__wrapped__:存放参数 value

__actions__:存放待执行的函数体 func, 函数参数 args,函数执行的 this 指向 thisArg

__chain__、 undefined两次取反转成布尔值 false,不支持链式调用。和 underscore一样,默认是不支持链式调用的。

__index__:索引值 默认 0。

__values__:主要 clone时使用。

接着往下搜索源码, LodashWrapper, 会发现这两行代码。

  1. LodashWrapper.prototype = baseCreate(baseLodash.prototype);

  2. LodashWrapper.prototype.constructor = LodashWrapper;

接着往上找 baseCreatebaseLodash这两个函数。

baseCreate 原型继承

  1. // 立即执行匿名函数

  2. // 返回一个函数,用于设置原型 可以理解为是 __proto__

  3. var baseCreate = (function() {

  4. // 这句放在函数外,是为了不用每次调用baseCreate都重复申明 object

  5. // underscore 源码中,把这句放在开头就申明了一个空函数 `Ctor`

  6. function object() {}

  7. return function(proto) {

  8. // 如果传入的参数不是object也不是function 是null

  9. // 则返回空对象。

  10. if (!isObject(proto)) {

  11. return {};

  12. }

  13. // 如果支持Object.create方法,则返回 Object.create

  14. if (objectCreate) {

  15. // Object.create

  16. return objectCreate(proto);

  17. }

  18. // 如果不支持Object.create 用 ployfill new

  19. object.prototype = proto;

  20. var result = new object;

  21. // 还原 prototype

  22. object.prototype = undefined;

  23. return result;

  24. };

  25. }());


  26. // 空函数

  27. function baseLodash() {

  28. // No operation performed.

  29. }


  30. // Ensure wrappers are instances of `baseLodash`.

  31. lodash.prototype = baseLodash.prototype;

  32. // 为什么会有这一句?因为上一句把lodash.prototype.construtor 设置为Object了。这一句修正constructor

  33. lodash.prototype.constructor = lodash;


  34. LodashWrapper.prototype = baseCreate(baseLodash.prototype);

  35. LodashWrapper.prototype.constructor = LodashWrapper;

笔者画了一张图,表示这个关系。 

衍生的 isObject 函数

判断 typeofvalue不等于 null,并且是 object或者 function

  1. function isObject(value) {

  2. var type = typeof value;

  3. return value != null && (type == 'object' || type == 'function');

  4. }

Object.create() 用法举例

面试官问:能否模拟实现JS的new操作符 之前这篇文章写过的一段。

笔者之前整理的一篇文章中也有讲过,可以翻看JavaScript 对象所有API解析

MDN Object.create()

Object.create(proto,[propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是 undefined)。

  1. var anotherObject = {

  2. name: '若川'

  3. };

  4. var myObject = Object.create(anotherObject, {

  5. age: {

  6. value:18,

  7. },

  8. });

  9. // 获得它的原型

  10. Object.getPrototypeOf(anotherObject) === Object.prototype; // true 说明anotherObject的原型是Object.prototype

  11. Object.getPrototypeOf(myObject); // {name: "若川"} // 说明myObject的原型是{name: "若川"}

  12. myObject.hasOwnProperty('name'); // false; 说明name是原型上的。

  13. myObject.hasOwnProperty('age'); // true 说明age是自身的

  14. myObject.name; // '若川'

  15. myObject.age; // 18;

对于不支持 ES5的浏览器, MDN上提供了 ployfill方案。

  1. if (typeof Object.create !== "function") {

  2. Object.create = function (proto, propertiesObject) {

  3. if (typeof proto !== 'object' && typeof proto !== 'function') {

  4. throw new TypeError('Object prototype may only be an Object: ' + proto);

  5. } else if (proto === null) {

  6. throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");

  7. }


  8. if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");


  9. function F() {}

  10. F.prototype = proto;

  11. return new F();

  12. };

  13. }

lodash上有很多方法和属性,但在 lodash.prototype也有很多与 lodash上相同的方法。肯定不是在 lodash.prototype上重新写一遍。而是通过 mixin挂载的。

mixin

mixin 具体用法

  1. _.mixin([object=lodash], source, [options={}])

添加来源对象自身的所有可枚举函数属性到目标对象。如果 object 是个函数,那么函数方法将被添加到原型链上。

注意: 使用 _.runInContext 来创建原始的 lodash 函数来避免修改造成的冲突。

添加版本

0.1.0

参数

[object=lodash] (Function|Object): 目标对象。

source (Object): 来源对象。

[options={}] (Object): 选项对象。

[options.chain=true] (boolean): 是否开启链式操作。

返回

(*): 返回 object.

mixin 源码

mixin源码,后文注释解析

  1. function mixin(object, source, options) {

  2. var props = keys(source),

  3. methodNames = baseFunctions(source, props);


  4. if (options == null &&

  5. !(isObject(source) && (methodNames.length || !props.length))) {

  6. options = source;

  7. source = object;

  8. object = this;

  9. methodNames = baseFunctions(source, keys(source));

  10. }

  11. var chain = !(isObject(options) && 'chain' in options) || !!options.chain,

  12. isFunc = isFunction(object);


  13. arrayEach(methodNames, function(methodName) {

  14. var func = source[methodName];

  15. object[methodName] = func;

  16. if (isFunc) {

  17. object.prototype[methodName] = function() {

  18. var chainAll = this.__chain__;

  19. if (chain || chainAll) {

  20. var result = object(this.__wrapped__),

  21. actions = result.__actions__ = copyArray(this.__actions__);


  22. actions.push({ 'func': func, 'args': arguments, 'thisArg': object });

  23. result.__chain__ = chainAll;

  24. return result;

  25. }

  26. return func.apply(object, arrayPush([this.value()], arguments));

  27. };

  28. }

  29. });


  30. return object;

  31. }

接下来先看衍生的函数。

其实看到具体定义的函数代码就大概知道这个函数的功能。为了不影响主线,导致文章篇幅过长。具体源码在这里就不展开。

感兴趣的读者可以自行看这些函数衍生的其他函数的源码。

mixin 衍生的函数 keys

在 mixin 函数中 其实最终调用的就是 Object.keys

  1. function keys(object) {

  2. return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);

  3. }

mixin 衍生的函数 baseFunctions

返回函数数组集合

  1. function baseFunctions(object, props) {

  2. return arrayFilter(props, function(key) {

  3. return isFunction(object[key]);

  4. });

  5. }

mixin 衍生的函数 isFunction

判断参数是否是函数

  1. function isFunction(value) {

  2. if (!isObject(value)) {

  3. return false;

  4. }

  5. // The use of `Object#toString` avoids issues with the `typeof` operator

  6. // in Safari 9 which returns 'object' for typed arrays and other constructors.

  7. var tag = baseGetTag(value);

  8. return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;

  9. }

mixin 衍生的函数 arrayEach

类似 [].forEarch

  1. function arrayEach(array, iteratee) {

  2. var index = -1,

  3. length = array == null ? 0 : array.length;


  4. while (++index < length) {

  5. if (iteratee(array[index], index, array) === false) {

  6. break;

  7. }

  8. }

  9. return array;

  10. }

mixin 衍生的函数 arrayPush

类似 [].push

  1. function arrayPush(array, values) {

  2. var index = -1,

  3. length = values.length,

  4. offset = array.length;


  5. while (++index < length) {

  6. array[offset + index] = values[index];

  7. }

  8. return array;

  9. }

mixin 衍生的函数 copyArray

拷贝数组

  1. function copyArray(source, array) {

  2. var index = -1,

  3. length = source.length;


  4. array || (array = Array(length));

  5. while (++index < length) {

  6. array[index] = source[index];

  7. }

  8. return array;

  9. }

mixin 源码解析

lodash 源码中两次调用 mixin

  1. // Add methods that return wrapped values in chain sequences.

  2. lodash.after = after;

  3. // code ... 等 153 个支持链式调用的方法


  4. // Add methods to `lodash.prototype`.

  5. // 把lodash上的静态方法赋值到 lodash.prototype 上

  6. mixin(lodash, lodash);


  7. // Add methods that return unwrapped values in chain sequences.

  8. lodash.add = add;

  9. // code ... 等 152 个不支持链式调用的方法



  10. // 这里其实就是过滤 after 等支持链式调用的方法,获取到 lodash 上的 add 等 添加到lodash.prototype 上。

  11. mixin(lodash, (function() {

  12. var source = {};

  13. // baseForOwn 这里其实就是遍历lodash上的静态方法,执行回调函数

  14. baseForOwn(lodash, function(func, methodName) {

  15. // 第一次 mixin 调用了所以赋值到了lodash.prototype

  16. // 所以这里用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 个不支持链式调用的方法。

  17. if (!hasOwnProperty.call(lodash.prototype, methodName)) {

  18. source[methodName] = func;

  19. }

  20. });

  21. return source;

  22. // 最后一个参数options 特意注明不支持链式调用

  23. }()), { 'chain': false });

结合两次调用 mixin 代入到源码解析如下 mixin源码及注释

  1. function mixin(object, source, options) {

  2. // source 对象中可以枚举的属性

  3. var props = keys(source),

  4. // source 对象中的方法名称数组

  5. methodNames = baseFunctions(source, props);


  6. if (options == null &&

  7. !(isObject(source) && (methodNames.length || !props.length))) {

  8. // 如果 options 没传为 undefined undefined == null 为true

  9. // 且 如果source 不为 对象或者不是函数

  10. // 且 source对象的函数函数长度 或者 source 对象的属性长度不为0

  11. // 把 options 赋值为 source

  12. options = source;

  13. // 把 source 赋值为 object

  14. source = object;

  15. // 把 object 赋值为 this 也就是 _ (lodash)

  16. object = this;

  17. // 获取到所有的方法名称数组

  18. methodNames = baseFunctions(source, keys(source));

  19. }

  20. // 是否支持 链式调用

  21. // options 不是对象或者不是函数,是null或者其他值

  22. // 判断options是否是对象或者函数,如果不是或者函数则不会执行 'chain' in options 也就不会报错

  23. // 且 chain 在 options的对象或者原型链中

  24. // 知识点 in [MDN in : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in

  25. // 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。


  26. // 或者 options.chain 转布尔值

  27. var chain = !(isObject(options) && 'chain' in options) || !!options.chain,

  28. // object 是函数

  29. isFunc = isFunction(object);


  30. // 循环 方法名称数组

  31. arrayEach(methodNames, function(methodName) {

  32. // 函数本身

  33. var func = source[methodName];

  34. // object 通常是 lodash 也赋值这个函数。

  35. object[methodName] = func;

  36. if (isFunc) {

  37. // 如果object是函数 赋值到 object prototype 上,通常是lodash

  38. object.prototype[methodName] = function() {

  39. // 实例上的__chain__ 属性 是否支持链式调用

  40. // 这里的 this 是 new LodashWrapper 实例 类似如下

  41. /**

  42. {

  43. __actions__: [],

  44. __chain__: true

  45. __index__: 0

  46. __values__: undefined

  47. __wrapped__: []

  48. }

  49. **/


  50. var chainAll = this.__chain__;

  51. // options 中的 chain 属性 是否支持链式调用

  52. // 两者有一个符合链式调用 执行下面的代码

  53. if (chain || chainAll) {

  54. // 通常是 lodash

  55. var result = object(this.__wrapped__),

  56. // 复制 实例上的 __action__ 到 result.__action__ 和 action 上

  57. actions = result.__actions__ = copyArray(this.__actions__);


  58. // action 添加 函数 和 args 和 this 指向,延迟计算调用。

  59. actions.push({ 'func': func, 'args': arguments, 'thisArg': object });

  60. //实例上的__chain__ 属性 赋值给 result 的 属性 __chain__

  61. result.__chain__ = chainAll;

  62. // 最后返回这个实例

  63. return result;

  64. }


  65. // 都不支持链式调用。直接调用

  66. // 把当前实例的 value 和 arguments 对象 传递给 func 函数作为参数调用。返回调用结果。

  67. return func.apply(object, arrayPush([this.value()], arguments));

  68. };

  69. }

  70. });


  71. // 最后返回对象 object

  72. return object;

  73. }

小结:简单说就是把 lodash上的静态方法赋值到 lodash.prototype上。分两次第一次是支持链式调用( lodash.after等 153个支持链式调用的方法),第二次是不支持链式调用的方法( lodash.add等 152个不支持链式调用的方法)。

lodash 究竟在.prototype挂载了多少方法和属性

再来看下 lodash究竟挂载在 _函数对象上有多少静态方法和属性,和挂载 _.prototype上有多少方法和属性。

使用 forin循环一试便知。看如下代码:

  1. var staticMethods = [];

  2. var staticProperty = [];

  3. for(var name in _){

  4. if(typeof _[name] === 'function'){

  5. staticMethods.push(name);

  6. }

  7. else{

  8. staticProperty.push(name);

  9. }

  10. }

  11. console.log(staticProperty); // ["templateSettings", "VERSION"] 2个

  12. console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305个

其实就是上文提及的 lodash.after 等 153个支持链式调用的函数 、 lodash.add 等 152不支持链式调用的函数赋值而来。

  1. var prototypeMethods = [];

  2. var prototypeProperty = [];

  3. for(var name in _.prototype){

  4. if(typeof _.prototype[name] === 'function'){

  5. prototypeMethods.push(name);

  6. }

  7. else{

  8. prototypeProperty.push(name);

  9. }

  10. }

  11. console.log(prototypeProperty); // []

  12. console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317个

相比 lodash上的静态方法多了 12个,说明除了 mixin 外,还有 12个其他形式赋值而来。

支持链式调用的方法最后返回是实例对象,获取最后的处理的结果值,最后需要调用 value方法。

笔者画了一张表示 lodash的方法和属性挂载关系图。

请出贯穿下文的简单的例子

  1. var result = _.chain([1, 2, 3, 4, 5])

  2. .map(el => {

  3. console.log(el); // 1, 2, 3

  4. return el + 1;

  5. })

  6. .take(3)

  7. .value();

  8. // lodash中这里的`map`仅执行了`3`次。

  9. // 具体功能也很简单 数组 1-5 加一,最后获取其中三个值。

  10. console.log('result:', result);

也就是说这里 lodash聪明的知道了最后需要几个值,就执行几次 map循环,对于很大的数组,提升性能很有帮助。
而 underscore执行这段代码其中 map执行了5次。如果是平常实现该功能也简单。

  1. var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3);

  2. console.log('result:', result);

而相比 lodash这里的 map执行了 5次。

  1. // 不使用 map、slice

  2. var result = [];

  3. var arr = [1, 2, 3, 4, 5];

  4. for (var i = 0; i < 3; i++){

  5. result[i] = arr[i] + 1;

  6. }

  7. console.log(result, 'result');

简单说这里的 map方法,添加 LazyWrapper 的方法到 lodash.prototype存储下来,最后调用 value时再调用。具体看下文源码实现。

添加 LazyWrapper 的方法到 lodash.prototype

主要是如下方法添加到到 lodash.prototype 原型上。

  1. // "constructor"

  2. ["drop", "dropRight", "take", "takeRight", "filter", "map", "takeWhile", "head", "last", "initial", "tail", "compact", "find", "findLast", "invokeMap", "reject", "slice", "takeRightWhile", "toArray", "clone", "reverse", "value"]

具体源码及注释

  1. // Add `LazyWrapper` methods to `lodash.prototype`.

  2. // baseForOwn 这里其实就是遍历LazyWrapper.prototype上的方法,执行回调函数

  3. baseForOwn(LazyWrapper.prototype, function(func, methodName) {

  4. // 检测函数名称是否是迭代器也就是循环

  5. var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),

  6. // 检测函数名称是否head和last

  7. // 顺便提一下 ()这个是捕获分组 而加上 ?: 则是非捕获分组 也就是说不用于其他操作

  8. isTaker = /^(?:head|last)$/.test(methodName),

  9. // lodashFunc 是 根据 isTaker 组合 takeRight take methodName

  10. lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],

  11. // 根据isTaker 和 是 find 判断结果是否 包装

  12. retUnwrapped = isTaker || /^find/.test(methodName);


  13. // 如果不存在这个函数,就不往下执行

  14. if (!lodashFunc) {

  15. return;

  16. }

  17. // 把 lodash.prototype 方法赋值到lodash.prototype

  18. lodash.prototype[methodName] = function() {

  19. // 取实例中的__wrapped__ 值 例子中则是 [1,2,3,4,5]

  20. var value = this.__wrapped__,

  21. // 如果是head和last 方法 isTaker 返回 [1], 否则是arguments对象

  22. args = isTaker ? [1] : arguments,

  23. // 如果value 是LayeWrapper的实例

  24. isLazy = value instanceof LazyWrapper,

  25. // 迭代器 循环

  26. iteratee = args[0],

  27. // 使用useLazy isLazy value或者是数组

  28. useLazy = isLazy || isArray(value);


  29. var interceptor = function(value) {

  30. // 函数执行 value args 组合成数组参数

  31. var result = lodashFunc.apply(lodash, arrayPush([value], args));

  32. // 如果是 head 和 last (isTaker) 支持链式调用 返回结果的第一个参数 否则 返回result

  33. return (isTaker && chainAll) ? result[0] : result;

  34. };


  35. // useLazy true 并且 函数checkIteratee 且迭代器是函数,且迭代器参数个数不等于1

  36. if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {

  37. // Avoid lazy use if the iteratee has a "length" value other than `1`.

  38. // useLazy 赋值为 false

  39. // isLazy 赋值为 false

  40. isLazy = useLazy = false;

  41. }

  42. // 取实例上的 __chain__

  43. var chainAll = this.__chain__,

  44. // 存储的待执行的函数 __actions__ 二次取反是布尔值 也就是等于0或者大于0两种结果

  45. isHybrid = !!this.__actions__.length,

  46. // 是否不包装 用结果是否不包装 且 不支持链式调用

  47. isUnwrapped = retUnwrapped && !chainAll,

  48. // 是否仅Lazy 用isLazy 和 存储的函数

  49. onlyLazy = isLazy && !isHybrid;


  50. // 结果不包装 且 useLazy 为 true

  51. if (!retUnwrapped && useLazy) {

  52. // 实例 new LazyWrapper 这里的this 是 new LodashWrapper()

  53. value = onlyLazy ? value : new LazyWrapper(this);

  54. // result 执行函数结果

  55. var result = func.apply(value, args);


  56. /*

  57. *

  58. // _.thru(value, interceptor)

  59. // 这个方法类似 _.tap, 除了它返回 interceptor 的返回结果。该方法的目的是"传递" 值到一个方法链序列以取代中间结果。

  60. _([1, 2, 3])

  61. .tap(function(array) {

  62. // 改变传入的数组

  63. array.pop();

  64. })

  65. .reverse()

  66. .value();

  67. // => [2, 1]

  68. */


  69. // thisArg 指向undefined 或者null 非严格模式下是指向window,严格模式是undefined 或者nll

  70. result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });

  71. // 返回实例 lodashWrapper

  72. return new LodashWrapper(result, chainAll);

  73. }

  74. // 不包装 且 onlyLazy 为 true

  75. if (isUnwrapped && onlyLazy) {

  76. // 执行函数

  77. return func.apply(this, args);

  78. }

  79. // 上面都没有执行,执行到这里了

  80. // 执行 thru 函数,回调函数 是 interceptor

  81. result = this.thru(interceptor);

  82. return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;

  83. };

  84. });

小结一下,写了这么多注释,简单说:其实就是用 LazyWrapper.prototype 改写原先在 lodash.prototype的函数,判断函数是否需要使用惰性求值,需要时再调用。

读者可以断点调试一下,善用断点进入函数功能,对着注释看,可能会更加清晰。

断点调试的部分截图

链式调用最后都是返回实例对象,实际的处理数据的函数都没有调用,而是被存储存储下来了,最后调用 value方法,才执行这些函数。

lodash.prototype.value 即 wrapperValue

  1. function baseWrapperValue(value, actions) {

  2. var result = value;

  3. // 如果是lazyWrapper的实例,则调用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法

  4. if (result instanceof LazyWrapper) {

  5. result = result.value();

  6. }

  7. // 类似 [].reduce(),把上一个函数返回结果作为参数传递给下一个函数

  8. return arrayReduce(actions, function(result, action) {

  9. return action.func.apply(action.thisArg, arrayPush([result], action.args));

  10. }, result);

  11. }

  12. function wrapperValue() {

  13. return baseWrapperValue(this.__wrapped__, this.__actions__);

  14. }

  15. lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;

如果是惰性求值,则调用的是 LazyWrapper.prototype.value 即 lazyValue

LazyWrapper.prototype.value 即 lazyValue 惰性求值

lazyValue源码及注释

  1. function LazyWrapper(value) {

  2. // 参数 value

  3. this.__wrapped__ = value;

  4. // 执行的函数

  5. this.__actions__ = [];

  6. this.__dir__ = 1;

  7. // 过滤

  8. this.__filtered__ = false;

  9. // 存储迭代器函数

  10. this.__iteratees__ = [];

  11. // 默认最大取值个数

  12. this.__takeCount__ = MAX_ARRAY_LENGTH;

  13. // 具体取值多少个,存储函数和类型

  14. this.__views__ = [];

  15. }

  16. /**

  17. * Extracts the unwrapped value from its lazy wrapper.

  18. *

  19. * @private

  20. * @name value

  21. * @memberOf LazyWrapper

  22. * @returns {*} Returns the unwrapped value.

  23. */

  24. function lazyValue() {

  25. // this.__wrapped__ 是 new LodashWrapper 实例 所以执行.value 获取原始值

  26. var array = this.__wrapped__.value(),

  27. //

  28. dir = this.__dir__,

  29. // 是否是函数

  30. isArr = isArray(array),

  31. // 是否从右边开始

  32. isRight = dir < 0,

  33. // 数组的长度。如果不是数组,则是0

  34. arrLength = isArr ? array.length : 0,

  35. // 获取 take(3) 上述例子中 则是 start: 0,end: 3

  36. view = getView(0, arrLength, this.__views__),

  37. start = view.start,

  38. end = view.end,

  39. // 长度 3

  40. length = end - start,

  41. // 如果是是从右开始

  42. index = isRight ? end : (start - 1),

  43. // 存储的迭代器数组

  44. iteratees = this.__iteratees__,

  45. // 迭代器数组长度

  46. iterLength = iteratees.length,

  47. // 结果resIndex

  48. resIndex = 0,

  49. // 最后获取几个值,也就是 3

  50. takeCount = nativeMin(length, this.__takeCount__);


  51. // 如果不是数组,或者 不是从右开始 并且 参数数组长度等于take的长度 takeCount等于长度

  52. // 则直接调用 baseWrapperValue 不需要

  53. if (!isArr || (!isRight && arrLength == length && takeCount == length)) {

  54. return baseWrapperValue(array, this.__actions__);

  55. }

  56. var result = [];


  57. // 标签语句 label

  58. // MDN label 链接

  59. // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label

  60. // 标记语句可以和 break 或 continue 语句一起使用。标记就是在一条语句前面加个可以引用的标识符(identifier)。

  61. outer:

  62. while (length-- && resIndex < takeCount) {

  63. index += dir;


  64. var iterIndex = -1,

  65. // 数组第一项

  66. value = array[index];


  67. while (++iterIndex < iterLength) {

  68. // 迭代器数组 {iteratee: function{}, typy: 2}

  69. var data = iteratees[iterIndex],

  70. iteratee = data.iteratee,

  71. type = data.type,

  72. // 结果 迭代器执行结果

  73. computed = iteratee(value);


  74. if (type == LAZY_MAP_FLAG) {

  75. // 如果 type 是 map 类型,结果 computed 赋值给value

  76. value = computed;

  77. } else if (!computed) {

  78. if (type == LAZY_FILTER_FLAG) {

  79. // 退出当前这次循环,进行下一次循环

  80. continue outer;

  81. } else {

  82. // 退出整个循环

  83. break outer;

  84. }

  85. }

  86. }

  87. // 最终数组

  88. result[resIndex++] = value;

  89. }

  90. // 返回数组 例子中则是 [2, 3, 4]

  91. return result;

  92. }

  93. // Ensure `LazyWrapper` is an instance of `baseLodash`.

  94. LazyWrapper.prototype = baseCreate(baseLodash.prototype);

  95. LazyWrapper.prototype.constructor = LazyWrapper;


  96. LazyWrapper.prototype.value = lazyValue;

笔者画了一张 lodash和 LazyWrapper的关系图来表示。 

小结: lazyValue简单说实现的功能就是把之前记录的需要执行几次,把记录存储的函数执行几次,不会有多少项数据就执行多少次,而是根据需要几项,执行几项。也就是说以下这个例子中, map函数只会执行 3次。如果没有用惰性求值,那么 map函数会执行 5次。

  1. var result = _.chain([1, 2, 3, 4, 5])

  2. .map(el => el + 1)

  3. .take(3)

  4. .value();

总结

行文至此,基本接近尾声,最后总结一下。

文章主要学习了 runInContext() 导出 _ lodash函数使用 baseCreate方法原型继承 LodashWrapper和 LazyWrapper, mixin挂载方法到 lodash.prototype、后文用结合例子解释 lodash.prototype.value(wrapperValue)和 Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

分享一个只知道函数名找源码定位函数申明位置的 VSCode 技巧: Ctrl+p。输入 @functionName 定位函数 functionName在源码文件中的具体位置。如果知道调用位置,那直接按 alt+鼠标左键即可跳转到函数申明的位置。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

推荐阅读

lodash github仓库
lodash 官方文档
lodash 中文文档
打造一个类似于lodash的前端工具库
惰性求值——lodash源码解读
luobo tang:lazy.js 惰性求值实现分析
lazy.js github 仓库
本文章学习的 lodash的版本 v4.17.15 unpkg.com链接

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 http://lxchuan12.github.io 使用 vuepress重构了,阅读体验可能更好些
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

微信交流群,加我微信lxchuan12,注明来源,拉您进前端视野交流群

下图是公众号二维码:若川视野,一个可能比较有趣的前端开发类公众号,目前前端内容不多

往期文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟

学习 jQuery 源码整体架构,打造属于自己的 js 类库

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


由于公众号限制外链,点击阅读原文,或许阅读体验更佳

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存