系列文章React系列(一)——2013起源OSCON——ReactArchitecturebyvjeuxReact系列(二)——React基本语法实现思路React系列(3)--Jsx、合成事件和RefsReact系列(四)---virtualdomdiff算法实现分析React系列(五)---从Mixin到HOCReact系列(六)---从HOC到HOOKS2004-人物时代拼接回到Facebook的早期,当马克扎克伯格还在他的宿舍里时,你用PHP构建网站的方式是使用字符串连接。事实证明,这是一种非常好的建站方式。无论你是后端、前端,甚至是完全没有编程经验,都可以搭建一个大型网站。这种编程方法唯一的问题是它不安全。如果您使用这个确切的代码,攻击者可以执行任意Javascript。这对Facebook尤其不利,因为这段代码将在用户上下文中执行。通过这种方式,您基本上可以接管用户帐户,如果您什么都不做,您就很容易受到攻击。更糟糕的是,对于大多数输入,它实际上会为处理该功能的开发人员很好地呈现。因此,他/她几乎没有动力添加适当的转义,而且非常糟糕的功能,为了安全起见,您需要将每个调用站点包含在数百名工程师编写的数百万行代码中。犯了一个错误?您的帐户可能会被接管为了避免这种不可能的情况,一个想法是无论发生什么都避免一切。不幸的是,它不能很好地工作,如果你对字符串进行两次转义,它将显示控制字符。如果您不小心转义了标记,它会向用户显示html!2010-XHP我们在Facebook提出的解决方案是扩展PHP的语法以允许开发人员编写标记。在这种情况下
不再在字符串中现在,标记中的所有内容都使用不同的语法编写,因此我们知道在生成HTML时不要对它进行转义。其他一切都被视为不受信任的字符串并自动转义。我们可以在保持安全性的同时保持开发的便利性一旦引入了XHP,人们很快就意识到他们可以创建自定义标签。事实证明,它们允许您通过组合大量这些标签轻松构建非常大的应用程序。这是语义Web和Web组件概念的实现。我们开始用Javascript做越来越多的事情来避免客户端和服务器之间的延迟。我们尝试了很多技术,例如拥有跨浏览器的DOM库和数据绑定方法,但没有一个真正对我们有用鉴于世界的现状,前端工程师JordanWake向他的经理提出了以下想法:将XHP移植到Javascript。为了证明这个概念,他不知何故开始练习了6个月。当我第一次听说这个项目时,我想,绝对不可能,但在一个难得的机会中,它会取得巨大的成功。当我终于开始使用它时,我立即开始宣传2013-JSX,第一个任务是编写一个支持这种奇怪的XML语法的Javascript扩展。我们在Facebook实施Javascript转换已有一段时间了。在此示例中,我使用另一种方法为下一个JavaScript标准ES6编写函数。JSX的实现用了大概一周的时间,这不是React最重要的部分,复现PHP的更新机制比较有挑战性,其实很简单。PHP——有什么变化吗?重新渲染一切。我们能让它足够快吗?每当有任何变化时,您都会转到一个新页面并获得一个全新的页面。从开发人员的角度来看,这使得编写应用程序变得非常容易,因为您不必担心更改,也不必在UI更改时确保所有内容都同步但是,每个人都会问的问题...它会很慢React——它不仅足够快,而且通常比以前的实现更快。经过2年的生产使用,我可以自信地说它比我们在其中替换的大多数代码要快得多。在本次演讲的其余部分,我将解释使AkimDemel成为可能的重大优化——“在成为好人之前,你需要先做对”我在学校的老师曾经说过,在成为好人之前,你需要先做对,是的,那么就可以做好。他的意思是,如果你试图构建性能良好的东西,那么如果你首先构建一个简单但高效的实现并迭代性能,而不是从一开始就尝试以最佳方式构建它,那么你成功的几率要高得多太天真了,所以让我们尝试应用他的建议。让我们先实现最简单的版本。每当有任何变化时,我们都会构建一个全新的DOM并替换旧的DOMDOM是有状态的,这是一个正在运行的案例,但有很多边缘案例。如果你销毁DOM,你将失去当前聚焦的元素和光标,文本选择和滚动位置也是如此。这实际上意味着DOM节点实际上包含状态。第一次尝试是尝试恢复那些状态,我们记住焦点输入,新元素被聚焦,光标和滚动位置相同。不幸的是,如果您在Mac上滚动,这还不够,您会有惯性。结果是没有用于读写滚动惯性的JavascriptApl。使用iframe,如果它来自另一个iframe,则更糟,安全策略实际上不允许您查看其中的内容,因此您无法恢复它。DOM不仅是有状态的,还包含隐藏状态ReuseNodes为了解决这个问题,思路不是销毁DOM并重新创建一个新的,而是重用两次渲染之间保持不变的DOM节点。我们将匹配节点,而不是删除以前的DOM树并用新的替换它,如果它们没有改变,则丢弃新的DOM树并保留当前呈现在屏幕上的旧DOM树。只要我们能匹配到节点,我们就会重复这个过程。但在某些时候,我们会看到一个以前不存在的新节点。在这个例子中,我们将把新的dom树移动到旧的(当前在屏幕上呈现的)dom树AdonisSMU——“我倾向于认为React是DOM的版本控制”我们现在对React的工作原理有了一个了解有了一个大致的想法,但没有具体的计划这是我从帽子里拿出模拟卡的那一刻回到编程的黑暗时代,如果你想让别人尝试你的代码,你会创建一个zip并将其发送给他。如果您更改任何内容,您将发送一个新的zip文件版本控制出来了,它的工作方式是,它拍摄代码快照并生成一个突变列表,如“删除那5行”、“添加3行”,"replacethisword"...使用diff算法,这正是React所做的,但使用DOM作为输入而不是文本文件OptimalDiff--O(n^3)因此,作为一名优秀的工程师,我们研究树diff算法在O(n)中找到最优解假设我们有一个包含10,000个DOM节点的页面。它很大,但并非不可想象。为了得到一个数量级,我们假设我们可以在一个CPU周期内执行一个操作(这不会发生),并且让一台1GHz的机器花17分钟来计算差异!我们不能用那个。..但是,不要害怕,我们知道我们仍处于需要正确的阶段。所以我们研究它是如何工作的(1)比较新树中的每个节点,(2)将再次匹配旧树的每个节点(3)匹配操作对整个子树进行操作。这里我们得到三个嵌套循环。想一想,在Web应用程序中,我们很少需要将元素移动到页面的任何其他位置。唯一想到的例子是拖放,但这不是很常见。唯一一次移动元素是在子元素之间你经常在列表中添加/删除/移动元素子元素是嵌套的,所以我们可以通过子元素来计算差异。我们从它的根开始,将它与其他根匹配,并为所有匹配的孩子这样做。我们从一个非常大的O(n到很多O(m,我们试图变得更好更快。事实证明我们不能直接使用Levenstein距离算法。理解原因的最好方法是通过一个小的示例。让我们看看第一个渲染有三个输入,下一个渲染只有两个。问题是如何匹配它们?直观的反应是将前两个匹配在一起并删除第三个,但是,我们也可以删除第一个和将最后两个匹配在一起的一个不太明显但仍然完全有效的解决方案是删除所有以前的元素并创建两个新的。所以在这一点上我们没有足够的信息来正确地进行匹配因为我们希望能够处理对于上述所有用例,人们认为不仅要使用标签名称,还要使用属性。如果它们前后相等,那么我们进行匹配结果证明这对值属性不起作用。如果您尝试输入“oscon”,两者会有所不同另一个更多的承诺输入焦点的ing属性是id属性。在表单的上下文中,这通常包含输入对应的模型的ID。现在,我们能够成功匹配两个列表!(您是否注意到与另一个匹配相比,它与三个示例相同?)但是,如果您通过AJAX提交表单而不是让浏览器这样做,则id属性不太可能被放入DOM。React引入了key属性。它唯一的工作是帮助diff匹配子元素嵌套的算法事实证明,我们可以通过哈希表在O(n)中使用比O(n)快得多的键进行匹配所以,如果我们将所有部分相加O(m),我们得到了O(n)的总复杂度。没有比这更好的复杂性了。让美好开始吧!在这一点上,我们有了一个正确的解决方案,我们现在可以开始实施所有很酷的优化以使其更快如果你对你的JS应用程序做过任何优化,你可能听说过DOM很慢。StackExchange上的Rafal就是一个很好的例子。如果你枚举一个空div的所有属性,你会看到很多!之所以有这么多属性,是因为浏览器渲染管道中的许多步骤都使用了DOM节点。浏览器首先查看CSS规则并找到与该节点匹配的规则,并在此过程中存储各种元数据以使其更快。例如,它维护一个ids到dom节点的映射。然后它采用这些样式并计算布局,其中包括屏幕中的位置和定位。同样,大量的元数据。它将尽可能避免重新计算布局,并缓存以前计算的值。然后,在某些时候,您实际上是在CPU或GPU上进行缓冲。所有这些步骤都需要中介表示并使用内存和CPU。浏览器在优化虚拟DOM的整个管道方面做得非常好,但如果你考虑一下React中发生的事情,我们只会在diff算法中使用那些DOM节点。所以我们可以使用一个只包含标签名称和属性的更轻量级的JavaScript对象。我们称之为虚拟DOM。diff算法生成DOM突变列表,与版本控制输出我们可以应用于真实DOM的文本突变的方式相同。然后我们让浏览器做所有的优化管道。我们最大限度地减少了昂贵但必需的DOM突变的数量。可汗学院的批处理BenAlpert通过批处理操作解决了这个问题。要告诉React发生了某些变化,您可以在元素上调用setState。React只是将元素标记为脏的,但不会立即计算任何内容。如果您在同一个节点上多次调用setState,一旦初始事件完全传播,它将执行相同的操作……我们可以从上到下重新渲染元素。这非常重要,因为它确保我们只渲染一次元素现在所有元素都已重新渲染到虚拟DOM,我们将它提供给diff算法,该算法会改变输出DOM。在此过程中我们不需要从DOM中读取任何内容。React只是在写SubtreeRe-rendering一开始我说心智模型是“当有任何变化时重新渲染一切”。这在实践中并不完全正确。我们只重新渲染标有setState的元素的子树。当您开始将React集成到您的应用程序中时,通常的模式是树中只有很少的状态,因此setState非常便宜,因为当更多的应用程序状态趋于上升时,它们只会重新渲染一小部分UI修剪转换时,这意味着当有任何变化时,您将重新渲染应用程序的大部分内容。为了在性能方面减轻这种情况,您可以实现shouldComponentUpdate,它采用前一个和下一个状态/道具来表示:“你知道吗,什么都没有改变,让我们跳过重新渲染这个子树”这使得你可以修剪大部分树并恢复性能shouldComponentUpdate?我们在开源版本中引入了shouldComponentUpdate,但我们不太清楚如何正确实现它。问题是在JavaScript中你经常使用对象来保存状态并直接改变它。这意味着状态的前一个版本和下一个版本是对该对象的相同引用。因此,当您尝试将上一个版本与下一个版本进行比较时,即使发生了一些变化,它也会说之前和之后。《纽约时报》的大卫·诺伦(DavidNolen)提出了一个不错的解决方案。在ClojureScript中,所有大多数值都是不可变的,这意味着当你更新一个时,你会得到一个新对象,而旧的保持不变。这适用于shouldComponentUpdate。他在React之上用ClojureScript编写了一个名为Om的库,该库使用不可变数据结构来默认实现shouldComponentUpdate。不幸的是,使用不可变数据结构需要思想上的巨大飞跃,而且大家还没有准备好。所以现在和可预见的未来React必须在没有它们的情况下工作,所以shouldComponentUpdate不能默认实现。相反,我们只是发布了一个性能工具。你在你的应用程序中玩了一会儿,每次你重新渲染一个组件,如果diff没有输出任何DOM变化,它会记住渲染花了多长时间。最后,您将得到一个漂亮的表格,告诉您哪些组件将从shouldComponentUpdate中获益最多!通过这种方式,您可以将其放在几个关键位置并获得最大的性能优势。结论在这次演讲中,我们介绍了React正在做的四项优化:diff算法、虚拟DOM、批处理和缩小。我希望它能阐明它们存在的原因以及它们的工作原理。React用于构建我们的桌面网站、移动网站和Instagram网站。它在Facebook非常成功,基本上所有新的前端产品都是用React编写的。这不是我们仅在内部工具或小功能中使用的项目,这是每个月有数亿人使用的Facebook页面所使用的!由于我们是在一个开源会议上,我想在结束时回顾一下我们在2010年开源了XHP,但我们在这方面做得很糟糕,而且我们在4年内只写了一篇博文。我们没有去开会解释它,写文档……但是,在Facebook内部,我们喜欢它,并且到处都在使用它。当我们去年开源React时,它变得更加困难,因为我们必须解释XHP的好处以及我们必须做的所有疯狂优化才能让它在客户端上工作。我们经常谈论开源的好处。这是一个很好的提醒,不开源你的核心技术会使其他项目更难开源ReferenceOSCON-ReactArchitecture