React到底是做什么的?

1,260 阅读13分钟

React到底是做什么的?

多么诱人的标题啊,对吗?但是忽略这一点,你对React的实际作用有多大的了解?当然,在高层次上,React是 "一个用于构建用户界面的JavaScript库"。但这个JavaScript到底是做什么的?它与其他框架或简单地编写普通JS有什么不同?

React是我每天都在使用的一个工具。然而,直到最近,我才对它的幕后工作有了浅显的了解。我觉得自己能够熟练地在React中工作,并理解其API和核心概念。然而,React如何以及为什么以这种方式工作,对我来说仍然是一个谜。在阅读了无数的文档、博客文章和源代码之后,React逐渐变得不再那么神奇,同时也变得更加特别。虽然许多细节并不那么适用于日常开发,但我也对React的真正作用有了更清晰的认识。

在下面的文章中,我的目标是描述这幅React工作的画面,同时省略一些繁琐的、不太重要的细节。希望这能激发其他人深入研究React的机制--或者至少让人们对这个网络上严重依赖的库有更多的了解。

为什么对React有更深入的了解?

事实上,这并不重要 。React在使这些实施细节成为不必要的方面做得很好,以产生令人难以置信的网络应用。也就是说,我们越了解我们的工具,我们就越能利用它们来完成我们需要的东西。React(以及整个JS,真的)是一个非常灵活的环境,开发者可以用许多不同的方式解决同一个问题。挑战往往是决定哪种解决方案对特定情况是最好的。根据我的经验,完全了解React的作用有助于评估可能的实现方式,并提高代码库的整体质量。

React库与框架

有大量的文章专门讨论React和其他前端框架之间的比较。然而,对于这篇文章来说,只需要强调React试图填补什么角色就可以了。

当然,我们在React中构建的任何东西都可以用纯JavaScript和各种Web API实现。那么为什么还要用React呢?很简单,它更容易。React的声明式语法(JSX)和对低级关注点的抽象化,与纯JS相比,在开发速度和代码的可读性方面有了巨大的改进。

一个更有趣的问题。为什么React被称为一个,而不是像Vue、Ember或Angular那样的框架?与这些 "包含电池 "的框架不同,React在其目的上更加 "单一"。它不提供路由、获取数据的工具,甚至不提供全局状态管理。React专注于一件事,而且做得非常好--有效地将数据渲染到你的显示屏上。但这有多重要呢?请继续阅读!

注意:当我们讨论React用于Web应用开发时,我们通常指的是两个独立的包:reactreact-domreact 包只负责定义组件的功能。它通常与一个 "渲染器 "搭配,如react-dom ,以提供我们通常认为是React的功能集。但后面会有更多关于这个的内容。

浏览器渲染过程

我们已经说过,React的目的是为了尽可能有效地在浏览器中渲染内容。为了更好地理解这一点,并欣赏React给我们带来的功能,我们需要简单地描述一下浏览器渲染和更新网页的过程。

当用户访问一个典型的网络应用程序的页面时,一个HTML文件被返回给浏览器,其中包含特定视图的必要资源。浏览器解析该文件的内容,下载并执行任何链接的JS、CSS和图像资产。在这个过程中还会构建两个树状结构,称为DOM (文档对象模型)和CSSOM (CSS对象模型)。DOM的节点代表构成视图的元素和内容,而CSSOM则包含DOM的每个节点的样式信息。

一旦DOM和CSSOM树构建完成,浏览器就可以开始将它们拼接成第三个结构,即渲染树。渲染树不仅仅是DOM和CSSOM的合并,它还具有微妙的意义。它是一个只包含渲染页面所需节点的精确地图。这意味着不占用任何空间的节点,如脚本标签或具有display: none 样式属性的元素,将从该树中省略。

现在,浏览器已经知道哪些内容需要在屏幕上呈现,它必须确定每块内容在页面上的位置。这一步被称为 "布局 "阶段。浏览器从渲染树的根部开始,根据设备的视口来计算每个节点的位置和大小。

在这一点上,浏览器已经确定了应该显示什么内容,应该是什么样子,以及应该放在什么位置。但它实际上还没有向用户呈现任何可见的东西。这就是绘画合成 步骤的作用。绘画需要填充页面上的像素,通常是在多个层中绘制内容。然后,这些图层被拼接在一起,并在一个叫做合成的过程中以正确的顺序应用。

总的来说,这个过程被称为"关键渲染路径",它代表了浏览器的大量工作。一个高性能的用户界面的关键是尽可能少地、有效地触发这个流程。在实践中,这意味着。

  • 在可能的情况下,分批更新DOM,以防止浏览器不得不频繁地重新渲染。
  • 优化渲染每一帧所需的JS,使之尽可能快和短。
  • 寻找机会,只触发这个流程的一部分 。例如,更新一个元素的颜色或不透明度比完全添加/删除一个元素的工作要少。如果元素在页面上的位置没有变化,那么可以跳过布局阶段,避免额外的工作。请看CSS触发器,以了解哪些属性会影响渲染路径的不同阶段的更全面的列表。

注意:这只是对渲染过程和关键渲染路径的一个非常高层次的概述。如果你想深入了解每个阶段的实际情况,请查看页面底部的资源部分的链接。

React在哪里合适?

牢记上面描述的关键渲染路径,考虑一个典型的Web应用程序。在任何给定的页面上,它可能从各种来源获取数据并向用户显示格式化的结果。它包含动态内容,定期更新或改变以响应用户的互动。有表单字段、动画、模态和弹出窗口。换句话说,该应用程序需要处理大量的 DOM更新。管理所有这些更新,同时确保页面保持良好的性能,这不是一个小任务。这就是React发挥作用的地方。高效和声明性地将更新应用于DOM的手术是其架构的核心。接下来让我们看看React是如何做到这一点的。

React虚拟DOM和调和算法

React应用程序是由组件组成的。这些组件包裹着构成用户界面的功能块。它们可以接受props 作为输入,也可以管理自己的state 。然而,一个组件的真正本质是定义一个由其render() 方法返回的React元素的分组。元素是简单、廉价的JS对象,代表了React应用程序的最小构建块。

type 如果你观察上面的代码片段中的元素对象,你会注意到第一个元素有一个CardCard 的元素也有一个叫做children 的道具。children 的值只是另一个元素,在本例中是一个h1 。通过这种映射,React元素建立了父子链接,与我们在DOM树中观察到的结构类似。这个元素树通常被描述为 虚拟DOM在React中。为了更具体地看到虚拟DOM的作用,让我们来看看一个基本的React应用程序。

上面的应用程序由一个按钮和一个随机生成的数字列表组成。点击按钮将向列表中添加一个新的数字。当React应用程序运行时,它走过每个Component并调用其render() 方法。render() 方法返回一个构成该组件的元素树。下面是这个应用程序的样子,以元素的形式表示。

或者,可视化为一棵组合的元素树。

注意:这个组合 对象的目的只是为了更好地可视化元素之间的关系。它不是由React创建的结构的实际表示。

一旦React构建了这个元素树,它就开始将该树与当前页面上显示的内容进行比较。React识别出准确的差异列表,并将变化列表交给一个 "渲染器",如ReactDOM。ReactDOM负责接收这批指令并将更新应用于浏览器,将应用程序与前面描述的渲染路径联系起来。

当应用程序首次初始化时,对比很容易。虚拟DOM中的东西目前不存在于实际的DOM中。所以发送到ReactDOM的指令列表必须包括从头开始创建一切的步骤。但当你点击 "添加随机数 "按钮时会发生什么?在视觉上,一个新的数字被添加到屏幕上的列表中。在程序上,state 变量,numList ,被更新以包含新条目。然而,再深入一步,虚拟DOM的作用变得更加普遍了。当一个组件上的state 值发生变化时,React会再次调用该组件的render() 方法。和以前一样,调用render() 将返回一个类似的元素树。type 不过这一次,该树将包括一个新的元素,li ,代表额外的列表项。React现在有一个旧的树,描述页面上当前可见的内容,还有一个新的树,描述我们希望更新后的页面是什么样子。现在React必须比较这两棵树,并向ReactDOM提供同步所有变化的指令。这导致新的数字被追加到屏幕上的列表中。

那么,为什么虚拟DOM是构建高性能Web应用的关键?虚拟DOM使开发人员能够只专注于描述他们希望更新的用户界面是什么样子。然后,React采用这种描述并建立一个元素树--与浏览器DOM元素相比,创建和浏览这些元素要便宜得多。有了这个特制的树,React可以有效地协调需要对视图进行哪些修改。然后,更新可以以高度优化的批次应用于实际的DOM。聪明,对吗?

让我们翻开React效率的另一块重要石头。React是如何廉价地确定新旧元素树之间的变化的?一个网络应用可以很容易地由成千上万的元素组成。使用传统的启发式方法将需要太长的时间来确定在如此庞大的树上应该发生什么更新。出于这个原因,React开发了Reconciliation算法。这个算法是一套可预测的、易于遵循的规则,使开发者能够根据自己的需要进行优化,或多或少。

关于Reconciliation规则的文档阅读起来很快,而且很全面,所以我就不在这里重复所有的内容了。但简而言之,当一个组件的render() 方法被调用时,通常是在stateprop 更改之后,返回的组成该组件的元素树将与之前产生的树进行比较。从每个树的根部开始,如果两个元素之间的type 是不同的,比如一个div 变为一个span ,那么这个节点以及树中所有在它下面的节点将立即被标记为 "新",而不需要进一步比较。任何嵌套的 "元素 "将被拆毁,所有产生的DOM节点将被从头创建。然而,如果这些元素有相同的类型,它们的props ,然后进行比较。对于DOM元素,像div ,React会识别出改变了的特定属性,并只更新现有DOM节点上的这些值。但是,对于具有组件type ,例如NumList ,该组件的render() 方法会被调用,并获得最新的prop ,这个过程在返回的元素树上递归重复。

经验之谈

简而言之,React为开发者提供了一种声明性的机制来描述网页应该是什么样子,而不需要经常关注浏览器渲染过程中的许多细微差别。它通过JS对象树和明确表达的规则来实现这一目标,以确定何时发生了变化。这些规则使开发人员能够进一步优化性能,以满足其应用程序的需求。下面是一些在开发中需要记住的概念。

  • React中的 "渲染"(或 "重新渲染")是一个比浏览器的渲染更便宜的操作。虽然要注意你的组件何时触发重新渲染,但在大多数情况下,没有必要消除所有非必要的渲染。事实上,在某些情况下,这甚至会损害 性能
  • 我们可以在代码中采用策略,以更好地与React的Reconciliation算法保持一致。确保你在生成组件列表时使用足够独特的key 道具,并有意识地改变一个元素的type ,特别是在你的应用程序的根部附近。有条件地将一个div 改为一个span ,或者使用三元组在两个父级组件之间切换,可能会有令人惊讶的代价。
  • React并不神奇。开发人员仍然需要知道DOM何时以及如何被更新。一个组件是否被频繁地从DOM中添加和删除?是否可以用CSS来限制浏览器的渲染路径的哪些部分被触发?如果一个React组件的state ,更新速度很快,有什么办法可以减缓被推送到DOM上的变化吗?也许可以用一个很好的debounce?
  • 我们每天使用的库和框架不一定是神秘的。这就是开源的魅力所在。潜入其中,做出贡献,学习事物的工作原理。你可能会带着一两个概念离开,从而改善你自己的应用程序。

资源

进一步阅读