源码共读09:学习 lodash 源码整体架构

149 阅读10分钟

lodash Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列第三篇,链接: juejin.cn/post/684490…

导读:

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

匿名函数执行

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

暴露lodash

var _ = runInContext();

runInContext函数

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

    // 浏览器处理context为window
    // ...
    function lodash(value) {
      // ...
      return new LodashWrapper(value);
    }
    // ...
    return lodash;
});

可以看到声明了一个runInContext函数。里面有一个lodash函数,以后处理返回这个lodash函数。 再看lodash函数中返回值new LodashWrapper(value)。

LodashWrapper函数

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

设置了这些属性:

  • wrapped:存放参数value
  • action:存放待执行的函数体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) {
            return objectCreate(proto);
        }
        // 如果不支持object.create,用ployfill new
        object.prototype = proto;
        var result = new object;
        // 还原object.prototype
        object.prototype = null;
        return result;
    };
}())

// 空函数
function baseLodash() {
  // no operation performed.
}

// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
// 由于上一句将lodash.prototype.constructor 也设置为Object。这句为了修正contructor
lodash.prototype.constructor = lodash;

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

image.png

衍生的isObject函数

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

Object.create()用法列举

  • 可以参考 JavaScript 对象所有API解析
  • MDN Object.create()

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

var personObejct = {
    name: 'sam'
}
var myObject = Object.create(personObject, {
    age: {
        value: 1
    }
});

// 获得它的原型
Obejct.getPrototypeOf(personObject) === Object.prototype; // true 说明personObject的原型是Obejct.prototype
Object.getPrototype(MyObject) // {name: 'sam'} 说明myObject的原型是personObject
myObject.hasOwnProperty('name') // false, 说明name是原型上的
myObject.hasOwnProperty('age') // true,说明age是自身的
myObject.name // sam
myObject.age // 1

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

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

mixin

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

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

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

添加版本 0.1.0

参数

  1. object=lodash: 目标对象
  2. source(Object): 来源对象
  3. options={}: 选项对象
  4. options.chain=true: 是否开启链式操作 返回 (*):返回object

mixin源码

衍生的函数

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;
  }
  
  var tag = baseGetTag(value);
  return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}

mixin衍生的函数arrayEach

类似[].forEach

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

mixin衍生的函数arrayPush

类似[].push

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

mixin衍生的函数copyArray

拷贝数组

function copyArray(source, array) {
  var index = -1,
    length = array.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 = {};
  // baseOwn 这里其实就是遍历lodash上的静态方法,执行回调函数
  baseOwn(lodash, function(func, methodName) {
    // 第一次 mixin 调用了所以赋值到了lodash.prototype
    // 所以这里用Object.hasOwnPrototype 排除不在lodash.prototype 上的方法。也就是 add 等152个不支持链式调用方法
    if (!hasOwnPrototype.call(lodash.prototype, methodName) {
      source[methodName] = func;
    })
  });
  return source;
  // 最后一个参数options 特意标明不支持链式调用。
})(), { chain: false });

结合两次调用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__),
              // 复制 实例上 __actions__到result.__actions__ 和 actions 上
                  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.after等153个支持链式调用的方法),第二次是不支持链式调用的方法(lodash.add等152个不支持链式调用的方法)

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

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

使用for...in...循环

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

staticMethods其实就是上文提及的 lodash.after153个支持链式调用的函数 、lodash.add152不支持链式调用的函数赋值而来

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

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

支持链式调用的方法最后返回的是实例对象,获取最后的处理的结果值,最后需要调用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
          // by the way, ()是捕获分组,?:是非捕获分组。也就是说不用于其他操作
          isTaker = /^(?:head|last)$/.test(methodName),
          // lodashFunc 是根据 isTake组合 takeRight take methodName
          lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
          // 根据isTake 和 是find 判断结果 是否包装
          retUnwrapped = isTaker || /^find/.test(methodName);

      // 如果不存在合格函数,就不往下执行
      if (!lodashFunc) {
        return;
      }
      // 把lodash.prototype 方法赋值到lodash.prototype
      lodash.prototype[methodName] = function() {
        // 取实例中的__wrappered__值,例子则是[1,2,3,4,5]
        var value = this.__wrapped__,
            // 如果head 和 last 方法, 方法isTaker 返回[1],否则是arguments对象
            args = isTaker ? [1] : arguments,
            // 如果value是lazyWrapper的实例
            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(isTake) 支持链式调用,返回结果的第一个参数,否则返回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 LodashWapper()
          value = onlyLazy ? value : new LazyWrapper(this);
          // result 执行函数的结果
          var result = func.apply(value, args);
          /*
          *
          // _.thru(value, interceptor)
          // 这个方法类似于_.tab, 除了他返回interceptor的返回结果。该方法的目的是“传递” 值到一个方法链序列以取代中间结果。
          // _([1,2,3])
             .tab(function (array) {
                // 该变传入的数组
               array.pop()
            })
            .reverse()
            .value() // => [2,1]
          *
          */
          // thisArg 指向undefined或者null,非严格模式下指向window,严格模式下指向undefined或null
          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的函数,判断函数是否需要使用惰性求值,需要是再调用。

Lodash.prototype.value 即wrapperValue

function baseWrapperValue(value, actions) {
      var result = value;
      if (result instanceof LazyWrapper) {
        result = result.value();
      }
      return arrayReduce(actions, function(result, action) {
        return action.func.apply(action.thisArg, arrayPush([result], action.args));
      }, result);
 }

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

lazyWrapper.prototype.value即lazyValue惰性求值

function LazyWrapper(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() {
      var array = this.__wrapped__.value(),
          dir = this.__dir__,
          isArr = isArray(array),
          isRight = dir < 0,
          arrLength = isArr ? array.length : 0,
          view = getView(0, arrLength, this.__views__),
          start = view.start,
          end = view.end,
          length = end - start,
          index = isRight ? end : (start - 1),
          iteratees = this.__iteratees__,
          iterLength = iteratees.length,
          resIndex = 0,
          takeCount = nativeMin(length, this.__takeCount__);

      if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
        return baseWrapperValue(array, this.__actions__);
      }
      var result = [];

      outer:
      while (length-- && resIndex < takeCount) {
        index += dir;

        var iterIndex = -1,
            value = array[index];

        while (++iterIndex < iterLength) {
          var data = iteratees[iterIndex],
              iteratee = data.iteratee,
              type = data.type,
              computed = iteratee(value);

          if (type == LAZY_MAP_FLAG) {
            value = computed;
          } else if (!computed) {
            if (type == LAZY_FILTER_FLAG) {
              continue outer;
            } else {
              break outer;
            }
          }
        }
        result[resIndex++] = value;
      }
      return result;
    }

小结:lazyValue简单来说实现的功能就是把之前的记录的需要执行几次,把记录存储的函数执行几次,不会有多少次执行多少次,而是根据需要几项,执行几项。如下例子:

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

总结

此文章为2024年04月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。