怎样通过读源码提高你的 JavaScript 知识
作者:Carl Mungazi翻译:疯狂的技术宅
原文:https://www.smashingmagazine....
未经允许严禁转载
你还记得自己第一次深入挖掘常用的库或框架的源代码时的情景吗?对我而言,那一刻是我三年前作为前端开发人员的第一份工作。
我们刚刚完成了用于创建在线课程的内部遗留框架的重写。在开始重写时,我们花时间研究了许多不同的解决方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。因为我是一个萌新(我刚从新闻转向网络开发),我记得每个框架的复杂性都让人感到害怕,而且不理解框架的工作方式。
当我开始更深入地研究我们选择的 Mithril 框架时,我的能力增长了。从那以后,我对 JavaScript 的了解以及一般的编程方式得到了很大的提高,我花了很多时间深入研究每天在工作种或在自己的项目中使用的库。在本文中,我将分享一些分析库或框架的方法。
通过 Mithril 的 hyperscript 功能介绍如何去阅读源代码。
阅读源代码的好处
阅读源代码的好处之一是可以使你学到更多的东西。当我第一次看到 Mithril 的代码库时,对虚拟 DOM 的含义只有一个模糊的概念。当我读完时,就知道了虚拟 DOM 是一种技术,它涉及创建描述用户界面的对象树应该是什么样的。然后使用 DOM API(例如 document.createElement
)将该树转换为 DOM 元素。通过创建描述用户界面未来状态的新树,然后将其与旧树中的对象进行比较来执行更新。
之前我已经在各种文章和教程中读到过这些内容,虽然很有帮助,但是在程序的上下文中能够观察它对我来说是非常有启发性的。它还告诉我在比较不同的框架时要问哪些问题。例如我现在不是去查看 GitHub 上的 star 数量,而是会问“每个框架执行更新的方式如何影响性能和用户体验?”这样的问题。
另一个好处是增加你对良好应用架构的理解。虽然大多数开源项目通常与其存储库遵循相同的结构,但每个项目都包含差异。Mithril 的结构非常扁平,如果你熟悉它的 API,可以对文件夹中的代码进行有根据的猜测,比如render
、router
和 request
等。另一方面,React 的结构也反映了它的新架构。维护者将负责 UI 更新的模块(react-reconciler
)与负责渲染 DOM 元素的模块(react-dom
)分开。
这样做的好处之一是,开发人员现在可以通过 hook 到 react-reconciler
包来编写自己的自定义渲染器。我最近研究过的模块捆绑包 Parcel 也有像 React 这样的 packages
文件夹。密钥模块名为 parcel-bundler
,它包含负责创建捆绑包、热启动模块服务器和命令行工具的代码。
不久之后,你正在阅读的源代码将引导你进入 JavaScript 规范。
另一个令我感到惊讶的好处是:你可以更轻松地阅读官方 JavaScript 规范,该规范定义了语言的工作方式。我第一次阅读规范的时候是在分析 throw Error
和 throw new Error
之间的区别。之所以要分析这个,是因为我注意到 Mithril 在其 m
函数的实现中使用了 throw Error
,我想知道这样是不是比 throw new Error
更好。从那以后,我也学会了逻辑运算符 &&
和 ||
不一定返回布尔值,找到了控制 ==
等式运算符如何强制赋值的规则和Object.prototype.toString.call({})
返回 '[object Object]'
的原因 。
阅读源代码的技巧
有很多方法可以处理源代码。我发现最简单的方法是,从你选择的库中挑选一种方法,并去记录调用它时会发生什么。不是去记录每一步,而是要尝试确定其整体流程和结构。
我最近用这种方法阅读了 ReactDOM.render
的代码 ,因此学到了很多关于 React Fibre 及其实现背后的一些原理。值得庆幸的是,由于 React 是一个流行的框架,我在同一个问题上看到过很多其他开发人员撰写的文章,这也加快了这个过程。
这深入探讨并向我介绍了co-operative scheduling,window.requestIdleCallback
方法和真实的链表的示例(React 通过把更新放入队列来处理更新,这是优先更新的链表)。执行此类操作时,建议用库创建一个非常基本的程序。这可以使得调试时更容易,因为你不用去处理由其他库引起的栈跟踪信息。
如果没有对代码进行深入研究,我会正在处理的项目中打开 /node_modules
文件夹,或者转到 GitHub 存储库。当我遇到错误或有趣的功能时,通常会发生这种情况。在 GitHub 上阅读代码时,请确保你正在阅读最新版本。你可以通过单击用于更改分支的按钮,并选择 “tags” 来查看带有最新版本标记的代码。库和框架永远在持续更新,所以你不希望把精力花费在下一版本中可能会删除的内容。
还有另一种阅读源代码的方式,我喜欢称之为“粗略一瞥”,这种方法并不那么简单。在我刚刚开始阅读代码的时候安装了 express.js,我打开了它的 /node_modules
文件夹并浏览了它的依赖项。如果 README
没有给我一个满意的解释,我就会阅读源代码。这样做让我得到了一些有趣的发现:
- Express 依赖两个模块,这两个模块都可以合并对象,但是合并方式的差异很大。
merge-descriptors
只添加在源对象上直接找到的属性,它还合并了不可枚举的属性,而utils-merge
只迭代对象的可枚举属性以及在其原型链中找到的属性。merge-descriptors
使用Object.getOwnPropertyNames()
和Object.getOwnPropertyDescriptor()
,而utils-merge
使用了for..in
; setprototypeof
模块提供了一种跨平台设置实例化对象原型的方式;escape-html
是一个有 78 行代码的模块,用于转义字符串内容,因此可以用它在 HTML 内容中进行插值。
虽然阅读源代码的结果不太可能立即就能用得上,但是能够使你对自己使用的库或框架的依赖关系有一个大致的了解,这是非常有用的。
在调试前端代码时,浏览器的调试工具是你最好的朋友。除此之外,它们允许你随时暂停程序并检查其状态、跳过函数的执行、进入或退出程序。不过有时这不可能立即做到,因为代码有可能已经被压缩过。我倾向于取消它们的通知,并将未经压缩的代码复制到 /node_modules
目录中的相关文件里。
像其他程序一样进行调试。形成一个假设,然后进行测试。
案例研究:Redux的 Connect 函数
React-Redux 是一个用于管理 React 应用状态的库。在处理诸如此类的库时,我首先会搜索已经编写过有关其实现的文章。在这个案例研究中,我遇到了这篇文章(https://blog.isquaredsoftware...)。这是阅读源代码的另一个好处。研究阶段通常会引导你去阅读这样的信息性文章,通常这些文章只会改善你自己的思路和理解。
connect
是一个 React-Redux 函数,它将 React 组件连接到应用程序的 Redux 存储。怎么样?好吧,根据官方文档的说明,它执行以下操作:
“...返回一个新的连接组件类,它将会包装你传入的组件。”
看完之后,我会问下列问题:
- 我知不知道函数接受输入的那些模式或概念,然后返回包含其他功能的相同输入?
- 如果我知道此类模式,又将如何根据文档中给出的解释实现此模式?
通常,下一步是创建一个使用 connect
的非常基本的示例程序。但是在这种情况下,我选择使用我们在 Limejump 上构建的新 React 程序,因为我想在程序的上下文中理解 connect
,最终再进入生产环境。
我关注的组件看起来像这样:
class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);
它是一个容器组件,包裹着四个较小的连接组件。你在导出 connect
方法的文件中遇到的第一件事就是这个评论:connect 是 connectAdvanced 的外观。这时我们就有了第一个学习的点:有机会观察外观设计模式。在文件的末尾,我们看到 connect
导出了一个名为 createConnect
的函数的调用。它的参数是一堆默认值,它们已被解构,如下所示:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})
同样,我们遇到了另一个学习的点:导出调用函数和解构默认函数参数。解构部分是一个学习的点,因为代码编写如下:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })
它会导致这个错误 Uncaught TypeError
:无法解析 undefined
或 null
的属性 connectHOC
。这是因为该函数没有默认参数可以依赖。
注意:有关此内容的更多信息,请阅读 David Walsh 的文章(https://davidwalsh.name/destr...)。根据你对语言的了解,一些学习的点可能看起来微不足道,所以最好专注于你以前从未见到过的或需要了解更多信息的内容。
createConnect
本身在其函数体中没有任何功能。它返回一个名为 connect
的函数,我在代码里使用的函数:
export default connect(null, mapDispatchToProps)(MarketContainer)
它需要四个参数,都是可选的,前三个参数根据参数是否存在及其值类型来定义它们的行为,这是通过 match
函数来实现的。现在因为提供给 match
的第二个参数是导入 connect
的三个函数之一,我必须决定应该遵循哪个线程。
在这里学习的重点是:如果这些参数是函数,用于将第一个参数包装为 connect
的代理函数,isPlainObject
用于检查普通对象或 warning
模块,它揭示了如何将调试器设置为中断所有异常。在匹配函数之后,我们来到 connectHOC
,这个函数接受我们的 React 组件并将它连接到 Redux。它是另一个函数调用,返回 wrapWithConnect
,实际上它用来处理将组件连接到 store 的函数。
看一看 connectHOC
的实现,我可以理解为什么它需要 connect
来隐藏它的实现细节。它是 React-Redux 的核心,其中包含不需要通过 connect
公开的逻辑。我将结束这里的深度探讨,如果我继续的话,将是查阅我之前发现的参考资料的最佳时机,因为它包含了对代码库的非常详细的解释。
总结
刚开始阅读源代码时很困难,但与所有的事情一样,随着时间的推移会变得更容易。我们的目标不是理解一切,而是要获得不同的思路和新知识。关键是要对整个过程进行深思熟虑,并对所有事物充满好奇心。
例如,我发现 isPlainObject
函数很有趣,因为它用 if (typeof obj !== 'object' || obj === null) return false
来确保给定的参数是普通对象。当我第一次阅读它的代码实现时,想知道为什么它没有用Object.prototype.toString.call(opts)!=='[object Object]'
,这可以用更少的代码来区分对象和对象子类型,例如 Date 对象。但是阅读下一行就会发现,当开发人员在使用 connect
返回 Date 对象的极不可能的事件中,将由 Object.getPrototypeOf(obj)=== null
检查来进行处理。
isPlainObject
的另一个吸引人的地方是这段代码:
while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }
谷歌搜索引导我找到这个 StackOverflow 帖子和 Redux issue,它们解释了该代码如何进行处理的案例,例如检查源自 iFrame 的对象。
有用的链接
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现