如果笔记

如果笔记

做有价值的技术笔记

lodash.chain源码解读

以 chain 方法为入口解读 lodash 实现链式调用的源码,并最终实现一个非常简单版的 lodash

> 以 chain 方法为入口解读 lodash 实现链式调用的源码,并最终实现一个非常简单版的 lodash 本文解读源码的方式一般是先贴代码,先会在代码里加一些注释,然后在代码段后面再做更多阐述。 开始吧: ```javascript function chain(value) { var result = lodash(value); result.__chain__ = true; return result; } function lodash(value) { if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { if (value instanceof LodashWrapper) { return value; } if (hasOwnProperty.call(value, '__wrapped__')) { return wrapperClone(value); } } return new LodashWrapper(value); } function LodashWrapper(value, chainAll) { this.__wrapped__ = value; this.__actions__ = []; this.__chain__ = !!chainAll; this.__index__ = 0; this.__values__ = undefined; } ``` 从上面的代码可以看出,如果调用`chain`,`chain`会直接调用`lodash`获得一个`LodashWrapper`的实例,并把该实例的链式调用标识`__chain__`置为true。当然,直接调用`lodash`函数也是得到一个`LodashWrapper`实例。 那么,形如`lodash.chain(obj).keys().value()`这样的调用,`keys`这些方法是`LodashWrapper`的原型方法吗? 继续看源码: ```javascript var objectCreate = Object.create; var baseCreate = (function() { function object() {} return function(proto) { if (!isObject(proto)) { return {}; } // 现在的浏览器几乎都支持Object.create了 if (objectCreate) { return objectCreate(proto); } // 下面是经典的原型链继承,但可惜基本没用了。。。 object.prototype = proto; var result = new object; object.prototype = undefined; return result; }; }()); // 包装器将继承该函数的原型链 function baseLodash() {} // Ensure wrappers are instances of `baseLodash`. lodash.prototype = baseLodash.prototype; lodash.prototype.constructor = lodash; LodashWrapper.prototype = baseCreate(baseLodash.prototype); LodashWrapper.prototype.constructor = LodashWrapper; // 因为大部分浏览器都支持Object.create,所以上面两行代码等效于下面这行 // LodashWrapper.prototype.__proto__ = baseLodash.prototype; // 还可以进一步理解成 // LodashWrapper.prototype.__proto__ = lodash.prototype; ``` 从上面的代码可以看出,`LodashWrapper`的原型上不会有我们要调用的方法,我们可使用的方法都是在`lodash.prototype`上,`LodashWrapper`通过原型链继承得到`lodash.prototype`上的方法。 那么,各种方法又是怎么弄到`lodash.prototype`上的呢?最核心的`mixin`出场了! lodash的原型方法中有两种是通过`mixin`挂到`lodash.prototype`上的,**一种是必须链式调用的**,**一种是默认不可链式调用的**。看代码: ```javascript // Add methods that return wrapped values in chain sequences. // 必须链式调用的,调用时返回包装过的值(即LodashWrapper实例) // 通过调用value方法或“默认不可链式调用”的方法获得最终结果 lodash.keys = keys; lodash.slice = slice; lodash.chain = chain; // 其他方法略。。。 // Add methods to `lodash.prototype`. mixin(lodash, lodash); // Add methods that return unwrapped values in chain sequences. // 默认不可链式调用的,调用时返回最终结果,而不是LodashWrapper实例 // 调用chain()后再调用此类方法,返回LodashWrapper实例,可继续链式调用其他方法 lodash.add = add; lodash.max = max; // 其他方法略。。。 mixin(lodash, (function() { // 这个匿名函数的作用是找出`默认不可链式调用`的方法 // 因为到目前为止,可链式和不可链式的都挂到了lodash下 // 而可链式的已经挂到了lodash.prototype上,所以寻找方法就是看有没有在lodash.prototype上。。。 var source = {}; baseForOwn(lodash, function(func, methodName) { if (!hasOwnProperty.call(lodash.prototype, methodName)) { source[methodName] = func; } }); return source; }()), { 'chain': false }); function mixin(object, source, options) { var props = baseKeys(source), methodNames = baseFunctions(source, props); if (options == null && !(isObject(source) && (methodNames.length || !props.length))) { options = source; source = object; object = this; methodNames = baseFunctions(source, baseKeys(source)); } var chain = !(isObject(options) && 'chain' in options) || !!options.chain, isFunc = isFunction(object); methodNames.forEach(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; // this.__actions__.push({ 'func': func, 'args': arguments, 'thisArg': object }); // return this; } return func.apply(object, arrayPush([this.value()], arguments)); }; } }); return object; } ``` 个人觉得第二个`mixin`调用可以再优化下,没必要先把方法挂到lodash上,然后又进行筛选。因为调用mixin的时候又把方法们再挂了一遍:`object[methodName] = func;`,所以何不先放到一个对象里呢? ```javascript // Add methods that return unwrapped values in chain sequences. var unwrappedValueFuncs = { add: add, max: max }; mixin(lodash, unwrappedValueFuncs, { 'chain': false }); ``` 每一个必须链式调用的方法,可通过调用`value`方法得到最后的结果,`value`源码如下: ```javascript lodash.prototype.value = wrapperValue; function wrapperValue() { return baseWrapperValue(this.__wrapped__, this.__actions__); } 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); } ``` 链式调用一个方法的时候,并不会立即执行该方法,而是要等到最后调用value方法的时候才开始执行之前的各方法。 最后,再来回顾一下主要原理:**调用chain,chain再调用lodash,lodash返回一个LodashWrapper实例,LodashWrapper通过原型链继承获得lodash原型方法,而大部分函数通过mixin同时“注册”为lodash的静态方法和原型方法。** 根据以上解读,可以实现一个非常简单版的lodash如下,为简单起见,只实现了keys、slice、add、max等简单方法: ```javascript ;(function () { function isObject(value) { var type = typeof value; return value != null && (type == 'object' || type == 'function'); } // The function whose prototype chain sequence wrappers inherit from. function baseLodash() {} function baseKeys(object) { var result = []; for (var key in Object(object)) { if (Reflect.hasOwnProperty.call(object, key) && key != 'constructor') { result.push(key); } } return result; } function keys(object) { return Reflect.ownKeys(object); } function slice(array, start, end) { return array.slice(start, end || array.length); } function add(a, b) { return a + b; } function max(array) { return Math.max.apply(null, array); } function isFunction(value) { return typeof value == 'function'; } function baseFunctions(source, props) { return props.filter(prop => isFunction(source[prop])); } function copyArray(array) { return array.concat(); } function arrayPush(array, elements) { array.push.apply(array, elements); return array; } function LodashWrapper(value, chainAll) { this.__wrapped__ = value; this.__actions__ = []; this.__chain__ = !!chainAll; this.__index__ = 0; this.__values__ = undefined; } function lodash(value) { if (isObject(value) && !Array.isArray(value)) { if (value instanceof LodashWrapper) { return value; } if (Reflect.hasOwnProperty.call(value, '__wrapped__')) { // return wrapperClone(value); throw new Error('no wrapperClone'); } } return new LodashWrapper(value); } function chain(value) { var result = lodash(value); result.__chain__ = true; return result; } lodash.prototype = baseLodash.prototype; lodash.prototype.constructor = lodash; // LodashWrapper.prototype = baseCreate(baseLodash.prototype); // LodashWrapper.prototype.constructor = LodashWrapper; LodashWrapper.prototype.__proto__ = baseLodash.prototype; lodash.prototype.value = function () { return this.__actions__.reduce(function(result, action) { return action.func.apply(action.thisArg, arrayPush([result], action.args)); }, this.__wrapped__); }; function mixin(object, source, options) { var props = baseKeys(source), methodNames = baseFunctions(source, props); if (options == null && !(isObject(source) && (methodNames.length || !props.length))) { options = source; source = object; object = this; methodNames = baseFunctions(source, baseKeys(source)); } var chain = !(isObject(options) && 'chain' in options) || !!options.chain, isFunc = isFunction(object); methodNames.forEach(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; // this.__actions__.push({ 'func': func, 'args': arguments, 'thisArg': object }); // return this; } return func.apply(object, arrayPush([this.value()], arguments)); }; } }); return object; } // Add methods that return wrapped values in chain sequences. lodash.keys = keys; lodash.slice = slice; lodash.chain = chain; // Add methods to `lodash.prototype`. mixin(lodash, lodash); // Add methods that return unwrapped values in chain sequences. var unwrappedFuncs = { add, max }; mixin(lodash, unwrappedFuncs, { 'chain': false }); if (isObject(global)) { global._ = lodash; } else { this._ = lodash; } }.call(this)); // 简单测试 const obj = { foo: 1, bar: 2, zoo: 3 }; console.log('\n静态方式调用 _.keys(obj):', obj) console.log(_.keys(obj)); console.log('\ncall _.chain(obj) 返回 LodashWrapper 实例:', obj) console.log(_.chain(obj)); console.log('\ncall _.chain(obj).keys() 依然返回 LodashWrapper 实例:') console.log(_.chain(obj).keys()); console.log('\ncall _.chain(obj).keys().slice(2).value(),调value方法时才依次执行函数,并返回最终结果: ') console.log(_.chain(obj).keys().slice(2).value()); console.log('\n静态方式调用 _.max(array):', [-1, 9, 2]) console.log(_.max([-1, 9, 2])); console.log('\n实例化方式调用 _(array).max():', [0, -1, 2]) console.log(_([0, -1, 2]).max()); // var keys = _.chain(obj).keys(); // var keys2 = keys.slice(2); // console.log(keys.value(), keys2.value()); ``` 把以上代码保持为`lodash.js`,安装了node的话在命令行运行`node lodash.js`可看效果。

lodash, chain, 原型链

前端

hahaboy