學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫

  • 2019 年 10 月 4 日
  • 筆記

前言

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

上上篇文章寫了 jQuery源碼整體架構,學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫

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

感興趣的讀者可以點擊閱讀。

underscore源碼分析的文章比較多,而 lodash源碼分析的文章比較少。原因之一可能是由於 lodash源碼行數太多。注釋加起來一萬多行。

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

本文章學習的 lodash的版本是: v4.17.15unpkg.com地址 https://unpkg.com/[email protected]/lodash.js

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

導讀:

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

匿名函數執行

;(function() {    }.call(this));

暴露 lodash

var _ = runInContext();

runInContext 函數

這裡的簡版源碼,只關注函數入口和返回值。

var runInContext = (function runInContext(context) {      // 瀏覽器中處理context為window      // ...      function lodash(value) {}{          // ...          return new LodashWrapper(value);      }      // ...      return lodash;  });

可以看到申明了一個 runInContext函數。裏面有一個 lodash函數,最後處理返回這個 lodash函數。

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

LodashWrapper 函數

function LodashWrapper(value, chainAll) {      this.__wrapped__ = value;      this.__actions__ = [];      this.__chain__ = !!chainAll;      this.__index__ = 0;      this.__values__ = undefined;  }

設置了這些屬性:

__wrapped__:存放參數 value

__actions__:存放待執行的函數體 func, 函數參數 args,函數執行的 this 指向 thisArg

__chain__undefined兩次取反轉成布爾值 false,不支持鏈式調用。和 underscore一樣,默認是不支持鏈式調用的。

__index__:索引值 默認 0。

__values__:主要 clone時使用。

接着往下搜索源碼, LodashWrapper, 會發現這兩行代碼。

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

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

baseCreate 原型繼承

//  立即執行匿名函數  // 返回一個函數,用於設置原型 可以理解為是 __proto__  var baseCreate = (function() {      // 這句放在函數外,是為了不用每次調用baseCreate都重複申明 object      // underscore 源碼中,把這句放在開頭就申明了一個空函數 `Ctor`      function object() {}      return function(proto) {          // 如果傳入的參數不是object也不是function 是null          // 則返回空對象。          if (!isObject(proto)) {              return {};          }          // 如果支持Object.create方法,則返回 Object.create          if (objectCreate) {              // Object.create              return objectCreate(proto);          }          // 如果不支持Object.create 用 ployfill new          object.prototype = proto;          var result = new object;          // 還原 prototype          object.prototype = undefined;          return result;      };  }());    // 空函數  function baseLodash() {      // No operation performed.  }    // Ensure wrappers are instances of `baseLodash`.  lodash.prototype = baseLodash.prototype;  // 為什麼會有這一句?因為上一句把lodash.prototype.construtor 設置為Object了。這一句修正constructor  lodash.prototype.constructor = lodash;    LodashWrapper.prototype = baseCreate(baseLodash.prototype);  LodashWrapper.prototype.constructor = LodashWrapper;

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

衍生的 isObject 函數

判斷 typeofvalue不等於 null,並且是 object或者 function

function isObject(value) {      var type = typeof value;      return value != null && (type == 'object' || type == 'function');  }

Object.create() 用法舉例

面試官問:能否模擬實現JS的new操作符 之前這篇文章寫過的一段。

筆者之前整理的一篇文章中也有講過,可以翻看JavaScript 對象所有API解析

MDN Object.create()

Object.create(proto,[propertiesObject]) 方法創建一個新對象,使用現有的對象來提供新創建的對象的proto。它接收兩個參數,不過第二個可選參數是屬性描述符(不常用,默認是 undefined)。

var anotherObject = {      name: '若川'  };  var myObject = Object.create(anotherObject, {      age: {          value:18,      },  });  // 獲得它的原型  Object.getPrototypeOf(anotherObject) === Object.prototype; // true 說明anotherObject的原型是Object.prototype  Object.getPrototypeOf(myObject); // {name: "若川"} // 說明myObject的原型是{name: "若川"}  myObject.hasOwnProperty('name'); // false; 說明name是原型上的。  myObject.hasOwnProperty('age'); // true 說明age是自身的  myObject.name; // '若川'  myObject.age; // 18;

對於不支持 ES5的瀏覽器, MDN上提供了 ployfill方案。

if (typeof Object.create !== "function") {      Object.create = function (proto, propertiesObject) {          if (typeof proto !== 'object' && typeof proto !== 'function') {              throw new TypeError('Object prototype may only be an Object: ' + proto);          } else if (proto === null) {              throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");          }            if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");            function F() {}          F.prototype = proto;          return new F();      };  }

lodash上有很多方法和屬性,但在 lodash.prototype也有很多與 lodash上相同的方法。肯定不是在 lodash.prototype上重新寫一遍。而是通過 mixin掛載的。

mixin

mixin 具體用法

_.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源碼,後文注釋解析

function mixin(object, source, options) {      var props = keys(source),          methodNames = baseFunctions(source, props);        if (options == null &&          !(isObject(source) && (methodNames.length || !props.length))) {          options = source;          source = object;          object = this;          methodNames = baseFunctions(source, keys(source));      }      var chain = !(isObject(options) && 'chain' in options) || !!options.chain,          isFunc = isFunction(object);        arrayEach(methodNames, function(methodName) {          var func = source[methodName];          object[methodName] = func;          if (isFunc) {              object.prototype[methodName] = function() {                  var chainAll = this.__chain__;                  if (chain || chainAll) {                      var result = object(this.__wrapped__),                          actions = result.__actions__ = copyArray(this.__actions__);                        actions.push({ 'func': func, 'args': arguments, 'thisArg': object });                      result.__chain__ = chainAll;                      return result;                  }                  return func.apply(object, arrayPush([this.value()], arguments));              };          }      });        return object;  }

接下來先看衍生的函數。

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

感興趣的讀者可以自行看這些函數衍生的其他函數的源碼。

mixin 衍生的函數 keys

mixin 函數中 其實最終調用的就是 Object.keys

function keys(object) {      return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);  }

mixin 衍生的函數 baseFunctions

返回函數數組集合

function baseFunctions(object, props) {      return arrayFilter(props, function(key) {          return isFunction(object[key]);      });  }

mixin 衍生的函數 isFunction

判斷參數是否是函數

function isFunction(value) {      if (!isObject(value)) {          return false;      }      // The use of `Object#toString` avoids issues with the `typeof` operator      // in Safari 9 which returns 'object' for typed arrays and other constructors.      var tag = baseGetTag(value);      return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;  }

mixin 衍生的函數 arrayEach

類似 [].forEarch

function arrayEach(array, iteratee) {      var index = -1,          length = array == null ? 0 : array.length;        while (++index < length) {          if (iteratee(array[index], index, array) === false) {              break;          }      }      return array;  }

mixin 衍生的函數 arrayPush

類似 [].push

function arrayPush(array, values) {      var index = -1,          length = values.length,          offset = array.length;        while (++index < length) {      array[offset + index] = values[index];      }      return array;  }

mixin 衍生的函數 copyArray

拷貝數組

function copyArray(source, array) {      var index = -1,          length = source.length;        array || (array = Array(length));      while (++index < length) {          array[index] = source[index];      }      return array;  }

mixin 源碼解析

lodash 源碼中兩次調用 mixin

// Add methods that return wrapped values in chain sequences.  lodash.after = after;  // code ... 等 153 個支持鏈式調用的方法    // Add methods to `lodash.prototype`.  // 把lodash上的靜態方法賦值到 lodash.prototype 上  mixin(lodash, lodash);    // Add methods that return unwrapped values in chain sequences.  lodash.add = add;  // code ... 等 152 個不支持鏈式調用的方法      // 這裡其實就是過濾 after 等支持鏈式調用的方法,獲取到 lodash 上的 add 等 添加到lodash.prototype 上。  mixin(lodash, (function() {      var source = {};      // baseForOwn 這裡其實就是遍歷lodash上的靜態方法,執行回調函數      baseForOwn(lodash, function(func, methodName) {          // 第一次 mixin 調用了所以賦值到了lodash.prototype          // 所以這裡用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 個不支持鏈式調用的方法。          if (!hasOwnProperty.call(lodash.prototype, methodName)) {              source[methodName] = func;          }      });      return source;  // 最後一個參數options 特意註明不支持鏈式調用  }()), { 'chain': false });

結合兩次調用 mixin 代入到源碼解析如下 mixin源碼及注釋

function mixin(object, source, options) {      // source 對象中可以枚舉的屬性      var props = keys(source),          // source 對象中的方法名稱數組          methodNames = baseFunctions(source, props);        if (options == null &&          !(isObject(source) && (methodNames.length || !props.length))) {          // 如果 options 沒傳為 undefined  undefined == null 為true          // 且 如果source 不為 對象或者不是函數          // 且 source對象的函數函數長度 或者 source 對象的屬性長度不為0          // 把 options 賦值為 source          options = source;          // 把 source 賦值為 object          source = object;          // 把 object 賦值為 this 也就是 _ (lodash)          object = this;          // 獲取到所有的方法名稱數組          methodNames = baseFunctions(source, keys(source));      }      // 是否支持 鏈式調用      // options  不是對象或者不是函數,是null或者其他值      // 判斷options是否是對象或者函數,如果不是或者函數則不會執行 'chain' in options 也就不會報錯      //  且 chain 在 options的對象或者原型鏈中      // 知識點 in [MDN in :  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in      // 如果指定的屬性在指定的對象或其原型鏈中,則in 運算符返回true。        // 或者 options.chain 轉布爾值      var chain = !(isObject(options) && 'chain' in options) || !!options.chain,          // object 是函數          isFunc = isFunction(object);        // 循環 方法名稱數組      arrayEach(methodNames, function(methodName) {          // 函數本身          var func = source[methodName];          // object 通常是 lodash  也賦值這個函數。          object[methodName] = func;          if (isFunc) {              // 如果object是函數 賦值到  object prototype  上,通常是lodash              object.prototype[methodName] = function() {                  // 實例上的__chain__ 屬性 是否支持鏈式調用                  // 這裡的 this 是 new LodashWrapper 實例 類似如下                  /**                   {                      __actions__: [],                      __chain__: true                      __index__: 0                      __values__: undefined                      __wrapped__: []                   }                   **/                    var chainAll = this.__chain__;                  // options 中的 chain 屬性 是否支持鏈式調用                  // 兩者有一個符合鏈式調用  執行下面的代碼                  if (chain || chainAll) {                      // 通常是 lodash                      var result = object(this.__wrapped__),                      // 複製 實例上的 __action__ 到 result.__action__ 和 action 上                      actions = result.__actions__ = copyArray(this.__actions__);                        // action 添加 函數 和 args 和 this 指向,延遲計算調用。                      actions.push({ 'func': func, 'args': arguments, 'thisArg': object });                      //實例上的__chain__ 屬性  賦值給 result 的 屬性 __chain__                      result.__chain__ = chainAll;                      // 最後返回這個實例                      return result;                  }                    // 都不支持鏈式調用。直接調用                  // 把當前實例的 value 和 arguments 對象 傳遞給 func 函數作為參數調用。返回調用結果。                  return func.apply(object, arrayPush([this.value()], arguments));              };          }      });        // 最後返回對象 object      return object;  }

小結:簡單說就是把 lodash上的靜態方法賦值到 lodash.prototype上。分兩次第一次是支持鏈式調用( lodash.after153個支持鏈式調用的方法),第二次是不支持鏈式調用的方法( lodash.add152個不支持鏈式調用的方法)。

lodash 究竟在和.prototype掛載了多少方法和屬性

再來看下 lodash究竟掛載在 _函數對象上有多少靜態方法和屬性,和掛載 _.prototype上有多少方法和屬性。

使用 forin循環一試便知。看如下代碼:

var staticMethods = [];  var staticProperty = [];  for(var name in _){      if(typeof _[name] === 'function'){          staticMethods.push(name);      }      else{          staticProperty.push(name);      }  }  console.log(staticProperty); // ["templateSettings", "VERSION"] 2個  console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305個

其實就是上文提及的 lodash.after153個支持鏈式調用的函數 、 lodash.add152不支持鏈式調用的函數賦值而來。

var prototypeMethods = [];  var prototypeProperty = [];  for(var name in _.prototype){      if(typeof _.prototype[name] === 'function'){          prototypeMethods.push(name);      }      else{          prototypeProperty.push(name);      }  }  console.log(prototypeProperty); // []  console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317個

相比 lodash上的靜態方法多了 12個,說明除了 mixin 外,還有 12個其他形式賦值而來。

支持鏈式調用的方法最後返回是實例對象,獲取最後的處理的結果值,最後需要調用 value方法。

筆者畫了一張表示 lodash的方法和屬性掛載關係圖。

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

var result = _.chain([1, 2, 3, 4, 5])  .map(el => {      console.log(el); // 1, 2, 3      return el + 1;  })  .take(3)  .value();  // lodash中這裡的`map`僅執行了`3`次。  // 具體功能也很簡單 數組 1-5 加一,最後獲取其中三個值。  console.log('result:', result);

也就是說這裡 lodash聰明的知道了最後需要幾個值,就執行幾次 map循環,對於很大的數組,提升性能很有幫助。underscore執行這段代碼其中 map執行了5次。如果是平常實現該功能也簡單。

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

而相比 lodash這裡的 map執行了 5次。

// 不使用 map、slice  var result = [];  var arr = [1, 2, 3, 4, 5];  for (var i = 0; i < 3; i++){      result[i] = arr[i] + 1;  }  console.log(result, 'result');

簡單說這裡的 map方法,添加 LazyWrapper 的方法到 lodash.prototype存儲下來,最後調用 value時再調用。具體看下文源碼實現。

添加 LazyWrapper 的方法到 lodash.prototype

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

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

具體源碼及注釋

// Add `LazyWrapper` methods to `lodash.prototype`.  // baseForOwn 這裡其實就是遍歷LazyWrapper.prototype上的方法,執行回調函數  baseForOwn(LazyWrapper.prototype, function(func, methodName) {      // 檢測函數名稱是否是迭代器也就是循環      var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),          // 檢測函數名稱是否head和last          // 順便提一下 ()這個是捕獲分組 而加上 ?:  則是非捕獲分組 也就是說不用於其他操作          isTaker = /^(?:head|last)$/.test(methodName),          // lodashFunc 是 根據 isTaker 組合 takeRight take methodName          lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],          // 根據isTaker 和 是 find 判斷結果是否 包裝          retUnwrapped = isTaker || /^find/.test(methodName);        // 如果不存在這個函數,就不往下執行      if (!lodashFunc) {          return;      }      // 把 lodash.prototype 方法賦值到lodash.prototype      lodash.prototype[methodName] = function() {          // 取實例中的__wrapped__ 值 例子中則是 [1,2,3,4,5]          var value = this.__wrapped__,              // 如果是head和last 方法 isTaker 返回 [1], 否則是arguments對象              args = isTaker ? [1] : arguments,              // 如果value 是LayeWrapper的實例              isLazy = value instanceof LazyWrapper,              // 迭代器 循環              iteratee = args[0],              // 使用useLazy isLazy value或者是數組              useLazy = isLazy || isArray(value);            var interceptor = function(value) {              // 函數執行 value args 組合成數組參數              var result = lodashFunc.apply(lodash, arrayPush([value], args));              // 如果是 head 和 last (isTaker) 支持鏈式調用 返回結果的第一個參數 否則 返回result              return (isTaker && chainAll) ? result[0] : result;          };            // useLazy true 並且 函數checkIteratee 且迭代器是函數,且迭代器參數個數不等於1          if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {              // Avoid lazy use if the iteratee has a "length" value other than `1`.              // useLazy 賦值為 false              // isLazy 賦值為 false              isLazy = useLazy = false;          }          // 取實例上的 __chain__          var chainAll = this.__chain__,              // 存儲的待執行的函數 __actions__ 二次取反是布爾值 也就是等於0或者大於0兩種結果              isHybrid = !!this.__actions__.length,              // 是否不包裝 用結果是否不包裝 且 不支持鏈式調用              isUnwrapped = retUnwrapped && !chainAll,              // 是否僅Lazy 用isLazy 和 存儲的函數              onlyLazy = isLazy && !isHybrid;            // 結果不包裝 且 useLazy 為 true          if (!retUnwrapped && useLazy) {              // 實例 new LazyWrapper 這裡的this 是 new LodashWrapper()              value = onlyLazy ? value : new LazyWrapper(this);              // result 執行函數結果              var result = func.apply(value, args);                /*              *              // _.thru(value, interceptor)              // 這個方法類似 _.tap, 除了它返回 interceptor 的返回結果。該方法的目的是"傳遞" 值到一個方法鏈序列以取代中間結果。              _([1, 2, 3])              .tap(function(array) {                  // 改變傳入的數組                  array.pop();              })              .reverse()              .value();              // => [2, 1]              */                // thisArg 指向undefined 或者null 非嚴格模式下是指向window,嚴格模式是undefined 或者nll              result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });              // 返回實例 lodashWrapper              return new LodashWrapper(result, chainAll);          }          // 不包裝 且 onlyLazy 為 true          if (isUnwrapped && onlyLazy) {              // 執行函數              return func.apply(this, args);          }          // 上面都沒有執行,執行到這裡了          // 執行 thru 函數,回調函數 是 interceptor          result = this.thru(interceptor);          return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;      };  });

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

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

斷點調試的部分截圖

鏈式調用最後都是返回實例對象,實際的處理數據的函數都沒有調用,而是被存儲存儲下來了,最後調用 value方法,才執行這些函數。

lodash.prototype.value 即 wrapperValue

function baseWrapperValue(value, actions) {      var result = value;      // 如果是lazyWrapper的實例,則調用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法      if (result instanceof LazyWrapper) {          result = result.value();      }      // 類似 [].reduce(),把上一個函數返回結果作為參數傳遞給下一個函數      return arrayReduce(actions, function(result, action) {          return action.func.apply(action.thisArg, arrayPush([result], action.args));      }, result);  }  function wrapperValue() {      return baseWrapperValue(this.__wrapped__, this.__actions__);  }  lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;

如果是惰性求值,則調用的是 LazyWrapper.prototype.valuelazyValue

LazyWrapper.prototype.value 即 lazyValue 惰性求值

lazyValue源碼及注釋

function LazyWrapper(value) {      // 參數 value      this.__wrapped__ = value;      // 執行的函數      this.__actions__ = [];      this.__dir__ = 1;      // 過濾      this.__filtered__ = false;      // 存儲迭代器函數      this.__iteratees__ = [];      // 默認最大取值個數      this.__takeCount__ = MAX_ARRAY_LENGTH;      // 具體取值多少個,存儲函數和類型      this.__views__ = [];  }  /**  * Extracts the unwrapped value from its lazy wrapper.  *  * @private  * @name value  * @memberOf LazyWrapper  * @returns {*} Returns the unwrapped value.  */  function lazyValue() {      // this.__wrapped__ 是 new LodashWrapper 實例 所以執行.value 獲取原始值      var array = this.__wrapped__.value(),          //          dir = this.__dir__,          // 是否是函數          isArr = isArray(array),          // 是否從右邊開始          isRight = dir < 0,          // 數組的長度。如果不是數組,則是0          arrLength = isArr ? array.length : 0,          // 獲取 take(3) 上述例子中 則是 start: 0,end: 3          view = getView(0, arrLength, this.__views__),          start = view.start,          end = view.end,          // 長度 3          length = end - start,          // 如果是是從右開始          index = isRight ? end : (start - 1),          // 存儲的迭代器數組          iteratees = this.__iteratees__,          // 迭代器數組長度          iterLength = iteratees.length,          // 結果resIndex          resIndex = 0,          // 最後獲取幾個值,也就是 3          takeCount = nativeMin(length, this.__takeCount__);        // 如果不是數組,或者 不是從右開始 並且 參數數組長度等於take的長度 takeCount等於長度      // 則直接調用 baseWrapperValue 不需要      if (!isArr || (!isRight && arrLength == length && takeCount == length)) {          return baseWrapperValue(array, this.__actions__);      }      var result = [];        // 標籤語句 label      // MDN label 鏈接      // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label      // 標記語句可以和 break 或 continue 語句一起使用。標記就是在一條語句前面加個可以引用的標識符(identifier)。      outer:      while (length-- && resIndex < takeCount) {          index += dir;            var iterIndex = -1,              // 數組第一項              value = array[index];            while (++iterIndex < iterLength) {              // 迭代器數組 {iteratee: function{}, typy: 2}              var data = iteratees[iterIndex],                  iteratee = data.iteratee,                  type = data.type,                  // 結果 迭代器執行結果                  computed = iteratee(value);                if (type == LAZY_MAP_FLAG) {                  // 如果 type 是 map 類型,結果 computed 賦值給value                  value = computed;              } else if (!computed) {                  if (type == LAZY_FILTER_FLAG) {                      // 退出當前這次循環,進行下一次循環                      continue outer;                  } else {                      // 退出整個循環                      break outer;                  }              }          }          // 最終數組          result[resIndex++] = value;      }      // 返回數組 例子中則是 [2, 3, 4]      return result;  }  // Ensure `LazyWrapper` is an instance of `baseLodash`.  LazyWrapper.prototype = baseCreate(baseLodash.prototype);  LazyWrapper.prototype.constructor = LazyWrapper;    LazyWrapper.prototype.value = lazyValue;

筆者畫了一張 lodashLazyWrapper的關係圖來表示。

小結: lazyValue簡單說實現的功能就是把之前記錄的需要執行幾次,把記錄存儲的函數執行幾次,不會有多少項數據就執行多少次,而是根據需要幾項,執行幾項。也就是說以下這個例子中, map函數只會執行 3次。如果沒有用惰性求值,那麼 map函數會執行 5次。

var result = _.chain([1, 2, 3, 4, 5])  .map(el => el + 1)  .take(3)  .value();

總結

行文至此,基本接近尾聲,最後總結一下。

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

分享一個只知道函數名找源碼定位函數申明位置的 VSCode 技巧Ctrl+p。輸入 @functionName 定位函數 functionName在源碼文件中的具體位置。如果知道調用位置,那直接按 alt+鼠標左鍵即可跳轉到函數申明的位置。

如果讀者發現有不妥或可改善之處,再或者哪裡沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支持。萬分感謝。

關於

作者:常以若川為名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。 個人博客 http://lxchuan12.github.io 使用 vuepress重構了,閱讀體驗可能更好些 https://github.com/lxchuan12/blog,相關源碼和資源都放在這裡,求個 star^_^~