Lodash一直是我很喜欢用的一个库,代码也十分简洁优美,一直想抽时间好好分析一下Lodash的源代码。最近抽出早上的一些时间来分析一下Lodash的一些我以为比较好的源码。由于函数之间可能会有相互依赖,因此不会按照文档顺序进行分析,而是根据依赖关系和简易程度由浅入深地进行分析。由于我的能力有限,若是理解有误差,还请直接指出,以便我及时修改。javascript
源码都是针对4.17.4
版本的,源docs写得也很好,还有不少样例。java
_.after
_.after
函数几乎是Lodash中最容易理解的一个函数了,它一共有两个参数,第一个参数是调用次数n
,第二个参数是n
次调用以后执行的函数func
。web
function after(n, func) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } n = toInteger(n); return function() { if (--n < 1) { return func.apply(this, arguments); } }; }
这个函数的核心代码就是:数组
func.apply(this,arguments);
可是必定要注意,这个函数中有闭包的应用,就是这个参数n
。n
本应该在函数_.after
返回的时候就应该从栈空间回收,但事实上它还被返回的函数引用着,一直在内存中:闭包
return function() { if (--n < 1) { return func.apply(this, arguments); } };
因此一直到返回的函数执行完毕,n
所占用的内存空间都没法被回收。app
咱们再来看看这个apply
函数,咱们知道apply
函数能够改变函数运行时的做用域,那么问题来了,_.after
中func.apply
函数的this
究竟是谁呢?其实这个东西咱们没有办法从源码中看出来,由于this
是在运行时决定的。那么this
会变吗?若是会的话怎么变呢?要知道这个问题的答案,咱们须要先弄懂_.after
函数怎么用。ide
_.after
函数调用后返回了另外一个函数,因此对于_.after
函数的返回值,咱们是须要再次调用的。因此最好的场景多是在延迟加载等场景中。固然为了简单起见我给出一个很简单的例子:函数
const _ = require("lodash"); function foo(func ){ console.log("invoked foo."); func(); } var done = _.after(2,function bar(){ console.log("invoke bar"); }); for( var i = 0; i < 4; i++ ){ foo(done); }
正如咱们前面说的,n
的做用域是_.after
函数内部,因此在执行过程当中n
会一直递减,所以输出结果应该是在调用两次foo
以后调用一次bar
,以后每次调用foo
,都会调用一次bar
。结果和咱们预期的一致:性能
invoked foo invoked foo invoke bar invoked foo invoke bar invoked foo invoke bar
那么咱们再看看this
指向的问题,咱们修改一下上面的调用函数,让bar
函数输出一下内部的this
的一些属性:ui
const _ = require("lodash"); function foo(func ){ this.name = "foo"; console.log("invoked foo: " + this.name ); func(); } var done = _.after(2,function bar(){ console.log("invoke bar: " + this.name); }); for( var i = 0; i < 4; i++ ){ foo(done); }
其实想来你们也应该可以猜到,在bar
函数中输出的this.name
也是foo
:
invoked foo: foo invoked foo: foo invoke bar: foo invoked foo: foo invoke bar: foo invoked foo: foo invoke bar: foo
这是由于bar
的this
应该指向的是_.after
建立的函数的this
,而这个函数是window调用的,所以this
实际上指向就是window
,可是为何会输出foo
呢?由于foo函数的调用者也是window,而在foo函数中,将window.name设置成了foo
,因此bar
函数输出的也是foo
(多谢评论指出!)。
_.map
_.map
函数咱们几乎随处可见,这个函数应用也至关普遍。
function map(collection, iteratee) { var func = isArray(collection) ? arrayMap : baseMap; return func(collection, getIteratee(iteratee, 3)); }
为了简化问题,咱们分析比较简单的状况:用一个func函数处理数组。
_.map([1,2,3],func);
在处理数组的时候,lodash是分开处理的,对于Array
采用arrayMap
进行处理,对于对象则采用baseMap
进行处理。
咱们先看数组arrayMap
:
function arrayMap(array, iteratee) { var index = -1, length = array == null ? 0 : array.length, result = Array(length); while (++index < length) { result[index] = iteratee(array[index], index, array); } return result; }
这个函数是一个私有函数,第一个参数是一个须要遍历的数组,第二个参数是在遍历过程中进行处理的函数;返回一个进行map处理以后的函数。
在看咱们须要进行遍历处理的函数iteratee
,这个函数式经过getIteratee
函数获得的:
function getIteratee() { var result = lodash.iteratee || iteratee; result = result === iteratee ? baseIteratee : result; return arguments.length ? result(arguments[0], arguments[1]) : result; }
若是lodash.iteratee
被从新定义,则使用用户定义的iteratee
,不然就用官方定义的baseIteratee
。须要强调的是,result(arguments[0],arguments[1])
是柯里化的函数返回,返回的仍旧是一个函数。不可避免地,咱们须要看看官方定义的baseIteratee
的实现:
function baseIteratee(value) { // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. if (typeof value == 'function') { return value; } if (value == null) { return identity; } if (typeof value == 'object') { return isArray(value) ? baseMatchesProperty(value[0], value[1]) : baseMatches(value); } return property(value); }
咱们能够看出来,这个iteratee
迭代者其实就是一个函数,在_.map
中getIteratee(iteratee, 3)
,给了两个参数,按照逻辑,最终返回的是一个baseIteratee
,baseIteratee
的第一个参数value
就是iteratee
,这是一个函数,因此,baseIteratee
函数在第一个判断就返回了。
因此咱们能够将map函数简化为以下版本:
function map(collection,iteratee){ return arrayMap(collection,getIteratee(iteratee,3)); } function arrayMap(array, iteratee) { var index = -1, length = array == null ? 0 : array.length, result = Array(length); while (++index < length) { result[index] = iteratee(array[index], index, array); } return result; } function getIteratee() { var result = baseIteratee; return arguments.length ? result(arguments[0], arguments[1]) : result; } function baseIteratee(value) { if (typeof value == 'function') { return value; } }
能够看到,最终调用函数func
的时候会传入3个参数。array[index],index,array
。咱们能够实验,将func
实现以下:
function func(){ console.log(“arguments[0] ” + arguments[0]); console.log(“arguments[1] ” + arguments[1]); console.log(“arguments[2] ” + arguments[2]); console.log("-----") }
输出的结果也和咱们的预期同样,输出的第一个参数是该列表元素自己,第二个参数是数组下标,第三个参数是整个列表:
arguments[0] 6 arguments[1] 0 arguments[2] 6,8,10 ----- arguments[0] 8 arguments[1] 1 arguments[2] 6,8,10 ----- arguments[0] 10 arguments[1] 2 arguments[2] 6,8,10 ----- [ undefined, undefined, undefined ]
上面的分析就是抛砖引玉,先给出数组的分析,别的非数组,例如对象的遍历处理则会走到别的分支进行处理,各位看官有兴趣能够深刻研究。
_.ary
这个函数是用来限制参数个数的。这个函数咋一看好像没有什么用,但咱们考虑以下场景,将一个字符列表['6','8','10']
转为整型列表[6,8,10]
,用_.map
实现,咱们天然而然会写出这样的代码:
const _ = require("lodash"); _.map(['6','8','10'],parseInt);
好像很完美,咱们输出看看:
[ 6, NaN, 2 ]
很诡异是否是,看看内部到底发生了什么?其实看了上面的-.map
函数的分析,其实缘由已经很明显了。对于parseInt
函数而言,其接收两个参数,第一个是须要处理的字符串,第二个是进制:
/** * @param string 必需。要被解析的字符串。 * @param radix * 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。 * 若是省略该参数或其值为 0,则数字将以 10 为基础来解析。若是它以 “0x” 或 “0X” 开头,将以 16 为基数。 * 若是该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN */ parseInt(string, radix) /** 当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。 举例,若是 string 以 "0x" 开头,parseInt() 会把 string 的其他部分解析为十六进制的整数。若是 string 以 0 开头,那么 ECMAScript v3 容许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。若是 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。 */
那么这样的输出也就不难理解了:
处理第一个数组元素6的时候,parseInt
实际传入参数(6,0)
,那么按照十进制解析,会获得6
,处理第二个数组元素的时候传入的实际参数是(8,1)
,返回NaN
,对于第三个数组元素,按照2进制处理,则10
返回的是2
。
因此在上述需求的时候咱们须要限制参数的个数,这个时候_.ary
函数就登场了,上面的函数这样处理就没有问题了:
const _ = require("lodash"); _.map(['6','8','10'],_.ary(parseInt,1));
咱们看看这个函数是怎么实现的:
function ary(func, n, guard) { n = guard ? undefined : n; n = (func && n == null) ? func.length : n; return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); }
这个函数先检查n
的值,须要说明的是func.length
返回的是函数的声明参数个数。而后返回了一个createWrap
包裹函数,这个函数能够说是脏活累活处理工厂了,负责不少函数的包裹处理工做,并且为了提高性能,还将不一样的判断用bitflag
进行与/非处理,能够说是很用尽心机了。
/** * Creates a function that either curries or invokes `func` with optional * `this` binding and partially applied arguments. * * @private * @param {Function|string} func The function or method name to wrap. * @param {number} bitmask The bitmask flags. * 1 - `_.bind` 1 0b0000000000000001 * 2 - `_.bindKey` 0b0000000000000010 * 4 - `_.curry` or `_.curryRight`... 0b0000000000000100 * 8 - `_.curry` 0b0000000000001000 * 16 - `_.curryRight` 0b0000000000010000 * 32 - `_.partial` 0b0000000000100000 * 64 - `_.partialRight` 0b0000000001000000 * 128 - `_.rearg` 0b0000000010000000 * 256 - `_.ary` 0b0000000100000000 * 512 - `_.flip` 0b0000001000000000 * @param {*} [thisArg] The `this` binding of `func`. * @param {Array} [partials] The arguments to be partially applied. * @param {Array} [holders] The `partials` placeholder indexes. * @param {Array} [argPos] The argument positions of the new function. * @param {number} [ary] The arity cap of `func`. * @param {number} [arity] The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; if (!isBindKey && typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } var length = partials ? partials.length : 0; if (!length) { bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); partials = holders = undefined; } ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); arity = arity === undefined ? arity : toInteger(arity); length -= holders ? holders.length : 0; if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { var partialsRight = partials, holdersRight = holders; partials = holders = undefined; } var data = isBindKey ? undefined : getData(func); var newData = [ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity ]; if (data) { mergeData(newData, data); } func = newData[0]; bitmask = newData[1]; thisArg = newData[2]; partials = newData[3]; holders = newData[4]; arity = newData[9] = newData[9] === undefined ? (isBindKey ? 0 : func.length) : nativeMax(newData[9] - length, 0); if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); } if (!bitmask || bitmask == WRAP_BIND_FLAG) { var result = createBind(func, bitmask, thisArg); } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { result = createCurry(func, bitmask, arity); } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { result = createPartial(func, bitmask, thisArg, partials); } else { result = createHybrid.apply(undefined, newData); } var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); }
看上去太复杂了,把无关的代码削减掉:
function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { // 0000000100000000 & 0000000000000010 // var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; var isBindKey = 0; var length = 0; // if (!length) { // 0000000000100000 | 0000000001000000 // ~(0000000001100000) // 1111111110011111 // &0000000100000000 // 0000000100000000 = WRAP_ARY_FLAG // bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); // bitmask = WRAP_ARY_FLAG; // partials = holders = undefined; // } bitmask = WRAP_ARY_FLAG; partials = holders = undefined; ary = undefined; arity = arity === undefined ? arity : toInteger(arity); // because holders == undefined //length -= 0; // because isBindKey == 0 // var data = isBindKey ? undefined : getData(func); var data = getData(func); var newData = [ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity ]; if (data) { mergeData(newData, data); } func = newData[0]; bitmask = newData[1]; thisArg = newData[2]; partials = newData[3]; holders = newData[4]; arity = newData[9] = newData[9] === undefined ? func.length : newData[9]; result = createHybrid.apply(undefined, newData); var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); }
简化了一些以后咱们来到了createHybrid
函数,这个函数也巨复杂,因此咱们仍是按照简化方法,把咱们用不到的逻辑给简化:
function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { var isAry = bitmask & WRAP_ARY_FLAG, isBind = bitmask & WRAP_BIND_FLAG, isBindKey = bitmask & WRAP_BIND_KEY_FLAG, isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), isFlip = bitmask & WRAP_FLIP_FLAG, Ctor = isBindKey ? undefined : createCtor(func); function wrapper() { var length = arguments.length, args = Array(length), index = length; while (index--) { args[index] = arguments[index]; } if (isCurried) { var placeholder = getHolder(wrapper), holdersCount = countHolders(args, placeholder); } if (partials) { args = composeArgs(args, partials, holders, isCurried); } if (partialsRight) { args = composeArgsRight(args, partialsRight, holdersRight, isCurried); } length -= holdersCount; if (isCurried && length < arity) { var newHolders = replaceHolders(args, placeholder); return createRecurry( func, bitmask, createHybrid, wrapper.placeholder, thisArg, args, newHolders, argPos, ary, arity - length ); } var thisBinding = isBind ? thisArg : this, fn = isBindKey ? thisBinding[func] : func; length = args.length; if (argPos) { args = reorder(args, argPos); } else if (isFlip && length > 1) { args.reverse(); } if (isAry && ary < length) { args.length = ary; } if (this && this !== root && this instanceof wrapper) { fn = Ctor || createCtor(fn); } return fn.apply(thisBinding, args); } return wrapper; }
把不须要的逻辑削减掉:
function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { var isAry = 1; function wrapper() { var length = arguments.length, args = Array(length), index = length; while (index--) { args[index] = arguments[index]; } var thisBinding = this, fn = func; length = args.length; if (isAry && ary < length) { args.length = ary; } return fn.apply(thisBinding, args); } return wrapper; }
好了,绕了一大圈,终于看到最终的逻辑了,_.ary
函数其实就是把参数列表从新赋值了一下,并进行了长度限制。想一想这个函数实在是太麻烦了,咱们本身能够根据这个逻辑实现一个简化版的_.ary
:
function ary(func,n){ return function(){ var length = arguments.length, args = Array(length), index = length; while(index--){ args[index] = arguments[index]; } args.length = n; return func.apply(this,args); } }
试试效果:
console.log(_.map(['6','8','10'],ary(parseInt,1)));
工做得很不错:
[ 6, 8, 10 ]
今天分析这三个函数就花了一成天的时间,可是收获颇丰,可以静下心来好好分析一个著名的开源库,并可以理解透里面的一些逻辑,确实是一件颇有意思的事情。我会在有时间的时候把Lodash这个我很喜欢的库都好好分析一遍,尽我最大的努力将里面的逻辑表述清楚,但愿可以简明易懂。
最后,最晚下周一将会更新第二篇分析文章,敬请期待。
© 版权全部,未经容许不得转载,宣传一下我的博客 chenquan.me