React重渲染re-render原理是什么?【译】

434 阅读8分钟

为什么React会Re-Render?【译】

来源www.joshwcomeau.com/react/why-r…

正文

1、背景

很多人不知道:

  • React-re-rendering重新渲染是怎么工作
  • 是什么触发了React的重渲染

如果我们不理解React渲染周期的原理,那么我们如何去理解React.memo或者useCallback这些Hook呢

下面我们对【React什么时候、为什么进行重新渲染】进行讲解,

2、React Loop的核心

首先一个基本事实:在React中的每一个重新渲染都伴随着一个state状态的改变,这是唯一一个可以触发React组件重新渲染的trigger

但是上面会不会听起来不太正确?毕竟组件的props参数改变难道不会引起组件的重新渲染吗?context上下文的改变难道不会引起重新渲染吗?

还有一句话:当一个组件重新渲染的时候,它的子代也同样会进行重渲染

import React from 'react';
​
function App() {
  return (
    <>
      <Counter />
      <footer>
        <p>Copyright 2022 Big Count Inc.</p>
      </footer>
    </>
  );
}
​
function Counter() {
  const [count, setCount] = React.useState(0);
  
  return (
    <main>
      <BigCountNumber count={count} />
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </main>
  );
}
​
function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">Count:</span>
      {count}
    </p>
  );
}
​
export default App;

App组件中有一个子代Counter,Counter里面又有一个子弟BigCountNumber

在React中,每一个state变量都依附在了一个特定的组件实例上,上面的代码中,我们仅有count这个state,这个state和我们的Counter组件相关联。

当该count 变量改变的时候,Counter组件会重新渲染,BigCountNumber 这个组件也会因为Counter重新渲染而重新渲染

当count改变的时候,Counter和BigCountNumber组件会重新渲染,但是外层的App不会重新渲染

所以现在我们澄清一个误解一:当一个state变量改变的时候整个应用都会重渲染

一些开发者会认为在React中每一个state的改变都会强制让整个应用都进行渲染,这个其实是不对的,重新渲染仅仅影响和当前state相关联的组件,以及该组件的后代组件。

那么为什么它要酱紫实现呢?

React的主要工作是保证应用的UI展示是和React state同步的,re-render重新渲染的意义其实是计算出UI的哪些内容被改变了。

让我们用上面的Counter案例来解释,当应用第一次挂载的时候,React渲染所有的组件并且计算出了下面这个HTML,表示DOM应该展现出什么样子的草图

<main>
  <p>
    <span class="prefix">Count:</span>
    0
  </p>
  <button>
    Increment
  </button>
</main>
<footer>
  <p>Copyright 2022 Big Count Inc.</p>
</footer>

当用户点击按钮的时候,count变量就从0变成1了,这个就会导致我们UI上面显示的Count值改变了。

React就会为Counter和BigCountNumber这两个组件重新运行代码,然后我们就可以生成一个新的sketch草图。

<main>
  <p>
    <span class="prefix">Count:</span>
    1
  </p>
  <button>
    Increment
  </button>
</main>
<footer>
  <p>Copyright 2022 Big Count Inc.</p>
</footer>

每一次的render渲染都是一个snapshot快照,就好像一个照片一样,这个照片就给我们展示了基于当前应用的state UI应该长什么样。

React就好像玩了一个“找不同”游戏一样去计算出新旧两个快照之间有上面不同,所以上面的例子中,两个render产生的快照里面就有一个不同,就是展示Count数值的text node从0变成了1,所以就通过修改这个text node去匹配上快照就行,完成了这个工作之后,React就“回到位置上”等待下一次state的改变。

以上就是React Loop的核心

因为count这个state是和我们Counter组件相关联的,所以直接影响了Counter,并且还要对Counter子代组件进行重新渲染,因为BigCountNumber组件负责展示了count的值,如果我们不重新渲染BigCountNumber这个组件的话,我们就不会知道我们快照里面的text node应该从0变成1,所以我们需要把BigCountNumber组件加入到我们的快照里面

所以re-render重新渲染的目的其实是计算出state的改变影响了哪些用户UI视图,为此我们需要去重新渲染所有 潜在影响的组件,这样才能得到一个准确的snapshot快照

3、重渲染难道不是因为组件的props改变吗?

第二个大误解:一个组件的参数props改变了,那么这个组件将进行re-render重新渲染

例子:

import React from 'react';
​
function App() {
  return (
    <>
      <Counter />
      <footer>
        <p>Copyright 2022 Big Count Inc.</p>
      </footer>
    </>
  );
}
​
function Counter() {
  const [count, setCount] = React.useState(0);
  
  return (
    <main>
      <BigCountNumber count={count} />
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <Decoration />
    </main>
  );
}
​
function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">Count:</span>
      {count}
    </p>
  );
}
​
function Decoration() {
  return (
    <div className="decoration">
      GOGOCJ
    </div>
  );
}
​
export default App;

例子中我们添加了一个Decoration组件,里面是一个写死的文本GOGOCJ,并没有依赖于count变量,那么当我们改变count变量的时候,它会不会重新渲染呢?

实验发现,会重新渲染

当一个组件重新渲染的时候,它会尝试去对它所有的子代都进行重新渲染,不管是否有state作为props或者没有,都会进行重新渲染。

那么我们并没有传递count这个参数去组件,为什么它需要重新渲染?

答:因为React很难百分百确定Decoration这个组件会不会直接或者间接的去依赖于count这个state变量。

①、在理想情况下,React组件应该总是pure的(纯函数、纯组件)

pure组件就意味着给相同的参数得到的是一样的UI结果、函数结果

②、但是真实情况是,我们很多的组件都是不纯的(impure),比如下面这个就是一个不使用state的非纯组件:

function CurrentTime() {
  const now = new Date();
  return (
    <p>It is currently {now.toString()}</p>
  );
}

每次渲染的时候当前组件都将展示不同的value值,因为当前组件依赖于当前时间。

假如我们使用ref去操作了一个本来是pure的子元素,那么React将永远没有办法去分清楚到底是pure还是impure,所以最保守的方式就是:React不做这个pure的判断,默认是重新渲染所有的子代,而吧这个设置一个组件是不是纯组件的权力交给了程序员。

4、创建一个纯组件

React.memo 和 React.PureComponent 类组件,这两种方式都可以让我们忽略重新渲染

function Decoration() {
  return (
    <div className="decoration">
      ⛵️
    </div>
  );
}
export default React.memo(Decoration);

通过React.memo包裹的组件就“告诉”React:当前组件是纯组件,放心吧,你不需要重新渲染,除非这个组件的props参数改变了“

这是一种memoization记忆化技术

这种方式就会让React将记住之前的snapshot,如果当前组件没有props改变的话,React将复用之前的snapshot快照而不是去重新的生成一个快照。

你可能会酱紫想:为什么这种记忆化模式要程序员做,而不是React在编译的时候进行识别作为一个默认的优化方式?

但是其实React的开发团队已经在积极的调研是否可以在对React代码编译时期进行【”auto-memoize“自动判断是否是纯组件并进行备份】的可能性,目前还是试验阶段,但是初期实验效果显著

5、那如果是通过Context上下文传递state呢?

默认情况下,当前state改变了,就会改变所在组件的所有子组件,即使是通过context把state传递到后代组件的也是一样会重新进行渲染的

例如:

const GreetUser = React.memo(() => {
  const user = React.useContext(UserContext);
  if (!user) {
    return "Hi there!";
  }
  return `Hello ${user.name}!`;
});

这里有一个纯组件使用了UserContext这个上下文,在这个例子中GreetUser组件是一个纯组件,因为没有使用props,但是它有一个”看不到“的依赖:通过context把一个存在于React state里面的user变量传递过来了

如果user这个state改变了,将发生一次重新渲染的操作,GreetUser也将要产生一个新的snapshot快照,因为React知道这个组件使用了一个额外的context,React会认为这个context其实也是一个props,props改变了那么就要重新进行渲染了

英语生词

  • get by 得到、过得去,应付过去

    • We understand enough to get by 我们懂得足够多,可以应付过去
  • a handful of 一把、少量的、少部分的

  • hand-wavy 简短的

    • hand-wavy answers 简短的回答
  • misconceptions 误解

  • uncertainty 不确定性

  • tutorial 教程

  • mental 思考的、精神的

  • Intended audience 阅读对象

    • intended 语气的、故意的、为...打算的
  • intermediate 中间的,中等水平的

  • bookmark 书签

  • fundamental 基本的

    • fundamental truth 基本的真理
  • that probably doesn't sound right... after all

    • 听起来不太对,毕竟。。。
  • descendants 后代

  • interactive graph 交互图

  • mechanic 机械工、手段 、方法

  • comes up with 提出

  • sketch 草图

  • flip 翻转

    • flip from ... to ...
  • brand new 全新的

  • liberty 自由

  • aside from 除了

  • no quite 不完全是

    • quite 相当
  • regardless 不管

  • intuitive 直观的、直觉的、易懂的

  • sneakier 隐蔽的,鬼鬼祟祟的,卑鄙的

  • mutated 突变

  • stale 不新鲜的

  • cascade 倾泻、连续传递

  • in order 为了,整理,有秩序的

  • tweak 调整、微调

  • sorta 近似,有几分

  • going through the trouble of 经历什么的麻烦

  • pretend 假装

  • exact 确切的,精确的

  • snap 断裂 折断,咔嚓

    • snap a picture 拍了一张照片
  • live-code 实况代码

  • wondering 想知道

  • improve performance 提高性能

  • overestimate 高估

  • a bunch of 一群

  • counter-productive 适得其反

    • counter计算器
    • productive 富有成效的
  • circumstances 场景、情况

  • investigate 调查

  • complicate 复杂的

  • stuff 东西、物品

  • more-or-less 或多或少

  • consumes 消耗 使用

  • Profiling 分析 资料收集

    • Profile 人的外形 轮廓,侧面
  • trick 技巧 花招

  • profiler 分析器

  • subtle 微妙的,不易察觉的

  • mind-bending 离奇古怪的

    • bend 弯曲
  • wrack 破坏、毁坏、沉船

  • havoc 灾难、造成严重破坏

  • inscrutable 神秘的,不可理解的

  • confession 供认、承认、坦白

  • pluck 摘录

  • modality 形式、形态

  • milliseconds 毫秒

  • percentile 百分位

  • tempting 吸引人的、诱人的

  • spree 疯狂、放纵

  • sluggish 缓慢的、迟钝的

  • aside 一边、旁边、除什么之外