前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >怎样通过读源码提高你的 JavaScript 知识[每日前端夜话0x9D]

怎样通过读源码提高你的 JavaScript 知识[每日前端夜话0x9D]

作者头像
疯狂的技术宅
发布2019-07-30 14:57:10
9390
发布2019-07-30 14:57:10
举报
文章被收录于专栏:京程一灯

正文共:1761 字

预计阅读时间:10 分钟

作者:Carl Mungazi

翻译:疯狂的技术宅

来源:smashingmagazine

你还记得自己第一次深入挖掘常用的库或框架的源代码时的情景吗?对我而言,那一刻是我三年前作为前端开发人员的第一份工作。

我们刚刚完成了用于创建在线课程的内部遗留框架的重写。在开始重写时,我们花时间研究了许多不同的解决方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。因为我是一个萌新(我刚从新闻转向网络开发),我记得每个框架的复杂性都让人感到害怕,而且不理解框架的工作方式。

当我开始更深入地研究我们选择的 Mithril 框架时,我的能力增长了。从那以后,我对 JavaScript 的了解以及一般的编程方式得到了很大的提高,我花了很多时间深入研究每天在工作种或在自己的项目中使用的库。在本文中,我将分享一些分析库或框架的方法。

Mithril 的超文本功能的源代码

通过 Mithril 的 hyperscript 功能介绍如何去阅读源代码

阅读源代码的好处

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

之前我已经在各种文章和教程中读到过这些内容,虽然很有帮助,但是在程序的上下文中能够观察它对我来说是非常有启发性的。它还告诉我在比较不同的框架时要问哪些问题。例如我现在不是去查看 GitHub 上的 star 数量,而是会问“每个框架执行更新的方式如何影响性能和用户体验?”这样的问题。

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

这样做的好处之一是,开发人员现在可以通过 hook 到 react-reconciler 包来编写自己的自定义渲染器(https://github.com/chentsulin/awesome-react-renderer)。我最近研究过的模块捆绑包 Parcel 也有像 React 这样的 packages 文件夹。密钥模块名为 parcel-bundler,它包含负责创建捆绑包、热启动模块服务器和命令行工具的代码。

解释 Object.prototype.toString 如何工作的 JavaScript 规范部分

不久之后,你正在阅读的源代码将引导你进入 JavaScript 规范

另一个令我感到惊讶的好处是:你可以更轻松地阅读官方 JavaScript 规范,该规范定义了语言的工作方式。我第一次阅读规范的时候是在分析 throw Errorthrow new Error 之间的区别。之所以要分析这个,是因为我注意到 Mithril 在其 m 函数的实现中使用了 throw Error,我想知道这样是不是比 throw new Error 更好。从那以后,我也学会了逻辑运算符 &&|| 不一定返回布尔值,找到了控制 == 等式运算符如何强制赋值的规则(http ://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparison)和Object.prototype.toString.call({})返回 '[object Object]' 的原因(http://www.ecma- international.org/ecma-262/#sec-object.prototype.tostring) 。

阅读源代码的技巧

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

我最近用这种方法阅读了 ReactDOM.render 的代码 ,因此学到了很多关于 React Fibre 及其实现背后的一些原理。值得庆幸的是,由于 React 是一个流行的框架,我在同一个问题上看到过很多其他开发人员撰写的文章,这也加快了这个过程。

这深入探讨并向我介绍了co-operative scheduling(https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API),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 目录中的相关文件里。

ReactDOM.render function 的源代码

像其他程序一样进行调试。形成一个假设,然后进行测试

案例研究:Redux的 Connect 函数

React-Redux 是一个用于管理 React 应用状态的库。在处理诸如此类的库时,我首先会搜索已经编写过有关其实现的文章。在这个案例研究中,我遇到了这篇文章(https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation)。这是阅读源代码的另一个好处。研究阶段通常会引导你去阅读这样的信息性文章,通常这些文章只会改善你自己的思路和理解。

connect 是一个 React-Redux 函数,它将 React 组件连接到应用程序的 Redux 存储。怎么样?好吧,根据官方文档(https://react-redux.js.org/api/connect)的说明,它执行以下操作:

“…返回一个新的连接组件类,它将会包装你传入的组件。”

看完之后,我会问下列问题:

  • 我知不知道函数接受输入的那些模式或概念,然后返回包含其他功能的相同输入?
  • 如果我知道此类模式,又将如何根据文档中给出的解释实现此模式?

通常,下一步是创建一个使用 connect 的非常基本的示例程序。但是在这种情况下,我选择使用我们在 Limejump (https://limejump.com/)上构建的新 React 程序,因为我想在程序的上下文中理解 connect,最终再进入生产环境。

我关注的组件看起来像这样:

代码语言:javascript
复制
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 的函数的调用。它的参数是一堆默认值,它们已被解构,如下所示:

代码语言:javascript
复制
export function createConnect({
 connectHOC = connectAdvanced,
 mapStateToPropsFactories = defaultMapStateToPropsFactories,
 mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
 mergePropsFactories = defaultMergePropsFactories,
 selectorFactory = defaultSelectorFactory
} = {})

同样,我们遇到了另一个学习的点:导出调用函数解构默认函数参数。解构部分是一个学习的点,因为代码编写如下:

代码语言:javascript
复制
export function createConnect({
 connectHOC = connectAdvanced,
 mapStateToPropsFactories = defaultMapStateToPropsFactories,
 mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
 mergePropsFactories = defaultMergePropsFactories,
 selectorFactory = defaultSelectorFactory
})

它会导致这个错误 Uncaught TypeError:无法解析 undefinednull 的属性 connectHOC。这是因为该函数没有默认参数可以依赖。

注意有关此内容的更多信息,请阅读 David Walsh 的文章(https://davidwalsh.name/destructuring-function-arguments)。根据你对语言的了解,一些学习的点可能看起来微不足道,所以最好专注于你以前从未见到过的或需要了解更多信息的内容。

createConnect 本身在其函数体中没有任何功能。它返回一个名为 connect 的函数,我在代码里使用的函数:

代码语言:javascript
复制
export default connect(null, mapDispatchToProps)(MarketContainer)

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

在这里学习的重点是:如果这些参数是函数,用于将第一个参数包装为 connect 的代理函数,isPlainObject 用于检查普通对象或 warning 模块,它揭示了如何将调试器设置为中断所有异常(https://developers.google.com/web/tools/chrome-devtools/javascript/breakpoints#exceptions)。在匹配函数之后,我们来到 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 的另一个吸引人的地方是这段代码:

代码语言:javascript
复制
while (Object.getPrototypeOf(baseProto) !== null) {
    baseProto = Object.getPrototypeOf(baseProto)
}

谷歌搜索引导我找到这个 StackOverflow 帖子(https://stackoverflow.com/questions/51722354/the-implementation-of-isplainobject-function-in-redux/51726564#51726564)和 Redux issue(https://github.com/reduxjs/redux/pull/2599#issuecomment-342849867),它们解释了该代码如何进行处理的案例,例如检查源自 iFrame 的对象。

有用的链接
  • “如何逆向工程框架”,Max Koretskyi,Medium (https://blog.angularindepth.com/level-up-your-reverse-engineering-skills-8f910ae10630)
  • “如何阅读代码”,Aria Stewart,GitHub (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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端先锋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 阅读源代码的好处
  • 阅读源代码的技巧
  • 案例研究:Redux的 Connect 函数
  • 总结
    • 有用的链接
    相关产品与服务
    命令行工具
    腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档