通过解析源代码来提高您的JavaScript知识

当您开始编程生涯时,挖掘开放库和框架的源代码似乎有些可怕。 在本文中,Karl Mungazi分享了他如何克服恐惧的经验,并开始使用源代码获取知识并发展技能。 他还使用Redux展示了他如何“解析”库。

您还记得第一次沉浸在经常使用的库或框架的代码中吗? 这是我三年前从事前端开发人员的第一份工作。

我们只是重写了一个过时的专有框架,该框架曾用于创建交互式培训课程。 在重写工作的开始,我们就研究了一些交钥匙解决方案,包括Mithril,Inferno,Angular,React,Aurelia,Vue和Polymer。 由于我还是一个年轻的Padawan(刚从新闻学转向Web开发),所以我非常害怕每个框架的复杂性以及对它们如何工作的了解。

当我开始仔细探索Mithril框架时,就开始有了理解。 从那时起,由于我每天在工作中和自己的项目中使用的库内部挖掘工作所花费的时间,我的JavaScript(以及一般编程)知识得到了极大的增强。 在本文中,我将告诉您如何使用自己喜欢的库作为教程

图片
我开始从秘银读取带有超脚本功能的源代码

解析源代码的优点


解析源代码的主要优点之一是,您可以学到很多东西。 当我开始解析Mithril代码时,我对虚拟DOM的概念非常了解。 当我完成时,我已经知道虚拟DOM是一种涉及创建描述用户界面的对象树的技术。 然后可以使用诸如document.createElement之类的DOM API将这棵树转换为DOM元素。 要进行更新,将创建一个新树,该树描述接口的未来状态,然后与该树的先前版本进行比较。

我在许多文章和手册中都读过有关此内容的文章,但最有启发性的是在处理我们的应用程序时要观察所有这些内容。 在比较框架时,我还学会了提出正确的问题。 例如,您可以问一个问题“而不是比较等级”,“这个框架如何应对变化,如何影响最终用户的性能和便利性?”

另一个优点是对良好的应用程序体系结构有了了解。 尽管大多数开源项目通常在结构上与其存储库大致相似,但它们仍然存在差异。 Mithril的结构非常平坦,如果您精通其API,则可以对render,router和request文件夹中的代码进行相当实际的假设。 另一方面,React的结构反映了它的新架构。 开发人员将负责更新UI的模块(react-reconciler)与负责呈现DOM元素的模块(react-dom)分开。

这种分离对开发人员的好处之一是,他们可以使用react-reconciler中的钩子编写自己的渲染器 。 我最近研究的模块构建器Parcel也有一个package文件夹,就像React一样。 关键模块称为包裹捆绑程序,它包含负责创建程序集,模块更新服务器(热模块服务器)和命令行工具的操作的代码。

图片
解析源代码很快会使您阅读JavaScript规范。

令我感到惊讶的另一个好处是,它使您可以更轻松地阅读正式的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方法以及链接列表的实时示例 (通过将更新发送到队列来进行响应处理,这是更新的优先级链接列表)。 在此过程中,最好使用该库创建一个简单的应用程序。 由于您不必处理其他库的堆栈跟踪,因此这使调试更加容易。

如果我不做详细的评论,那么我将在我正在处理的项目中打开node_modules文件夹,或查看GitHub。 当我遇到错误或有趣的功能时,我总是这样做。 在GitHub上阅读代码时,请确保这是最新版本。 通过单击更改分支的按钮并选择“标签”,可以看到最新版本的代码。 库和框架中的更改正在进行中,因此您不太可能想解析下一个版本中可能没有的内容。

学习源代码的一个更肤浅的版本是我所谓的“快速外观”。 我以某种方式安装了express.js,打开了node_modules文件夹并检查了依赖项。 如果README没有给我令人满意的解释,请阅读源代码。 这使我产生了有趣的发现:

  • Express使用两个模块来合并对象,并且这些模块的操作非常不同。 merge-descriptors仅添加在源对象中找到的属性,并且还添加不可枚举的属性,而utils-merge遍历对象及其整个原型链的枚举属性。 merge-descriptor使用Object.getOwnPropertyNames()和Object.getOwnPropertyDescriptor(),而utils-merge用于..in;。
  • setprototypeof模块提供了一个跨平台选项,用于指定所创建(实例化)对象的原型。
  • escape-html是一个78行的字符串转义模块,之后可以将内容插入HTML;

尽管这些发现很可能立即没有用,但是对库或框架的依赖项的一般理解非常有帮助。

在前端调试代码时,调试浏览器工具是您最好的朋友。 除其他外,它们使您可以随时停止程序并同时检查其状态,跳过该功能,进入内部或退出该程序。 在缩小的代码中,这是不可能的-这就是为什么我解压缩此代码并将其放在node_modules文件夹中的相应文件中的原因。

图片
使用调试器作为有用的应用程序。 作一个假设,然后进行测试。

案例研究:Redux中的connect函数


React-Redux是一个用于管理React应用程序状态的库。 当我使用像这样的流行图书馆时,我首先要查找有关其使用的文章。 在准备此示例时,我回顾了本文 。 这是学习源代码的另一个优点-它使您获得诸如此类的内容丰富的文章,从而改善您的思维和理解。

Connect是一个react-redux函数,该函数链接react组件和应用程序的redux存储。 怎么了 根据文档,她执行以下操作:
“ ...返回一个新的相关组件类,它是传递给它的组件的包装。”
阅读此内容后,我会提出以下问题:

  • 我是否知道模式或概念,其中函数返回带有附加功能的输入参数?
  • 如果是这样,如何根据文档中的描述使用它?

通常,下一步是使用connect函数创建原始应用程序。 尽管如此,在这种情况下,我还是在React上使用了一个新的应用程序,因为我们一直在研究该应用程序,因为我想了解应用程序上下文中的连接,这种连接很有可能很快就会投入生产。

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

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); 

这是一个容器组件,用作四个较小的相关组件的包装。 您在连接导出文件中发现的第一件事就是注释“连接是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:无法解构属性'connectHOC'的'undefined'或'null'。 因为该函数没有默认参数值,所以会发生这种情况。

注意:要更好地理解参数的重组,可以阅读David Walsh的文章 根据您对语言的了解,有些观点看似微不足道-然后您可以专注于您不熟悉的那些观点。

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)返回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) } 

在Google上找到答案后,我想到了StackOverflow上的该线程以及GitHub的Redux上的注释 ,它解释了此代码如何处理例如从iFrame传输对象的情况。

--

首先决定翻译这篇文章。 我很感谢您的澄清,建议和建议

Source: https://habr.com/ru/post/zh-CN460181/


All Articles