用户友好的 JavaScript 前端的一个长期讽刺之处在于,构建它们通常需要艰难地处理 DOM(文档对象模型),而 DOM 并非以对开发人员友好而闻名。但现在,由于 Facebook 决定开源其用于构建用户界面组件的 React 库,开发人员有了一种避免直接与 DOM 交互的方法。
React 本质上设法抽象出了 DOM,从而简化了编程模型,同时也出人意料地提高了性能。这两项进步的关键在于,由标准 JavaScript 对象构建的组件充当了 React 内部框架的基本构建块,从而大大简化了可组合性。一旦开发人员设法习惯以这种方式构建前端,他们通常会发现他们可以更容易地了解正在发生的事情,同时在如何构建和显示数据方面也享有更大的灵活性。
所有这些都让我们想知道 React 最初是如何创建的,以及它最重要的指导原则是什么。对我们来说幸运的是,Pete Hunt 当时是 Instagram 的工程经理,也是 Facebook React 核心团队中较为杰出的成员之一,他愿意阐明 React 的起源。Hunt 后来与人共同创立了 Smyte,这是一家位于旧金山的初创公司,专注于市场和社交网络的安全性。
Paul O'Shannessy 也参与讲述了这个故事,他是 Facebook 最早全职致力于 React 的工程师之一。他从 Mozilla 加入,之前曾在 Mozilla 从事 Firefox 前端方面的工作。
推动讨论前进的探究性问题由 Dave Smith 和 Terry Coatta 负责提出。Smith 是 HireVue 的工程总监,HireVue 是一家位于盐湖城的公司,专注于团队建设软件,在那里他有机会广泛使用 Angular 和 React。Coatta 是 Marine Learning Systems 的首席技术官,他在那里构建一个面向海事行业的学习管理系统。他也是 acmqueue 编辑委员会的成员。
DAVE SMITH 究竟是什么促成了 React 的创建?
PETE HUNT 在 Facebook 的所有 Web 应用程序中,最复杂的一个是我们用来创建广告和管理广告帐户的应用程序。最大的问题之一是保持 UI 与业务逻辑和应用程序状态同步。传统上,我们通过使用集中式事件总线手动操作 DOM 来实现这一点,无论是将事件放入队列还是让侦听器侦听事件,然后让他们执行自己的操作。
事实证明这非常麻烦,因此几年前我们实施了当时我们认为是最先进的 DOM 监控系统,称为 Bolt。它有点像带有 observable 的 Backbone,您可以在其中注册计算属性,这些属性最终会被刷新到 DOM。但是后来我们发现这也很难管理,因为您永远无法确定您的属性何时会被更新——这意味着如果您更改了一个值,您无法确定它是否会导致单次更新、级联更新或根本不更新。弄清楚这些更新实际上可能何时发生也被证明是一个非常困难的问题。
最初 React 背后的整个想法只是找到某种方法来连接这些更改处理程序,以便工程师们能够真正理解它们。Bolt 的情况并非如此,结果我们最终得到了许多无人能解决的错误。因此,开始研究补救方法的工程师们疯狂工作了几个月,并提出了这个看起来很奇怪的东西,没有人认为它有任何机会奏效。如果您对 React 有些许了解,您就已经知道,每当您的底层数据模型发生更改时,它基本上都会重新渲染整个应用程序,然后进行差异比较,以查看渲染结果中实际发生了什么变化。然后,只有页面的那些部分会被更新。
这里有些人对性能有些担忧,因此早期版本的 React 最终经历了严格的工程测试,在测试中,它与几乎所有可以抛给它的东西进行了基准测试。当然,作为其中的一部分,我们研究了这种新的编程模型在 Bolt 模型和我们旧的事件模型面前的表现。React 最终让很多人感到惊讶——事实上,它几乎立即作为我们 News Feed 上“点赞和评论”界面的一部分发布。那是 React 的第一次重大考验,那是几年前的事了。
然后我们在 Instagram.com 上尝试了它,这就是我进入这个领域的原因,因为我是 Instagram 上负责使用 React 构建一些东西的人。我们对它非常满意,因为它证明它能够运行我们的整个页面,而不仅仅是一个或两个小部件。这给了我们一个相当好的迹象,表明它实际上会奏效。从那时起,它基本上已经成为人们在 Facebook 上编写 JavaScript 的默认方式。
TERRY COATTA 我听说 React 采用了不同的数据绑定方法。React 在这方面有何不同?
PH 我在 Web 上下文中考虑数据绑定的方式是,您从某种可观察的数据结构一直到 DOM 节点。挑战在于,当您实现某种可观察系统时,您有义务在应用程序接触数据模型的任何地方观察此数据结构。
例如,如果您使用像 Ember 这样的东西,您所做的一切都将使用 getter 和 setter,这意味着您将需要始终意识到整个应用程序中的这种可观察抽象。因此,如果您想计算一个值,您将不会只使用一个函数;您将使用一个计算属性编号,这是一个 Ember 的特定领域的东西。
我认为 Angular 在这方面做得更好,因为它使用脏检查,这意味着您可以实际利用常规 JavaScript 对象。但是,Angular 的问题在于,它使应用程序的组合变得困难。这是因为,您必须通过作用域传递所有内容才能观察到这些更改,而不是使用常规函数或对象来构建抽象(就像您使用 JavaScript 那样)。然后,您最终得到的数据绑定会以不一定那么清晰或明显的方式将程序的各个部分耦合在一起。
例如,假设我们正在寻找对您的热门好友列表进行排序——这是我们在这里一直做的事情。为了让我们使用可观察系统做到这一点,我们必须为您列出的每一千个好友设置一个观察者,即使我们真正想做的只是渲染前十个。因此,您可以想象,维护整个表示将占用大量的内存。
显然,有一些方法可以解决这个问题,但是人们通常只是完全打破数据绑定抽象,以便他们可以手动进行。现在,我通常不喜欢说某些东西不会扩展,但很明显,这会带来一些扩展问题。很明显,您的应用程序越大,您就越会遇到这种边缘情况。
TC 我完全同意 Angular 的情况,因为我也发现那里的组合非常棘手,原因正是您提到的——也就是说,您最终会使应用程序的不同部分通过双向数据绑定静默耦合。但是我看到 React 也有数据绑定,所以我很好奇您是如何在存在这种耦合的情况下设法提供更好的可组合性的。
PH 让我在这里稍微放大一下来观察,在非常高的层面上,React 本质上将您的用户代码视为一个黑匣子,同时接受您告诉它接受的任何数据。这基本上允许任何结构。它可以像 Backbone 一样。它可以是纯 JSON。它可以是任何你想要的。然后你的代码将继续执行它应该做的事情,并得到 JavaScript 全部功能的后盾。
但是,最后,它将返回一个值,我们称之为虚拟 DOM 数据结构。这基本上只是 JavaScript 对象的花哨句柄,它告诉您它们是什么类型的元素以及它们的属性是什么。因此,如果您将数据绑定视为使 UI 与底层模型保持同步的一种方式,您可以使用 React 通过发出信号来完成此操作,“嘿,我的数据模型中的某些东西可能刚刚更改了。” 这将提示 React 调用黑匣子用户代码,而黑匣子用户代码反过来将发出新的虚拟 DOM 表示。然后,在保留先前的表示之后,React 将查看新版本和旧版本,并对两者进行差异比较。基于此,它可能会得出结论,“哦,我们需要在这个节点构建一个 className 属性。”
这种方法的优点是它不涉及对底层数据模型的实际跟踪。您无需预先支付数据绑定成本。大多数需要您跟踪数据模型内的更改,然后使 UI 与之保持同步的系统都面临着由底层数据模型的大小驱动的数据绑定成本。另一方面,React 支付的成本仅与实际渲染的内容有关。
TC 如果我理解正确,您的意思是 React 在某种意义上是一个高度函数式的环境,它接受一些任意输入,渲染一个输出,然后计算两者之间的差异,以确定它应该在屏幕上显示什么。
PH 完全正确。我喜欢将此描述为“引用透明的 UI”。也就是说,您的用户界面通常是某些输入集合的纯函数,并且对于给定的数据输入,每次都发出相同类型的虚拟 DOM 结构。
TC 因此,在 Angular 中给我们带来麻烦的数据绑定在这里以相反的方向运行,因为它们反映了绑定到底层模型对象或作用域变量的 DOM 元素的值。那里的任何更改实际上几乎同时在整个代码中的多个位置变得可见,这意味着可组合性问题会浮出水面,因为代码中不同的位置几乎同时意识到从 UI 反向传播的更改。
PH 另一个问题是,您可能对同一数据源有多个绑定。那么哪段代码将被视为确定值应该是什么的权威来源?
这就是为什么在 React 中,我们强调单向数据流。正如我之前所说,我们模型中的数据首先进入这个应用程序黑匣子,黑匣子反过来发出虚拟 DOM 表示。然后我们用简单的浏览器事件来闭环。我们将捕获一个 KeyUp 事件并命令,“根据该 KeyUp 事件更新此位置的数据模型。” 我们还对系统进行了架构设计,以鼓励您在应用程序中保持尽可能少的易变状态。事实上,由于 React 是这样一个函数式系统,我们只是在每次新渲染时按需重新计算该值,而不是计算一个值然后将其存储在某处。
问题是人们有时想要一个大型表单,其中包含大约 20,000 个字段,然后绑定到一些简单的键和数据对象。好消息是,我们实际上很容易在简单事件循环之上构建一个抽象,该抽象基本上捕获所有可能更新此字段值的事件,然后设置一个自动处理程序以将值从数据模型传递到表单字段。表单和数据模型基本上同时更新。这意味着您最终会得到一个看起来很像数据绑定的系统,但如果您剥开它,您会发现它实际上只是事件循环之上的简单语法糖。
TC 我观察到 React 的一件事是,它似乎是人们所说的相当有主见的。也就是说,有一种使用 React 做事的特定方式。这与 Angular 形成对比,我会说 Angular 没有主见,因为它通常允许您以几种不同的方式做事。您认为这是一个准确的描述吗?
PH 这取决于情况。在某些地方,React 非常有主见,而在另一些地方,它却相当没有主见。例如,React 在您如何表达视图逻辑方面没有主见,因为它将您的 UI 视为一个黑匣子,并且只查看输出。但它在某种意义上是有主见的,因为我们真的鼓励幂等函数、最少的易变状态和非常清晰的状态转换。
我用 React 构建了很多东西,我的团队也用它运行了很多东西。从所有这些经验来看,我可以告诉您,每当您在 React 应用程序中遇到错误时,十分之九的情况下,您都会发现这是因为您在其中有太多状态。我们尝试将尽可能多的易变状态从应用程序中推出,以达到我喜欢称之为完全规范化的应用程序状态。在这方面,是的,我们非常有主见,但这仅仅是因为如果您有太多易变状态,许多 React 抽象就无法很好地工作。
我认为 Angular 在这方面实际上不太有主见,但它肯定对您需要如何组合应用程序有自己的看法。它非常类似于模型-视图-呈现器类型的架构。如果您想创建合理的窗口小部件,您将必须使用指令,而指令是非常有主见的。
TC 我立即注意到的关于 React 的另一件事是,它非常面向组件。转向这个方向的原因是什么?
PH 我们实际上认为组件与 JavaScript 函数非常相似。事实上,函数和组件之间唯一的区别在于组件需要了解一些关于它们自身的生命周期钩子,因为重要的是它们要知道它们何时被添加到 DOM 或从 DOM 中删除,以及它们何时能够获得自己的 DOM 节点。组件是我们构建自己的内部框架的基础构建块。现在,开源世界的许多其他人也在它之上构建。
我们强调它,是因为它是可组合的,这是 React 组件与 Angular 指令和像 partial 和模板这样的 Web 组件最不同的地方。这种对可组合性的关注——我将其视为在多层上构建嵌套组件的能力——不仅使人们更容易了解实际发生的事情,而且还使您在如何构建和显示数据方面具有灵活性,同时还允许您以更可扩展和明智的方式覆盖行为和传递数据。
PAUL O'SHANNESSY 这也与我们如何在服务器上构建应用程序有很大关系,在那里我们有一个核心组件库,任何产品团队都可以使用它作为构建自己组件的基础。使用组件的想法实际上只是我们在 PHP 和 XHP 中构建事物的核心方式的自然扩展,其想法只是将越来越大的组件由较小的组件组成。
PH 这些产品团队通常由使用各种不同语言的通才组成,也就是说,他们不一定是 JavaScript 或 CSS(层叠样式表)方面的专家。我们强烈建议普通产品工程师不要编写太多 CSS。相反,我们建议他们从货架上取下这些组件,将它们放入他们正在做的任何事情中,然后稍微调整一下布局。这对我们来说效果非常好。
PO 这样,我们最终编写了相当不错的代码,因为较少有人跑到疯狂的土地上编写 CSS。基本上,这只是为我们在顶层控制所有这些提供了一种方法。
尽管 React 在很多方面简化了用户界面的创建,但它也给新接触该环境的开发人员带来了学习曲线。特别是,那些过去主要从事单体系统工作的人可能会发现采用更面向组件的思维方式具有挑战性。他们很快也会发现,React 对状态应该如何处理是有主见的,这可能会导致一些惨痛的教训和严厉的提醒,只要人们偏离轨道。
TC React 有很多吸引人的地方,但在深入研究之前,人们应该注意哪些尖锐的边缘?哪些类型的错误可能会使他们的生活更加痛苦?
PH 大部分痛点几乎肯定与状态有关。状态无论如何都是构建应用程序中最难的部分,而 React 只是使这一点变得非常明确。如果您不以正确的方式考虑状态,React 将在流程的早期为您突出显示这些错误。
TC 给我一个关于人们可能以错误方式考虑状态的具体例子。
PH 好的,我正在查看一个今天早些时候启动的由 React 驱动的网站。看起来该页面有四个主要组件:侧边导航、搜索结果列表、搜索栏以及包含搜索栏和搜索结果列表的内容区域。
当您在搜索栏中键入内容时,它会过滤结果以显示在结果网格中。如果我问您过滤器状态应该位于何处,您很可能会认为,“好吧,搜索结果列表正在进行过滤,因此状态可能应该位于那里。” 这就是直觉上合理的。
但实际上,状态应该位于搜索框和搜索结果列表之间的共同祖先中,有点像视图控制器。这是因为搜索框具有搜索过滤器的状态以及搜索结果。不过,搜索结果列表也需要访问该数据。React 会很快让您知道,“嘿,您实际上需要将其放在共同祖先中。”
PO 如果您使用 Angular 构建相同的 UI,并对搜索框使用指令,然后对搜索结果使用另一个指令,在这种情况下,您也会被鼓励将状态放在共同祖先中。这意味着让控制器持有作用域变量,因为您会在那里找到搜索文本,这两个指令都将绑定到该文本。我认为您实际上正在查看那里一个非常相似的范例。
PH 很有见地。但我认为仍然需要区分的是,React 组件是构建块,可用于构建许多概念上不同的组件或对象。您可以使用 React 组件来实现视图控制器或一些纯粹的仅视图事物——而对于 Angular,控制器与指令不同,指令又与“服务”不同,“服务”是 Angular 描述您将所有其他逻辑塞入其中的事物的方式。有时,将所有这些东西都做成 React 组件是有意义的。
DS 在这种情况下,如果您使用 React 构建 UI,共同祖先会是什么?一个 React 组件?
PH 是的。我将对所有内容使用 React 组件。
DS 当我刚开始使用 React 时,我认为对我来说最难掌握的事情之一是所有东西都是组件的想法。即使当我浏览 React 网站上的一个示例时,该示例包含一个评论框和一个评论列表,我还是很惊讶地了解到即使这些也被视为组件。我还发现自己迷失在这些组件之间的关系中。我想知道您是否发现这是其他新的 React 开发人员的常见问题。
PO 对于那些习惯于构建更庞大的东西的人来说,这通常被证明是一个问题。在 Facebook,我们一直使用 PHP 进行编码,我们习惯于构建微组件,然后将它们组合起来,因此这在这里并没有被证明是一个巨大的问题。无论如何,我认为我们一直鼓励的是,每当您考虑重用某些东西时,请将其分解为最小的元素。这就是为什么在您引用的示例中,您想要将评论框与评论列表分开,因为您可以在应用程序的其他部分重用这两者。我们确实鼓励人们以这种方式思考。
PH 我们还鼓励您使东西无状态化。基本上,我喜欢认为人们会对自己使用状态感到非常糟糕。我知道有时这是一个必要的恶,但每当您不得不求助于这样做时,您仍然应该感到肮脏。那是因为然后您会开始思考,“好的,所以我真的只想将此搜索状态放在我的应用程序中的一个位置。” 通常,这意味着您会找到正确的放置位置,因为您不会想处理必须在整个应用程序中同步状态的问题。而且,如果它只存在于一个规范位置,您就不必这样做。
DS 还有哪些主要的差异化因素使 React 与其他 JavaScript 框架区分开来?
PH 我们尚未谈论 React 作为表达用户界面或视图层次结构的一种通用方式,将 DOM 视为许多潜在渲染后端之一的想法。例如,它还可以渲染到 Canvas 和 SVG(可缩放矢量图形)。 除此之外,这意味着 React 可以在服务器上渲染,而无需像完整的浏览器 DOM 那样启动。它的工作方式不像它只是 DOM 之上的一些其他特定领域语言。基本上,React 非常讨厌 DOM,并且希望尽可能地在浏览器之外生存。我绝对认为这是 React 与其他 JavaScript 框架之间的一个巨大差异。
PO 我们基本上已经看到 WebGL 或任何其他通用渲染平台也发生了同样的事情。这只是回到了立即模式与保留模式的问题,您很快就会发现,只要您可以输出某些内容,它就真的无关紧要。您只需清除之前存在的任何内容即可。
DS 我也对 React 的函数式编程方面感到好奇。特别是,我想更多地了解您采用了哪些特定的函数式原则。
PH 事实是,我们实际上是一群函数式编程爱好者。部分原因是,如果您真正信奉函数式编程教会,您可以免费获得许多性能优势。例如,如果您的数据模型是可序列化的,并且您将渲染方法视为属性的纯函数,那么您可以免费获得服务器端渲染和客户端渲染,因为两者最终都成为线两端相同数据的纯函数。这样,您可以保证当您的应用程序初始化时,它将在两侧自动进入相同的状态。如果您有一个非常状态化的面向对象的易变系统,这可能非常重要,因为否则同步这两个状态会变得非常非常困难。
另一个优势与优化您的应用程序有关。我们有一个名为 shouldComponentUpdate 的钩子,您可以用更快的自定义算法替换 React 的 diff 算法。此外,许多函数式程序员真的喜欢使用不可变数据结构,因为它们可以让他们快速找出某些东西是否已更改——这只是您如何以这种方式获得免费性能优势的另一个例子。
TC 在不可变数据结构方面,我听说过一个非常强大的库,即 David Nolen 的 Om。
PH 那是一项非常酷的技术。它是为 ClojureScript 设计的,ClojureScript 是编译为 JavaScript 的 Clojure 版本。Clojure 真正酷的地方在于它的持久数据结构,这些数据结构基本上是非常快速且易于使用的不可变数据结构。
这对我们来说意味着,如果您在 Facebook 上发布了一篇文章,有人点赞了它,那么这会给您一个新的点赞事件,该事件应反映在帖子上显示的赞数中。通常,您只需改变它,但这样您就无法检测到更改是否实际发生,这意味着您基本上需要重新渲染整个内容,然后再进行差异比较。从该差异中,您将了解到只有 UI 的特定部分实际发生了更改。但是,如果您使用的是不可变持久数据结构,则可以只复制故事对象,而不是改变赞数,然后在该副本中更新赞数。
通常,这将是一种非常昂贵的方法,但在 Clojure 中,复制并不昂贵,因为它有一种方法可以做到这一点,它可以与该数据结构的所有其他部分共享指针,然后仅为实际更改的任何内容分配新对象。这是一个很好的例子,说明了底层非常复杂的抽象,但设法呈现了一个非常非常简单的用户界面——人们非常容易理解的东西。
TC 我假设这也可能有助于撤消/重做功能。
PH 对。当一切都是不可变的时,一切都会变得更简单。Om 的撤消和重做基本上只是保留指向先前状态和下一个状态的指针。当您想要撤消时,您只需将旧对象传递给 React,它将相应地更新整个 UI。
TC 整个内容?
PO 当您的状态在顶层序列化为一个对象时,您所做的只是传递它并重新渲染它——您就完成了。在我看过的一些 Om 示例中,它只是在每个点快照状态,然后为您提供一个 UI,指示您有多少个状态。然后您只需在其上前后拖动即可。或者,您可以在树的帮助下开始做一些更花哨的事情,以产生一个真正高级的撤消系统。
PO 我还应该指出,React 显然不是纯粹函数式的。我们也有一些非常命令式的步骤和钩子,可让您跳出函数式范式。但在理想的世界中,您没有任何其他数据源,因此一切都在顶层,并且只是流过——这意味着一切最终都成为这些渲染函数的非常纯粹的输出。
DS 稍早些时候,您使用了术语“引用透明性”来描述 React 渲染 UI 的方式。您能解释一下这是什么意思吗?
PH 基本上,React 组件具有 props,props 是可用于实例化这些组件的参数。您可以将它们视为函数参数。为了说“我想使用这些选项创建一个类型提前”,您只需将选项列表作为 prop 传入即可。
其思想是,如果您使用相同的 props 和状态渲染组件,您将始终渲染相同的用户界面。但这可能会变得有点棘手。例如,您不能从随机数生成器中读取,因为这会改变输出。不过,如果您将此作为 props 和状态的纯函数处理,并确保您不从其他任何地方读取,您可能会看到这将使测试变得非常快速和容易。您基本上会说,“我只想确保我的组件在获得此数据时看起来是这种特定方式。” 然后,由于您不必采用 Web-driver 方法来单击每个按钮以使应用程序进入正确的状态,然后再仔细检查以确保您做对了所有事情……好吧,很明显,这如何使测试变得容易得多——当然,这也使调试变得更容易。
版权 © 2016 由所有者/作者持有。出版权已授权给 。
消除准入壁垒
我们必须选择构建一个每个人都可以访问的网络。
- Rich Harris
https://queue.org.cn/detail.cfm?id=2790378
火焰图
这种软件执行可视化对于性能分析和调试来说是一种新的必需品。
- Brendan Gregg
https://queue.org.cn/detail.cfm?id=2927301
组件化 Web
我们可能正处于 Web 开发新革命的风口浪尖。
- Taylor Savage
https://queue.org.cn/detail.cfm?id=2844732
最初发表于 Queue 第 14 卷,第 4 期—
在 数字图书馆 中评论本文
Bruce Johnson - 在约束中狂欢
Web 向互动性发展的历程始于最初用于验证 HTML 表单的简单 JavaScript 代码片段,而近年来真正开始加速。一种新型 Web 应用程序开始涌现,这些应用拥有基于直接操作浏览器 DOM(文档对象模型)的、交互性越来越强的用户界面,并越来越多地使用 JavaScript。Google Wave 就是这种新型 Web 应用程序的典范,它于 2009 年 5 月在旧金山举行的 Google I/O 开发者大会上首次公开演示。
Eric Schrock - 在生产环境中调试 AJAX
JavaScript 语言有着一段有趣的历程。最初,它只是一种简单的工具,旨在让 Web 开发者能够向原本静态的网页添加动态元素,但如今,它已经发展成为一个复杂平台的核心,用于交付基于 Web 的应用程序的平台。在早期,该语言静默处理错误的能力被视为一项优势。如果图片翻转效果失败,与其向用户呈现难看的错误对话框,不如保持流畅的 Web 体验。
Jeff Norwalk - 案例研究:转向 AJAX
小型初创公司常常面临一系列令人眼花缭乱的技术选择:如何交付他们的应用程序,使用什么语言,是采用现有的组件(商业的或开源的)还是自行开发……等等。更重要的是,围绕这些选择的决策通常需要快速做出。本案例研究真实地反映了一家年轻的初创公司在努力将其产品部署到 Web 上时所面临的各种挑战。与许多初创公司一样,这也是一个没有美好结局的故事。