學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫

前言

這是 學習源碼總體架構系列第三篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。前端

上上篇文章寫了 jQuery源碼總體架構,學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫git

上一篇文章寫了 underscore源碼總體架構,學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫github

感興趣的讀者能夠點擊閱讀。面試

underscore源碼分析的文章比較多,而 lodash源碼分析的文章比較少。緣由之一多是因爲 lodash源碼行數太多。註釋加起來一萬多行。編程

分析 lodash總體代碼結構的文章比較少,筆者利用谷歌、必應、 github等搜索都沒有找到,多是找的方式不對。因而打算本身寫一篇。日常開發大多數人都會使用 lodash,並且都或多或少知道, lodash比 underscore性能好,性能好的主要緣由是使用了惰性求值這一特性。數組

本文章學習的 lodash的版本是: v4.17.15。 unpkg.com地址 https://unpkg.com/lodash@4.17.15/lodash.js瀏覽器

文章篇幅可能比較長,能夠先收藏再看。架構

導讀:app

文章主要學習了 runInContext() 導出 _ lodash函數使用 baseCreate方法原型繼承 LodashWrapper和 LazyWrapper, mixin掛載方法到 lodash.prototype、後文用結合例子解釋 lodash.prototype.value(wrapperValue)和 Lazy.prototype.value(lazyValue)惰性求值的源碼具體實現。async

匿名函數執行

  1. ;(function() {

  2.  

  3. }.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;

接着往上找 baseCreate、baseLodash這兩個函數。

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. // 空函數

  28. function baseLodash() {

  29. // No operation performed.

  30. }

  31.  

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

  33. lodash.prototype = baseLodash.prototype;

  34. // 爲何會有這一句?由於上一句把lodash.prototype.construtor 設置爲Object了。這一句修正constructor

  35. lodash.prototype.constructor = lodash;

  36.  

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

  38. LodashWrapper.prototype.constructor = LodashWrapper;

筆者畫了一張圖,表示這個關係。 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

衍生的 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.  

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

  10.  

  11. function F() {}

  12. F.prototype = proto;

  13. return new F();

  14. };

  15. }

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.  

  5. if (options == null &&

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

  7. options = source;

  8. source = object;

  9. object = this;

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

  11. }

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

  13. isFunc = isFunction(object);

  14.  

  15. arrayEach(methodNames, function(methodName) {

  16. var func = source[methodName];

  17. object[methodName] = func;

  18. if (isFunc) {

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

  20. var chainAll = this.__chain__;

  21. if (chain || chainAll) {

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

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

  24.  

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

  26. result.__chain__ = chainAll;

  27. return result;

  28. }

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

  30. };

  31. }

  32. });

  33.  

  34. return object;

  35. }

接下來先看衍生的函數。

其實看到具體定義的函數代碼就大概知道這個函數的功能。爲了避免影響主線,致使文章篇幅過長。具體源碼在這裏就不展開。

感興趣的讀者能夠自行看這些函數衍生的其餘函數的源碼。

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.  

  5. while (++index < length) {

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

  7. break;

  8. }

  9. }

  10. return array;

  11. }

mixin 衍生的函數 arrayPush

相似 [].push

  1. function arrayPush(array, values) {

  2. var index = -1,

  3. length = values.length,

  4. offset = array.length;

  5.  

  6. while (++index < length) {

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

  8. }

  9. return array;

  10. }

mixin 衍生的函數 copyArray

拷貝數組

  1. function copyArray(source, array) {

  2. var index = -1,

  3. length = source.length;

  4.  

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

  6. while (++index < length) {

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

  8. }

  9. return array;

  10. }

mixin 源碼解析

lodash 源碼中兩次調用 mixin

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

  2. lodash.after = after;

  3. // code ... 等 153 個支持鏈式調用的方法

  4.  

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

  6. // 把lodash上的靜態方法賦值到 lodash.prototype 上

  7. mixin(lodash, lodash);

  8.  

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

  10. lodash.add = add;

  11. // code ... 等 152 個不支持鏈式調用的方法

  12.  

  13.  

  14. // 這裏其實就是過濾 after 等支持鏈式調用的方法,獲取到 lodash 上的 add 等 添加到lodash.prototype 上。

  15. mixin(lodash, (function() {

  16. var source = {};

  17. // baseForOwn 這裏其實就是遍歷lodash上的靜態方法,執行回調函數

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

  19. // 第一次 mixin 調用了因此賦值到了lodash.prototype

  20. // 因此這裏用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 個不支持鏈式調用的方法。

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

  22. source[methodName] = func;

  23. }

  24. });

  25. return source;

  26. // 最後一個參數options 特地註明不支持鏈式調用

  27. }()), { '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.  

  7. if (options == null &&

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

  9. // 若是 options 沒傳爲 undefined undefined == null 爲true

  10. // 且 若是source 不爲 對象或者不是函數

  11. // 且 source對象的函數函數長度 或者 source 對象的屬性長度不爲0

  12. // 把 options 賦值爲 source

  13. options = source;

  14. // 把 source 賦值爲 object

  15. source = object;

  16. // 把 object 賦值爲 this 也就是 _ (lodash)

  17. object = this;

  18. // 獲取到全部的方法名稱數組

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

  20. }

  21. // 是否支持 鏈式調用

  22. // options 不是對象或者不是函數,是null或者其餘值

  23. // 判斷options是不是對象或者函數,若是不是或者函數則不會執行 'chain' in options 也就不會報錯

  24. // 且 chain 在 options的對象或者原型鏈中

  25. // 知識點 in [MDN in : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in

  26. // 若是指定的屬性在指定的對象或其原型鏈中,則in 運算符返回true。

  27.  

  28. // 或者 options.chain 轉布爾值

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

  30. // object 是函數

  31. isFunc = isFunction(object);

  32.  

  33. // 循環 方法名稱數組

  34. arrayEach(methodNames, function(methodName) {

  35. // 函數自己

  36. var func = source[methodName];

  37. // object 一般是 lodash 也賦值這個函數。

  38. object[methodName] = func;

  39. if (isFunc) {

  40. // 若是object是函數 賦值到 object prototype 上,一般是lodash

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

  42. // 實例上的__chain__ 屬性 是否支持鏈式調用

  43. // 這裏的 this 是 new LodashWrapper 實例 相似以下

  44. /**

  45. {

  46. __actions__: [],

  47. __chain__: true

  48. __index__: 0

  49. __values__: undefined

  50. __wrapped__: []

  51. }

  52. **/

  53.  

  54. var chainAll = this.__chain__;

  55. // options 中的 chain 屬性 是否支持鏈式調用

  56. // 二者有一個符合鏈式調用 執行下面的代碼

  57. if (chain || chainAll) {

  58. // 一般是 lodash

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

  60. // 複製 實例上的 __action__ 到 result.__action__ 和 action 上

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

  62.  

  63. // action 添加 函數 和 args 和 this 指向,延遲計算調用。

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

  65. //實例上的__chain__ 屬性 賦值給 result 的 屬性 __chain__

  66. result.__chain__ = chainAll;

  67. // 最後返回這個實例

  68. return result;

  69. }

  70.  

  71. // 都不支持鏈式調用。直接調用

  72. // 把當前實例的 value 和 arguments 對象 傳遞給 func 函數做爲參數調用。返回調用結果。

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

  74. };

  75. }

  76. });

  77.  

  78. // 最後返回對象 object

  79. return object;

  80. }

小結:簡單說就是把 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的方法和屬性掛載關係圖。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

請出貫穿下文的簡單的例子

  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. // 若是不存在這個函數,就不往下執行

  15. if (!lodashFunc) {

  16. return;

  17. }

  18. // 把 lodash.prototype 方法賦值到lodash.prototype

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

  20. // 取實例中的__wrapped__ 值 例子中則是 [1,2,3,4,5]

  21. var value = this.__wrapped__,

  22. // 若是是head和last 方法 isTaker 返回 [1], 不然是arguments對象

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

  24. // 若是value 是LayeWrapper的實例

  25. isLazy = value instanceof LazyWrapper,

  26. // 迭代器 循環

  27. iteratee = args[0],

  28. // 使用useLazy isLazy value或者是數組

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

  30.  

  31. var interceptor = function(value) {

  32. // 函數執行 value args 組合成數組參數

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

  34. // 若是是 head 和 last (isTaker) 支持鏈式調用 返回結果的第一個參數 不然 返回result

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

  36. };

  37.  

  38. // useLazy true 而且 函數checkIteratee 且迭代器是函數,且迭代器參數個數不等於1

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

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

  41. // useLazy 賦值爲 false

  42. // isLazy 賦值爲 false

  43. isLazy = useLazy = false;

  44. }

  45. // 取實例上的 __chain__

  46. var chainAll = this.__chain__,

  47. // 存儲的待執行的函數 __actions__ 二次取反是布爾值 也就是等於0或者大於0兩種結果

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

  49. // 是否不包裝 用結果是否不包裝 且 不支持鏈式調用

  50. isUnwrapped = retUnwrapped && !chainAll,

  51. // 是否僅Lazy 用isLazy 和 存儲的函數

  52. onlyLazy = isLazy && !isHybrid;

  53.  

  54. // 結果不包裝 且 useLazy 爲 true

  55. if (!retUnwrapped && useLazy) {

  56. // 實例 new LazyWrapper 這裏的this 是 new LodashWrapper()

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

  58. // result 執行函數結果

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

  60.  

  61. /*

  62. *

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

  64. // 這個方法相似 _.tap, 除了它返回 interceptor 的返回結果。該方法的目的是"傳遞" 值到一個方法鏈序列以取代中間結果。

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

  66. .tap(function(array) {

  67. // 改變傳入的數組

  68. array.pop();

  69. })

  70. .reverse()

  71. .value();

  72. // => [2, 1]

  73. */

  74.  

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

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

  77. // 返回實例 lodashWrapper

  78. return new LodashWrapper(result, chainAll);

  79. }

  80. // 不包裝 且 onlyLazy 爲 true

  81. if (isUnwrapped && onlyLazy) {

  82. // 執行函數

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

  84. }

  85. // 上面都沒有執行,執行到這裏了

  86. // 執行 thru 函數,回調函數 是 interceptor

  87. result = this.thru(interceptor);

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

  89. };

  90. });

小結一下,寫了這麼多註釋,簡單說:其實就是用 LazyWrapper.prototype 改寫原先在 lodash.prototype的函數,判斷函數是否須要使用惰性求值,須要時再調用。

讀者能夠斷點調試一下,善用斷點進入函數功能,對着註釋看,可能會更加清晰。

斷點調試的部分截圖

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

鏈式調用最後都是返回實例對象,實際的處理數據的函數都沒有調用,而是被存儲存儲下來了,最後調用 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.  

  52. // 若是不是數組,或者 不是從右開始 而且 參數數組長度等於take的長度 takeCount等於長度

  53. // 則直接調用 baseWrapperValue 不須要

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

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

  56. }

  57. var result = [];

  58.  

  59. // 標籤語句 label

  60. // MDN label 連接

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

  62. // 標記語句能夠和 break 或 continue 語句一塊兒使用。標記就是在一條語句前面加個能夠引用的標識符(identifier)。

  63. outer:

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

  65. index += dir;

  66.  

  67. var iterIndex = -1,

  68. // 數組第一項

  69. value = array[index];

  70.  

  71. while (++iterIndex < iterLength) {

  72. // 迭代器數組 {iteratee: function{}, typy: 2}

  73. var data = iteratees[iterIndex],

  74. iteratee = data.iteratee,

  75. type = data.type,

  76. // 結果 迭代器執行結果

  77. computed = iteratee(value);

  78.  

  79. if (type == LAZY_MAP_FLAG) {

  80. // 若是 type 是 map 類型,結果 computed 賦值給value

  81. value = computed;

  82. } else if (!computed) {

  83. if (type == LAZY_FILTER_FLAG) {

  84. // 退出當前此次循環,進行下一次循環

  85. continue outer;

  86. } else {

  87. // 退出整個循環

  88. break outer;

  89. }

  90. }

  91. }

  92. // 最終數組

  93. result[resIndex++] = value;

  94. }

  95. // 返回數組 例子中則是 [2, 3, 4]

  96. return result;

  97. }

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

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

  100. LazyWrapper.prototype.constructor = LazyWrapper;

  101.  

  102. LazyWrapper.prototype.value = lazyValue;

筆者畫了一張 lodash和 LazyWrapper的關係圖來表示。 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

小結: 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連接

 

 

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相關文章
相關標籤/搜索