作者 | Carl Mungazi
译者 | 王强
编辑 | Yonie
当你还是编程行业的一名菜鸟时,深入研究开源库和框架的源代码对你来说可能就像读天书一样。Carl Mungazi 也有过这样的经历,但他克服了恐惧心理开始研究源代码来提高他的知识和技能,并在这篇文章中分享了相关经验;他还使用 Redux 来演示了如何深入研究一个库。 你还记得自己第一次深入挖掘经常使用的库或框架的源代码的经历吗?对我来说,三年前我作为前端开发人员的第一份工作就是做这种事情。
我们当时刚刚重写了用于创建电子学习课程的内部遗留框架。开始重写前,我们花时间研究了许多备选解决方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer 等。因为我是一名新手(那时我刚从新闻业转向 Web 开发工作),我记得所有框架看起来都非常复杂,令人害怕;反正哪个框架的工作机制我都没搞明白。
后来我们选择了 Mithril 这个框架;当我开始更深入地研究它时也开始理解很多东西了。从那以后,我花了很多时间深入研究日常工作或我自己的项目中用到的库,而我对 JavaScript 和编程工作的了解也随之快速加深。在这篇文章中我将分享一些方法,教你如何研究自己喜欢的框架或库并从中汲取知识。
Mithril 的 hyperscript 函数的源代码
我第一次阅读源代码的经历就是研究 Mithril 的 hyperscript 函数。
Web 正在快速发展,我们开发者也是如此。推荐一个可爱的 Smashing 电子邮件通讯,它每月更新两次,为设计师和开发人员提供了有用的提示。
相关链接:
Smashing 电子邮件通讯:
https://www.smashingmagazine.com/the-smashing-newsletter/
阅读源代码的好处
阅读源代码的主要好处之一是你可以学到很多知识。在我第一次看到 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 规范的一节,解释了 Object.prototype.toString 的工作原理
你正在阅读的源代码将引导你深入了解 JavaScript 规范。
我还发现了另一个好处,那就是你可以更轻松地理解官方 JavaScript 规范是如何定义语言的机制的。我第一次阅读规范是要调查 throw Error 和 throw new Error 之间的区别(剧透——它们没区别)。我调查这个是因为我注意到 Mithril 在 m 函数的实现中使用了 throw Error,我想知道用它替换 throw new Error 有什么好处。从那以后,我也知道了逻辑运算符 && 和||不一定返回布尔值,还找到了等于运算符 == 强制转换值的规则以及 Object.prototype.toString.call({}) 返回'[object Object]'的原因。
相关链接:
throw Error 和 throw new Error 之间的区别:
http://www.ecma-international.org/ecma-262/7.0/#sec-error-constructor
&& 和||不一定返回布尔值:
https://tc39.es/ecma262/#prod-LogicalORExpression
阅读源代码的技巧
有很多方法可以处理源代码。我发现最简单的方法是从你选择的库中选择一种方法并记录调用它时会发生的事情。不要每一步都记录下来,而要尝试找出其整体流程和结构。
我最近对 ReactDOM.render 就做了这种研究,因此学到了很多关于 React Fiber 及其实现背后的机制。值得庆幸的是,由于 React 是一个流行的框架,我在这个问题上找到了很多其他开发人员撰写的文章,加快了我的学习过程。
这次深入挖掘的经历还让我理解了合作调度的概念、window.requestIdleCallback 方法,并接触了链接列表的真实案例(React 将它们放入队列中来处理更新,队列就是排序过的更新的链接列表)。在做类似研究时,建议你使用库创建一个非常基本的应用程序。这样调试起来更容易,因为你不必处理由其他库引起的堆栈跟踪。
如果我没在做深入研究,就会打开我正在处理的项目中的 /node_modules 文件夹,或者转到 GitHub 存储库。当我遇到错误或有趣的功能时通常就会这样做。在 GitHub 上阅读代码时,请确保你正在阅读的是最新版本。你可以点击选择分支的按钮并选择“标记”来查看最新版本提交中的代码。库和框架永远在变化不止,你肯定不想去研究可能在下一版本中就删除的内容。
阅读源代码时,另一种不那么简单的方法我叫它“惊鸿一瞥”。我刚开始读源码的时候,一次我安装了 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 的连接函数
React-Redux 是一个用于管理 React 应用程序状态的库。在处理这类流行库时,我首先会搜索查阅有关其实现的文章。在这个案例中我找到了这篇 文章。这也是阅读源代码的另一个好处。你在研究过程中通常会阅读这类干货文章,帮助你加深自己的思考和理解。
connect 是一个 React-Redux 函数,它将 React 组件连接到应用程序的 Redux 存储。那么它是怎么做的呢?根据说明文档(https://react-redux.js.org/api/connect),它做了以下事情:“... 返回一个新的、连接的组件类,它包装你传入的组件。” 看完之后我会问下列问题:
- 函数接收输入,然后返回包装其他函数的这个输入的过程有哪些模式或概念?
- 如果我了解这类模式,该如何根据文档中给出的解释实现这一点?
通常来说,下一步工作是创建一个使用 connect 的非常基本的示例应用程序。但在本案中我选择使用我们在 Limejump(https://limejump.com/)上构建的新 React 应用程序,因为我想了解生产环境的应用程序上下文中的 connect 是什么样的。
我研究的组件如下所示:- class MarketContainer extends Component {}const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) }}export default connect(null, mapDispatchToProps)(MarketContainer);
复制代码 它是一个容器组件,包装着四个较小的连接组件。你在导出 connect 方法的文件中会看到这条说明:connect 是一个关于 connectAdvanced 的外观。于是我们还没太深入就找到了学习机会:一个观察外观设计模式的机会,链接地址为:
http://jargon.js.org/_glossary/FACADE_PATTERN.md
在文件的末尾,我们看到 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 的文章:
https://davidwalsh.name/destructuring-function-arguments
有些内容你可能早就理解得很透彻了,因此最好将注意力放在你以前从未见过或需要了解更多信息的事情上。
createConnect 本身在其函数体中不执行任何操作。它返回一个名为 connect 的函数,就是我在这里使用的函数:- export default connect(null, mapDispatchToProps)(MarketContainer)
复制代码 它需要四个参数,都是可选的,前三个参数都通过一个 match 函数来根据参数是否存在及其值类型定义它们的行为。现在,因为提供给 match 的第二个参数是导入 connect 的三个函数之一,我必须决定要遵循哪个线程。
用来包装 connect 的第一个参数(如果参数是函数)的代理函数、用于检查普通对象的 isPlainObject 实用程序、告诉你如何设置调试器以中断所有异常的警告模块都是这里的知识点。看过 match 函数后再来看 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 check 处理。
isPlainObject 的另一个有趣之处是这段代码:- while (Object.getPrototypeOf(baseProto) !== null) {baseProto = Object.getPrototypeOf(baseProto)}
复制代码 在谷歌搜索后,我找到了 StackOverflow 线程和 Redux 问题,解释该代码如何检查源自 iFrame 的对象等机制:
https://github.com/reduxjs/redux/pull/2599#issuecomment-342849867
参考资料
- 如何对框架做逆向工程:https://blog.angularindepth.com/level-up-your-reverse-engineering-skills-8f910ae10630
- 如何阅读代码:https://github.com/aredridel/how-to-read-code/blob/master/how-to-read-code.md
英文原文:
https://www.smashingmagazine.com/2019/07/javascript-knowledge-reading-source-code/#comments-javascript-knowledge-reading-source-code
活动推荐前端位于研发的应用层,永远对迭代速度要求很高,同时又跟随业务在不断发展变化,团队也在快速发展和变化,工程化面临的挑战始终很大。在这个超级 APP 割据,又有大量后继挑战者的移动互联网后半场,没有最好的工程化方案,只有最适合的工程化方案。
点击「阅读原文」或识别二维码来 QCon 上海 2019 看各个互联网公司一系列经历实战考验的前端实践。大会 8 折报名中,立减 1760 元,有任何问题欢迎联系票务小姐姐 Ring:17310043226(微信同号)
|
|