重学 React -- 渲染那些事

1,186 阅读7分钟

前置知识:

  1. 这篇文章并不是面向 0 基础新同学的,需要有一些 react / vue 基础
  2. 同时读者需要知道什么是虚拟 dom

render 函数

我们知道 React 的一大特性就是引入了 vnode 的概念,用操作虚拟 dom 树来代替操作真实 dom 树,减少性能损耗

当调用 React 的 render() 方法时,会创建一棵由 React 元素组成的树。在下一次 statesprops 更新时,相同的 render() 方法会返回一棵不同的树。React 会对这两棵树做 diff,找出差异点,只对有差异的部分做渲染

传统的 diff 需要 O(n^3) 的复杂度,而 React 经过优化之后降值 O(n) 的复杂度:

  • 假设两个不同类型的元素会产生出不同的树,那么当某个节点类型都不同时,直接删掉整个节点,然后重建,不再做后续无谓(或者说收益很小)的 diff
  • 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变,这在循环渲染中很有用

具体的节点比较可以参考 React 官方的声明

由上述我们知道,React 在调用 render 函数时会做 diff,diff 判断到两个 vdom 的差异之后就会开始去操作 dom 做真正的 UI 渲染。在函数组件中,整个函数的执行相当于在调用 render 函数,所以某种意义上 React 的渲染就是函数组件的执行过程

render 函数和 UI 渲染的关系

什么时候执行 render 呢,或者说,函数组件什么时候被调用呢?

  1. 当组件的状态(propsstates)发生变更时
  2. 当父组件 re-render 时,子组件也会 re-render,除非子组件被 React.memo 装饰

当组件的状态(propsstates)发生变更时 render 函数会被执行,但这并不代表会做 UI 更新。这时候虚拟 dom 的优势就表现出来了。React 会对整颗虚拟 dom 树做 diff,只渲染有差异的部分

React 是通过 bail out 来确定 props / states 是否有变更的,背后的原理是 Object.is,我们可以测试一下 react 渲染和 dom UI 重绘的关系

首先我们需要安装 React Developer Tools,安装完后我们在控制台中打开 React 的 render highlight(PS. 这插件有可能被墙,无法从官方商店下载的小伙伴请自行找资源 😅 )

1.png

我们写个简单的 Demo:

  • 父组件 App 有两个子组件 Son,一个按钮,以及一个 msg 的 state
  • 子组件接收来自父组件的 msg
  • 点击父组件的按钮,将会设置 msg
// 父组件 App
import React, { useState } from "react";
import Son from "./Son";

function App() {
  const [msg, setMsg] = useState("");
  return (
    <>
      <Son msg={msg} />
      <Son />
      <button onClick={() => setMsg("Hello")}>set msg</button>
    </>
  );
}

export default App;
// 子组件Son
import React, { FC } from "react";

interface SonProps {
  msg?: string;
}
const Son: FC<SonProps> = (props) => {
  return <div>The message is: {props.msg}</div>;
};

export default Son;

我们点击按钮,msg 的 state 被赋值,触发 render 函数,页面中 render 的组件会高亮

2.png

上文中我们说过,在函数组间中,函数组件的执行可以理解成 render 函数的执行。 所以上面的代码总共执行了 3 次 render 函数(App 一次,两个 Son 组件各一次)

我们发现,即使第二个 Son 组件没有任何 props,但它的 render 函数也被调用了,但这并不代表 UI 重渲染了 3 次。还记得上文提到的 diff 么,React 会通过 diff 判断到 App 组件与第二个 Son 组件并没有发生变更,所以真正的 UI 渲染,其实只渲染了第一个 Son 组件而已

我们把 React 的高亮关掉,打开 UI 线程渲染的高亮

3.png

刷新页面,重新点击按钮

4.png

可以发现只有第一个 Son 高亮了,这是因为 React 做完 diff 之后,知道第二个 Son 组件不需要重新渲染,所以只操作真实 Dom 做第一个组件的渲染

至于那个 button 为什么高亮了,这是因为当前打开的是 Chrome Render 线程真实的渲染高亮。button hover 以及 click 的时候样式都会有变,所以渲染线程也会高亮

上面有提到 React 是通过 Object.is 来判断状态是否相同的,感兴趣的同学可以尝试一下:

  • props 由 1 → 1(react render 不执行)
  • props 由 1 → 2(react render 执行)
  • props 由 {a:1} → {a:1}(react render 执行)
  • props 由 {a:1} → {a:2}(react render 执行)

开发者能做的优化

React 确实在性能上在框架层就帮我们做了很多优化,确实它尽力了。但这还不够,有部分优化是需要开发者自己去实现的。

key

我们先看一段很简单的 dom 结构

<!-- before -->
<ul>
  <li>A</li>
  <li>B</li>
</ul>

<!-- after -->
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

如果我们要在一个 ul 中插入一个节点,假设是插入到末尾,那么好办,React 比较前两个 节点,没发现差异,最后一个节点是新增的,直接插入

但如果是这样呢

<!-- before -->
<ul>
  <li>A</li>
  <li>B</li>
</ul>

<!-- after -->
<ul>
  <li>C</li>
  <li>A</li>
  <li>B</li>
</ul>

如果我们插入在开头,那么 React 就懵逼了。它对比第一个 li 节点,发现不一样;对比第二,第三个,都发现不一样,对每一个有差异的节点,都需要做一次更新

但事实上,作为开发者,我们知道:A、B 还是那个 A、B,只不过 C 插入到了列表的开头。 那这时候,我们可以给每个节点绑定一个唯一标识符,主动告诉 React 这个节点是谁

<!-- before -->
<ul>
  <li key="1">A</li>
  <li key="2">B</li>
</ul>

<!-- after -->
<ul>
  <li key="3">C</li>
  <li key="1">A</li>
  <li key="2">B</li>
</ul>

通过这种类似于 “身份标记” 的手段,React 就能知道:
哦原来这个 A、B 就是刚刚那个 A、B

需要注意的是,因为我们绑定的 key 相当于给这个元素一个身份证,所以这个标记必须是唯一且不变的,否则就不能通过这个标识证明 “这个节点就是刚刚那家伙” 。所以 key 不能用类似于 Math.random() 这种随机生成的不稳定的值

这个也很好理解,想象一下如果你的身份证号不是唯一的,当出现一个和你身份证一样的人时,你怎么证明你是你呢?

更多的优化

我们回到上面的 Demo。虽然 React 已经做了大量的优化,比如通过 vdom 减少不必要的 UI 渲染,甚至在 diff 上大费周章只为得到最佳的 diff 性能。但仍有两道坎是它必须面对的:

1. React 必须在每个组件上运行 diff 算法,以检查它是否应该更新UI。
2. 这些渲染函数或函数组件中的所有代码都将再次执行。

就这两点来说,React 它自身是没办法从框架层做优化的,因为它没法在做 diff 前知道组件的差异

等等,假如说,我们提前告诉 React 呢?比如类似于断言,主动告诉 React “当前节点需要更新或不需要更新” 是否可行呢?

答案是肯定的。这部分优化的内容涉及到 React.memouseCallbackuseMemo 等新概念,我将单独用一篇文章来阐述这些优化手段

总结

Q1:React 的 render 函数和 UI 渲染是什么关系?
A1:组件 render 函数执行时,并不代表每个组件对应的 dom UI 都会重新渲染。React 会做 diff,没变更的 UI 就不会触发 UI 绘制

Q2:React 什么时候执行 render 函数?
A2:props 或 states 变更的时候。如果涉及到组件嵌套,父组件 render 时,子组件也会 render

Q3:React 是怎么判断 props / states 是否变更的?
A3:Object.is

Q4:循环渲染时 key 的作用是什么?
A4:身份标识,提高 diff 准确性

预告一下,下一篇我会扯一下关于 React 渲染优化的事情