翻译中,怎么那么多术语,读起来能get到他意思,翻译准确却好难。 如果翻译有误,不吝赐教,及时纠正。

还记得第一次你深入研究自己经常使用的库或者框架的源码吗?对于我来说,这发生在三年前,当我作为一个前端开发人员的首份工作时。

我们刚重写了一个用于生成电子课程的内部遗留框架。在重写之前,我们花时间去调研了很多不同的方案包括Mithril, Inferno, Angular, React, Aurelia, Vue, and Polymer。那时我还是一个入门级开发者(我刚从新闻学转行web开发),我记得每个框架的复杂性让我感到恐怖,并且不了解每个框架是如何运行的。

当我开始更深入地研究选定的框架Mithril, 我的理解能力日益增强。从那开始,我花费了很多时间去深入研究日常工作或者个人项目中使用的库的核心。在本文中,我将分享一些研究你喜爱的框架或者库并将它作为一种学习手段的方法。

2-improve-your-javascript-knowledge-by-reading-source-code.png
我第一次接触阅读源码是利用Mithril的hyperscript函数。
_

阅读源码的好处

阅读源码的主要好处之一是你可以学习到很多东西。在第一次了解Mithril的代码库前,我对virtual DOM的概念模糊。在看过源码之后,我了解到virtual DOM是一个涉及创建一棵描绘UI的对象树的技术。然后通过使用类似 document.createElement 之类的 DOM API,将那棵对象树映射成DOM元素。通过创建一棵描述未来状态UI的虚拟DOM对象树,然后将其与原来的DOM对象树进行对比来实现更新。

我已经在各种各样的文章和教程中阅读了关于Virtual DOM的内容,虽然这对我很有帮助,但对我来说,能够在工作上已经交付的应用程序上下文中观察它们更有启发。这还教会我在对比不同框架时需要问哪些问题。举个例子,现在我不关注GitHub的star数,而是问一些问题,例如“每个框架执行更新是如何影响性能和用户体验?”

另一个好处是增加了对优秀的应用程序体系架构的欣赏和认识。同时大多数开源项目的仓库都会采用相同的结构,但每个项目内容都不尽相同。Mithril的结构非常扁平,如果你熟悉它的API,你可以对目录里的代码比如 renderrouterrequest 等做出推测。另一方面,React的结构反映了它的新架构。(React的)维护者们将负责UI更新( react-reconciler )模块从负责渲染DOM元素( react-dom )模块中抽离。

模块分离的一个好处是现在开发者可以更容易写自定义的渲染器注册到 react-reconciler 包里。我最近学习的一个模块打包工具Parcel也有一个类似React的 packages 目录。这个关键的模块被命名为 parcel-bundler ,其中包含了负责打包、启动模块热替换和命令行工具的代码。

1-improve-your-javascript-knowledge-by-reading-source-code.jpg
不久之后,你正在阅读的源代码将带你进入JavaScript规范。

还有另外一个好处—令我感到惊喜的是—可以更轻松地阅读定义语言如何工作的官方JavaScript规范。第一次阅读规范是在研究 throw Errorthrow new Error 两者之间的区别时(有剧透慎入,这里什么都没有)。我对此进行了研究,因为我注意到Mithril 在其 m 函数的实现中使用 throw Error ,我猜想是否使用 throw Errorthrow new Error 更妙。从此,我了解到逻辑运算符 &&|| 不必返回布尔值,发现了管理 = = 相等操作符如何强制转换值类型的规则Object.prototype.toString.call({}) 返回 '[object Object]'原因

读取源码的技巧

处理源码有很多种方式。我已经找到一个最容易开始的方式就是通过从所选库中挑选一个方法,并记录调用时发生的情况。不要去记录每个单步操作,而要尝试去明确它的整体流程和结构。

最近我调试了 ReactDOM.render 的源码,因此了解了很多关于React Fiber及其实现的原因。幸运的是,由于React是一个流行框架,我看了很多其他开发者针对同一问题撰写的文章,这加快了(调试)过程。

这次深入研究还向我介绍了合作调度的概念、window.requestIdleCallback方法和一个链表的真实案例(React通过将待更新任务放入一个优先处理链表的队列中来处理更新)。在深入研究源码时,建议使用这个库创建一个基础应用。这将使得debug非常容易,因为不必去处理由其他库造成的堆栈跟踪。

若不深入审查,我会打开当前项目的 /node_modules 目录否则就去查看GitHub仓库。这经常发生在碰到一个bug或者有趣的功能。在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中。

这些发现可能不会立即有用,但对库和框架使用的依赖项有一个大概的了解还是很有帮助的。

在需要调试前端代码时,浏览器的调试工具是你最好的帮手。除其他功能之外,调试工具允许随意停止程序并查看此时程序的状态,跳过一个function的执行或单步调试或退出程序。有时候因代码被压缩而无法接调试代码。我会试着去解压代码,并且复制解压后的代码到对应的 /node_modules 文件中。
3-improve-your-javascript-knowledge-by-reading-source-code.png

像其他任何应用程序一样进行调试。作出假设,然后进行检验。

案例研究:Redux的Connect功能

React-Redux 是用于管理Reac应用状态的库。
在处理诸如此类流行库时,我从搜索有关其实现的文章开始。在做这个案例研究时,我看了这篇文章。这是阅读源码的额外好处。研究阶段通常让你接触到诸如此类的内容丰富多彩的文章,这些文章恰可以提高你的思维及理解力。
React-Redux提供一个 connect 函数,用于链接React组件到应用的Redux store。怎么样?当然,根据文档描述如下:

“…returns a new, connected component class that wraps the component you passed in.”

看完之后,我会问如下几个问题:

  • 自己是否了解一些可以返回相同输入并封装额外功能的模式和概念?
  • 如果知道任意模式,我该如何基于文档给出的说明实现此模式?

通常下一步会创建一个使用 connect 非常基础的示例应用。然而,这次我选择使用我们在Limejump上构建的新React应用程序,因为我想了解最终即将进入生产环境的应用程序上下文中的 connect 是怎样的。

我关注的组件如下所示:

  1. class MarketContainer extends Component {
  2. // code omitted for brevity
  3. }
  4. const mapDispatchToProps = dispatch => {
  5. return {
  6. updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today))
  7. }
  8. }
  9. export default connect(null, mapDispatchToProps)(MarketContainer);

它是一个包装着四个小的连接组件的容器组件。在导出 connect 方法的文件中,你首先遇到的是这个注释:connect is a facade over connectAdvanced。事不宜迟,我们有了第一次学习时刻:一个观察实际中的外观设计模式的机会。在文件末尾, connect 导出一个 createConnect 函数的调用。它的参数是一串已被解构的默认值,类似这样:

  1. export function createConnect({
  2. connectHOC = connectAdvanced,
  3. mapStateToPropsFactories = defaultMapStateToPropsFactories,
  4. mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  5. mergePropsFactories = defaultMergePropsFactories,
  6. selectorFactory = defaultSelectorFactory
  7. } = {})

再一次,我们遇到了另一个学习时刻:导出调用的函数以及结构默认函数参数。解构部分是一个学习时刻,因为代码可以写成这样:

  1. export function createConnect({
  2. connectHOC = connectAdvanced,
  3. mapStateToPropsFactories = defaultMapStateToPropsFactories,
  4. mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  5. mergePropsFactories = defaultMergePropsFactories,
  6. selectorFactory = defaultSelectorFactory
  7. })

这会造成此错误:

  1. Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.

这是因为函数没有返回默认的参数。
提醒有关更多的信息,请参考David Walsh的文章。有些学习时机可能显得微不足道,根据你对语言的了解情况,因此最好集中精力于以前从未见过的事物或需要学习更多的知识。

createConnect 函数本身没有做任何事情。它返回 connect 函数,我使用过如下:

  1. export default connect(null, mapDispatchToProps)(MarketContainer)

它有四个可选参数,前三个参数每个都要传入一个 match 函数,这个函数根据参数是否存在、参数类型来帮助定义其行为。目前,因为传入 match 的第二个参数是导入 connect 的三个函数之一,因此我必须考虑遵循哪个线程。(todo)

若这些参数都是function类型,proxy代理函数用于将第一个参数封装给 connectisPlainObject工具用于检查普通对象;warning模块展示了如何设置调试器以在所有异常时中断等等都是学习时刻。看过match函数之后,我们看到 connectHOCconnectHOC 函数获取React组件并将其与Redux连接。这是另一个函数调用,该函数返回wrapWithConnect,它实际上处理将组件连接到store的过程。

看着 connectHOC 的实现,我可以理解为什么 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中的另一个小心机是此代码:

  1. while (Object.getPrototypeOf(baseProto) !== null) {
  2. baseProto = Object.getPrototypeOf(baseProto)
  3. }

一些谷歌搜索使我转向StackOverflow的解决思路和Redux的issue,解释了该代码如何处理诸如检查源自iFrame的对象之类的情况。

阅读源码有用的链接

原文地址:https://www.smashingmagazine.com/2019/07/javascript-knowledge-reading-source-code/ 原文作者:Carl Mungazi 参考资料:React文档 特别感谢:Filna