通过阅读源代码提高你的 JavaScript 知识

已发表: 2022-03-10
快速总结 ↬当您还处于编程生涯的早期阶段时,挖掘开源库和框架的源代码可能是一项艰巨的工作。 在本文中,Carl Mungazi 分享了他如何克服恐惧并开始使用源代码来提高自己的知识和技能。 他还使用 Redux 来展示他如何分解库。

你还记得你第一次深入挖掘你经常使用的库或框架的源代码吗? 对我来说,那一刻发生在三年前我作为前端开发人员的第一份工作期间。

我们刚刚重写了用于创建电子学习课程的内部遗留框架。 在重写开始时,我们花时间研究了许多不同的解决方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。 由于我是一个初学者(我刚刚从新闻业转向网络开发),我记得对每个框架的复杂性感到害怕,并且不了解每个框架是如何工作的。

当我开始更深入地研究我们选择的框架 Mithril 时,我的理解增长了。 从那时起,我花大量时间深入研究我每天在工作中或在我自己的项目中使用的库的内容,极大地帮助了我对 JavaScript 和一般编程的了解。 在这篇文章中,我将分享一些你可以使用你最喜欢的库或框架并将其用作教育工具的方法。

秘银超标函数源码
我对阅读代码的第一次介绍是通过 Mithril 的超脚本函数。 (大预览)
跳跃后更多! 继续往下看↓

阅读源代码的好处

阅读源代码的主要好处之一是你可以学到很多东西。 当我第一次查看 Mithril 的代码库时,我对虚拟 DOM 是什么有一个模糊的概念。 完成后,我了解到虚拟 DOM 是一种涉及创建对象树的技术,这些对象树描述了您的用户界面应该是什么样子。 然后使用诸如document.createElement之类的 DOM API 将该树转换为 DOM 元素。 通过创建一个描述用户界面未来状态的新树然后将其与旧树中的对象进行比较来执行更新。

我已经在各种文章和教程中阅读了所有这些内容,虽然它很有帮助,但能够在我们发布的应用程序的上下文中观察它对我来说非常有启发性。 它还教会了我在比较不同的框架时要问哪些问题。 例如,我现在知道要问诸如“每个框架执行更新的方式如何影响性能和用户体验?”之类的问题,而不是查看 GitHub 的星星。

另一个好处是增加了您对良好应用程序架构的欣赏和理解。 虽然大多数开源项目的存储库通常遵循相同的结构,但每个项目都包含差异。 Mithril 的结构非常扁平,如果您熟悉它的 API,您可以对诸如renderrouterrequest文件夹中的代码进行有根据的猜测。 另一方面,React 的结构反映了它的新架构。 维护者将负责 UI 更新的模块( react-reconciler )与负责渲染 DOM 元素的模块( react-dom )分开。

这样做的好处之一是,开发人员现在可以更轻松地通过挂钩到react-reconciler包来编写自己的自定义渲染器。 Parcel,我最近一直在研究的一个模块捆绑器,也有一个像 React 这样的packages文件夹。 关键模块名为parcel-bundler ,它包含负责创建包、启动热模块服务器和命令行工具的代码。

JavaScript 规范中解释 Object.prototype.toString 工作原理的部分
不久之后,您正在阅读的源代码将引导您了解 JavaScript 规范。 (大预览)

还有一个好处——这让我很惊喜——是你可以更轻松地阅读官方的 JavaScript 规范,该规范定义了该语言的工作方式。 我第一次阅读规范是在我研究throw Errorthrow new Error之间的区别时(剧透警告 - 没有)。 我对此进行了调查,因为我注意到 Mithril 在其m函数的实现中使用了throw Error ,我想知道使用它是否比throw new Error有好处。 从那以后,我也学会了逻辑运算符&&|| 不一定返回布尔值,找到管理==相等运算符如何强制值的规则以及Object.prototype.toString.call({})返回'[object Object]'原因。

阅读源代码的技巧

有很多方法可以处理源代码。 我发现最简单的开始方法是从您选择的库中选择一个方法并记录调用它时发生的情况。 不要记录每一步,而是尝试确定其整体流程和结构。

我最近使用ReactDOM.render完成了这项工作,因此学到了很多关于 React Fiber 以及其实现背后的一些原因。 值得庆幸的是,由于 React 是一个流行的框架,我遇到了很多其他开发人员就同一问题撰写的文章,这加快了进程。

这次深入探讨还向我介绍了协作调度的概念、 window.requestIdleCallback方法和链表的真实示例(React 通过将更新放入队列中来处理更新,队列是优先更新的链表)。 这样做时,建议使用该库创建一个非常基本的应用程序。 这使得调试时更容易,因为您不必处理由其他库引起的堆栈跟踪。

如果我不进行深入审查,我将在我正在处理的项目中打开/node_modules文件夹,或者我将转到 GitHub 存储库。 当我遇到错误或有趣的功能时,通常会发生这种情况。 在 GitHub 上阅读代码时,请确保您阅读的是最新版本。 您可以通过单击用于更改分支的按钮并选择“标签”来查看带有最新版本标签的提交中的代码。 库和框架永远在发生变化,因此您不想了解下一个版本中可能会删除的内容。

另一种阅读源代码较少涉及的方法是我喜欢称之为“粗略浏览”的方法。 在我开始阅读代码的早期,我安装了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文件夹中的相关文件中。

ReactDOM.render 函数的源代码
像对待任何其他应用程序一样进行调试。 形成一个假设,然后对其进行检验。 (大预览)

案例研究:Redux 的连接函数

React-Redux 是一个用于管理 React 应用程序状态的库。 在处理诸如此类的流行库时,我首先搜索有关其实现的文章。 在此案例研究中,我遇到了这篇文章。 这是阅读源代码的另一个好处。 研究阶段通常会引导您阅读诸如此类的内容丰富的文章,这些文章只会提高您自己的思维和理解。

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: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. 这是因为该函数没有可依赖的默认参数。

注意有关这方面的更多信息,您可以阅读 David Walsh 的文章。 有些学习时刻可能看起来微不足道,这取决于您对语言的了解,因此最好专注于您以前从未见过或需要了解更多的东西。

createConnect本身在其函数体中什么也不做。 它返回一个名为connect的函数,我在这里使用的函数:

 export default connect(null, mapDispatchToProps)(MarketContainer)

它有四个参数,都是可选的,前三个参数每个都通过一个match函数,该函数根据参数是否存在以及它们的值类型来帮助定义它们的行为。 现在,因为提供给match的第二个参数是导入connect的三个函数之一,所以我必须决定要遵循哪个线程。

如果这些参数是函数,则用于包装第一个参数以connect的代理函数、用于检查普通对象的isPlainObject实用程序或揭示如何设置调试器以在所有异常上中断的warning模块都有学习时刻。 在匹配函数之后,我们来到connectHOC ,该函数将我们的 React 组件连接到 Redux。 这是另一个返回wrapWithConnect的函数调用,该函数实际处理将组件连接到商店。

查看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 问题,解释了该代码如何处理案例,例如检查源自 iFrame 的对象。

阅读源代码的有用链接

  • “如何逆向工程框架”,Max Koretskyi,Medium
  • “如何阅读代码”,Aria Stewart,GitHub