[译] 通过阅读代码来提升你的 JavaScript 水平

1,537 阅读10分钟

写在前面的话

原文地址: Improve Your JavaScript Knowledge By Reading Source Code

为了符合阅读习惯,本篇采用意译。

前言:在你的早期程序生涯中,弄清楚开源库和框架的源码是一个不小的挑战。这篇文章中,作者卡尔给我们分享了他是如何客服恐惧开始阅读源码的,这对提高知识水平有帮助。他也用 Redux 作为一个例子来展示如何攻克一个库。

你还记得第一次去深度阅读源码时的情况吗?对我而言,是三年前的事了。

当时我们已经完成了内部遗留框架的重写。在重写之初,我们花时间去调查了多种解决方案包括 Mithril,Inferno, Angular, React, Aurelia, Vue 和 Polymer。作为一个刚刚从新闻行业转行的我,我还记得被每种框架的复杂性支配的恐惧,同时不理解它们是如何工作的。

当我开始研究选择的框架时,我的理解也就在增长了。从那时起,我深入研究那些在工作或个人项目中使用的库,在数个小时之内,我的 Javascript 知识和编程技巧也都受益匪浅。在这片文章中,我会分享几种方式,你可以找一个你喜欢的库或者框架,然后使用它作为学习工具。

阅读源码的好处

学习源码的主要好处是能学到很多东西。当我第一次看到Mithril的代码库时,我对虚拟DOM的含义有一个模糊的概念。 当我完成时,我知道虚拟DOM是一种技术,它涉及创建描述用户界面应该是什么样的对象树。 然后使用DOM API(例如document.createElement)将该树转换为DOM元素。 通过创建描述用户界面的未来状态的新树,然后将其与旧树中的对象进行比较来执行更新。

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

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

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

另一个好处 - 令我感到惊讶的是 - 可以更轻松地阅读官方JavaScript规范定义的语言如何工作。 我第一次阅读规范是在调查 throw Error 和 throw 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上阅读代码时,请确保正在阅读最新版本。可以通过单击用于更改分支的按钮并选择“tags”来查看具有最新版本标记的提交中的代码。库和框架永远在进行更改,因此不需要了解可能在下一版本中删除的内容。

另一种不那么简单的阅读源代码的方式是我喜欢称之为“粗略一瞥”的方法。在我开始阅读代码的早期,我安装了express.js,打开了它的/ node_modules文件夹并完成了它的依赖项。如果自述文件没有给我一个令人满意的解释,我会阅读源代码。这样做让我得到了以下有趣的发现:

  • Express依赖于两个模块,这两个模块合以非常不同的方式并对象。 merge-descriptors只添加直接在原对象上直接找到的属性,它还合并了不可枚举的属性,而utils-merge 只迭代对象的可枚举属性以及在其原型链中找到的属性。 merge-descriptors使用 Object.getOwnPropertyNames()Object.getOwnPropertyDescriptor(),而utils-merge使用for..in;
  • setprototypeof 模块提供了一种设置实例化对象原型的跨平台方式;
  • escape-html是一个78行模块,用于转义一串内容。可以在HTML内容中进行插值。

虽然这些发现不太可能立即有用,但是库或框架使用的依赖关系有一个大致的了解是有用的。

在调试前端代码时,浏览器的调试工具是最好的朋友。除此之外,它们允许您随时停止程序并检查其状态,跳过函数的执行或进入或退出程序。有时这不能立即生效,因为代码被压缩。我倾向于将其解压并将解压的代码复制到 /node_modules文件夹中的相关文件中。

案例研究:Redux 的 Connent Function

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)

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

如果这些参数是函数,是检查普通对象的实用程序或者是设置断点来调试暴露错误的警告模块,那么现在就是学习代理函数的时候,这个函数包裹了第一个参数给 connect。在匹配函数之后,来看看 connectHOC,这个函数接受我们的 React 组件并将它连接到 Redux。它是另一个函数调用,它返回 wrapWithConnect,它实际上处理将组件连接到存储的函数。

看看 connectHOC 的实现,我可以理解为什么它需要连接来隐藏它的实现细节。它是 React-Redux 的核心,包含不需要通过连接公开的逻辑。即使这里的探讨即将结束,如果继续,这将是查阅之前发现的参考资料的最佳时机,因为它包含对代码库的非常详细的解释。

总结

读取源代码起初很困难,但与任何事情一样,随着时间的推移变得更容易。我们的目标不是理解一切,而是要获得不同的视角和新知识。关键是要对整个过程进行深思熟虑,并对所有事情充满好奇。

举个例子,我发现 isPlainObject 函数很有趣,因为它使用了if(typeof obj!=='object'|| obj === null) return false以确保给定的参数是普通对象。当我第一次阅读它的实现时,我想知道为什么它没有使用 Object.prototype.toString.call(opts)!=='[object Object]',这是用更少的代码并区分对象和对象子类型,比如 Date 对象。但是,阅读下一行显示,例如,在使用 connect 的开发人员返回 Date 对象的极不可能的事件中,这将由 Object.getPrototypeOf(obj)=== null 检查处理。

isPlainObject 的另一个吸引人的是这段代码:

whileObject.getPrototypeOf(baseProto)!== null){
 baseProto = Object.getPrototypeOf(baseProto)
}

谷歌一下可以找到相关话题,StackOverflow下的Redux问题,解释该代码如何检查源自iFrame的对象。

参考

pic