前言
你好,我是若川。这是
学习源码整体架构系列第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。
学习源码整体架构系列文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
感兴趣的读者可以点击阅读。
underscore源码分析的文章比较多,而lodash源码分析的文章比较少。原因之一可能是由于lodash源码行数太多。注释加起来一万多行。
分析lodash整体代码结构的文章比较少,笔者利用谷歌、必应、github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用lodash,而且都或多或少知道,lodash比underscore性能好,性能好的主要原因是使用了惰性求值这一特性。
本文章学习的lodash的版本是:v4.17.15。unpkg.com地址 https://unpkg.com/lodash@4.17.15/lodash.js
文章篇幅可能比较长,可以先收藏再看,所以笔者使用了展开收缩的形式。
导读:
文章主要学习了
runInContext()导出_lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。
匿名函数执行
1 | 复制代码;(function() { |
暴露 lodash
1 | 复制代码var _ = runInContext(); |
runInContext 函数
这里的简版源码,只关注函数入口和返回值。
1 | 复制代码var runInContext = (function runInContext(context) { |
可以看到申明了一个runInContext函数。里面有一个lodash函数,最后处理返回这个lodash函数。
再看lodash函数中的返回值 new LodashWrapper(value)。
LodashWrapper 函数
1 | 复制代码function LodashWrapper(value, chainAll) { |
设置了这些属性:
__wrapped__:存放参数value。
__actions__:存放待执行的函数体func, 函数参数 args,函数执行的this 指向 thisArg。
__chain__、undefined两次取反转成布尔值false,不支持链式调用。和underscore一样,默认是不支持链式调用的。
__index__:索引值 默认 0。
__values__:主要clone时使用。
接着往下搜索源码,LodashWrapper,
会发现这两行代码。
1 | 复制代码LodashWrapper.prototype = baseCreate(baseLodash.prototype); |
接着往上找baseCreate、baseLodash这两个函数。
baseCreate 原型继承
1 | 复制代码// 立即执行匿名函数 |
笔者画了一张图,表示这个关系。
lodash 原型关系图
衍生的 isObject 函数
判断typeof value不等于null,并且是object或者function。
1 | 复制代码function isObject(value) { |
Object.create() 用法举例
面试官问:能否模拟实现JS的new操作符 之前这篇文章写过的一段,所以这里收缩起来了。
点击 查看 Object.create() 用法举例
笔者之前整理的一篇文章中也有讲过,可以翻看JavaScript 对象所有API解析
Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是undefined)。
1 | 复制代码var anotherObject = { |
对于不支持ES5的浏览器,MDN上提供了ployfill方案。
1 | 复制代码if (typeof Object.create !== "function") { |
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) { |
接下来先看衍生的函数。
其实看到具体定义的函数代码就大概知道这个函数的功能。为了不影响主线,导致文章篇幅过长。具体源码在这里就不展开。
感兴趣的读者可以自行看这些函数衍生的其他函数的源码。
mixin 衍生的函数 keys
在 mixin 函数中 其实最终调用的就是 Object.keys
1 | 复制代码function keys(object) { |
mixin 衍生的函数 baseFunctions
返回函数数组集合
1 | 复制代码function baseFunctions(object, props) { |
mixin 衍生的函数 isFunction
判断参数是否是函数
1 | 复制代码function isFunction(value) { |
mixin 衍生的函数 arrayEach
类似 [].forEarch
1 | 复制代码function arrayEach(array, iteratee) { |
mixin 衍生的函数 arrayPush
类似 [].push
1 | 复制代码function arrayPush(array, values) { |
mixin 衍生的函数 copyArray
拷贝数组
1 | 复制代码function copyArray(source, array) { |
mixin 源码解析
lodash 源码中两次调用 mixin
1 | 复制代码// Add methods that return wrapped values in chain sequences. |
结合两次调用mixin 代入到源码解析如下
点击这里展开mixin源码及注释
1 | 复制代码function mixin(object, source, options) { |
小结:简单说就是把lodash上的静态方法赋值到lodash.prototype上。分两次第一次是支持链式调用(lodash.after等 153个支持链式调用的方法),第二次是不支持链式调用的方法(lodash.add等152个不支持链式调用的方法)。
lodash 究竟在_和_.prototype挂载了多少方法和属性
再来看下lodash究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。
使用for in循环一试便知。看如下代码:
1 | 复制代码var staticMethods = []; |
其实就是上文提及的 lodash.after 等153个支持链式调用的函数 、lodash.add 等 152不支持链式调用的函数赋值而来。
1 | 复制代码var prototypeMethods = []; |
相比lodash上的静态方法多了12个,说明除了 mixin 外,还有12个其他形式赋值而来。
支持链式调用的方法最后返回是实例对象,获取最后的处理的结果值,最后需要调用value方法。
笔者画了一张表示lodash的方法和属性挂载关系图。
lodash的方法和属性挂载关系
请出贯穿下文的简单的例子
1 | 复制代码var result = _.chain([1, 2, 3, 4, 5]) |
也就是说这里lodash聪明的知道了最后需要几个值,就执行几次map循环,对于很大的数组,提升性能很有帮助。
而underscore执行这段代码其中map执行了5次。
如果是平常实现该功能也简单。
1 | 复制代码var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3); |
而相比lodash这里的map执行了5次。
1 | 复制代码// 不使用 map、slice |
简单说这里的map方法,添加 LazyWrapper 的方法到 lodash.prototype存储下来,最后调用 value时再调用。
具体看下文源码实现。
添加 LazyWrapper 的方法到 lodash.prototype
主要是如下方法添加到到 lodash.prototype 原型上。
1 | 复制代码// "constructor" |
点击这里展开具体源码及注释
1 | 复制代码// Add `LazyWrapper` methods to `lodash.prototype`. |
小结一下,写了这么多注释,简单说:其实就是用LazyWrapper.prototype 改写原先在lodash.prototype的函数,判断函数是否需要使用惰性求值,需要时再调用。
读者可以断点调试一下,善用断点进入函数功能,对着注释看,可能会更加清晰。
点击查看断点调试的部分截图
例子的chain和map执行后的debugger截图
例子的chain和map执行后的结果截图
链式调用最后都是返回实例对象,实际的处理数据的函数都没有调用,而是被存储存储下来了,最后调用value方法,才执行这些函数。
lodash.prototype.value 即 wrapperValue
1 | 复制代码function baseWrapperValue(value, actions) { |
如果是惰性求值,则调用的是 LazyWrapper.prototype.value 即 lazyValue。
LazyWrapper.prototype.value 即 lazyValue 惰性求值
点击这里展开lazyValue源码及注释
1 | 复制代码function LazyWrapper(value) { |
笔者画了一张 lodash和LazyWrapper的关系图来表示。
lodash和LazyWrapper的关系图
小结:lazyValue简单说实现的功能就是把之前记录的需要执行几次,把记录存储的函数执行几次,不会有多少项数据就执行多少次,而是根据需要几项,执行几项。
也就是说以下这个例子中,map函数只会执行3次。如果没有用惰性求值,那么map函数会执行5次。
1 | 复制代码var result = _.chain([1, 2, 3, 4, 5]) |
总结
行文至此,基本接近尾声,最后总结一下。
文章主要学习了
runInContext()导出_lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。
分享一个只知道函数名找源码定位函数申明位置的VSCode 技巧:Ctrl + p。输入 @functionName 定位函数functionName在源码文件中的具体位置。如果知道调用位置,那直接按alt+鼠标左键即可跳转到函数申明的位置。
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。
推荐阅读
lodash 仓库 | lodash 官方文档 | lodash 中文文档
本文章学习的lodash的版本v4.17.15 unpkg.com链接
笔者往期文章
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客-若川,使用vuepress重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~
若川视野
本文使用 mdnice 排版
本文转载自: 掘金