前言
你好,我是若川。这是
学习源码整体架构系列
第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。
学习源码整体架构系列
文章如下:
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 排版
本文转载自: 掘金