[FE] React 初窥门径(八):组件的重新渲染

1. 术语的一些解释

渲染 在维基百科中是这样解释的,

在电脑绘图中,是指以软件由模型生成图像的过程。

在前端领域,(页面)渲染指的是,代码中通过调用 DOM API 使得修改结果最终显示在 web 页面上。
在 React 源码中,渲染是由 performSyncWorkOnRoot 完成的,它包含了两个阶段

  • renderRootSync(render 阶段):创建/修改 Fiber Tree
  • commitRoot(commit 阶段):将 Fiber Tree 的修改最终落实到 DOM 中

所以,拿电脑绘图领域的术语来讲,Fiber Tree 就是 模型commitRoot 才叫 渲染
renderRootSync 只是 修改 模型的过程。

因此,当我们提到 组件的渲染/组件的重新渲染 时,指的是 performSyncWorkOnRoot 而不是单独的 render 阶段
只有单独提到 render 阶段 时,才指 renderRootSync

2. 回顾

上一篇 文章,我们介绍了 hook 相关的内容,
当组件里有多个 hook,一个 hook 被调用多次时,这些与组件相关的状态是如何创建或更新的。
(1)多个 hook 会被关联到 Fiber Node 上
(2)每个 hook 会维护一个 update quque 用来计算最终状态,每次调用 hook 会在队尾加一个 update 元素
(3)不管是多个 hook 还是一个 hook,只有第一次 hook 调用会立即计算结果。后续所有调用,都会在调用返回后,由 React 通过 flushSyncCallbackQueuerender 阶段 计算最终结果

熟悉 React 的朋友也许知道,早起 React 有两大核心概念:Vitual DOM 和 Diff 算法。
现在看来,Fiber Tree 就是 Virtual DOM 了。
那么 Diff 算法在哪里呢?什么情况下组件会重新渲染呢?Diff 是如何做的呢?

本文我们就来分析一下组件的重新渲染过程。

3. 示例项目的修改

示例项目 中,我们增加了一个文件 example-project/src/AppDiff.js

import { useState, memo } from 'react';

const App = () => {
  debugger;
  return <div>
    <FnComp />
  </div>
};

const FnComp = () => {
  debugger;
  const [state, setState] = useState(0);
  debugger;

  const onDivClick = () => {
    debugger;
    setState(state + 1);
    debugger;
  };

  return <div onClick={onDivClick}>
    <MemoFnComp />
  </div>;
};

const MemoFnComp = memo(() => {
  debugger;
  return 'memo fn comp';
});

export default App;

组件结构如下:

[HostRoot] {tag: 3}
  [FunctionComponent] (App) {tag: 0}
    [HostComponent] (div) {tag: 5}
      [FunctionComponent] (FnComp) {tag: 0}
        [HostComponent] (div) {tag: 5}
          [SimpleMemoComponent] (MemoFnComp) {tag: 15}
            [HostText] ('memo fn comp') {tag: 6}

4. 第二棵 Fiber Tree 的创建过程

根据前面几篇文章的分析,我们知道页面载入时,React 只会创建一棵完整的 Fiber Tree(current),
另一棵 Fiber Tree 只包含一个根节点,如下图所示,

我们来跟踪一下组件 FnComp 更新 state 时,第二棵 Fiber Tree 的变化,
8. 组件的重新渲染

图中按序号(顺序)标明了,第二棵 Fiber Tree 的创建过程,


可以看到几件事情:

(1)render 阶段 renderRootSync 对 Fiber Tree 的处理,总是从 root(FiberRootNode)开始。
所以,即使是状态更新发生在了 FnComp 中,其祖先组件所对应的 Fiber Node 也被遍历了,
因此第二棵 Fiber Tree 中 FnComp 以上的 Fiber Node 都会被创建出来(下次更新会复用这些节点)。

(2)MemoFnComp 以下的 Fiber Node 没有被创建,仍然指向了第一棵 Fiber Tree(直到 MemoFnComp 有更新)。

(3)每一次调用 perfomrUnitWork 处理一层 Fiber Node。
从上到下依次是(与组件结构一致),

[HostRoot] {tag: 3}
  [FunctionComponent] (App) {tag: 0}
    [HostComponent] (div) {tag: 5}
      [FunctionComponent] (FnComp) {tag: 0}
        [HostComponent] (div) {tag: 5}
          [SimpleMemoComponent] (MemoFnComp) {tag: 15}
            [HostText] ('memo fn comp') {tag: 6}

处理到特定组件时(例如 FnComp),会判断该组件是否需要更新。
如需更新就会通过 renderWithHooks 再次调用 FnComp,此时 组件通过 hook 获取最新的值。
如果不需要更新,例如所有其他组件,都会调用 bailoutOnAlreadyFinishedWork 只处理 Fiber Node,不调用组件。

值得一提的是,React 似乎称 调用组件 FnComprender(看函数名 renderWithHooks 可知)。
所以在这样的语境中,我们可以说,React 通过一套机制来决定 组件是否 re-render(“重新渲染”)。
看来这套机制就是所谓的 Diff 算法,与通常的理解不同的是,Diff 算法并不是一个独立的函数,而是耦合在 Fiber Node 的 re-render 过程中。

5. 组件的更新细节

组件是否需要 re-render(“重新渲染”)是 beginWork L19642 这个函数判断的(有 360 多行),

function beginWork(current, workInProgress, renderLanes) {
  ...
  if (current !== null) {
    ...
    if (oldProps !== newProps || hasContextChanged() || (
      ...
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      ...
      // 不更新
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      ...
    }
  } else {
    ...
  }

  ...
  switch (workInProgress.tag) {
    ...
    case FunctionComponent:
      {
        ...
        // 更新 函数组件(例如 FnComp)
        return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
      }
    ...
    case HostComponent:
      // 更新 html 标签组件(例如 div)
      return updateHostComponent(current, workInProgress, renderLanes);
    case SimpleMemoComponent:
      {
        // 更新 记忆函数组件(例如 MemoFnComp)
        return updateSimpleMemoComponent(current, workInProgress, workInProgress.type, workInProgress.pendingProps, updateLanes, renderLanes);
      }
    ...
  }
  ...
}

以上示例项目中的组件更新,总共用到了 beginWork 4 个出口:

  • bailoutOnAlreadyFinishedWork:不更新
  • updateFunctionComponent:更新 函数组件 的业务逻辑
  • updateHostComponent:更新 html 标签组件 的业务逻辑
  • updateSimpleMemoComponent:更新 记忆函数组件 的业务逻辑
[0] performSyncWorkOnRoot
    [1] renderRootSync
        [2] workLoopSync
            [3] performUnitOfWork {tag: 3}
                [4] beginWork$1
                    [5] beginWork
                        [6] bailoutOnAlreadyFinishedWork  <- 不更新 root
            [3] performUnitOfWork {tag: 0}
                [4] beginWork$1
                    [5] beginWork
                        [6] bailoutOnAlreadyFinishedWork  <- 不更新 App
            [3] performUnitOfWork {tag: 5}
                [4] beginWork$1
                    [5] beginWork
                        [6] bailoutOnAlreadyFinishedWork  <- 不更新 div
            [3] performUnitOfWork {tag: 0}
                [4] beginWork$1
                    [5] beginWork
                        [6] updateFunctionComponent       <- 更新 FnComp
                            [7] renderWithHooks
                                [8] Component
                            [7] reconcileChildren
            [3] performUnitOfWork {tag: 5}
                [4] beginWork$1
                    [5] beginWork
                        [6] updateHostComponent           <- 更新 div
                            [7] reconcileChildren
            [3] performUnitOfWork {tag: 15}
                [4] beginWork$1
                    [5] beginWork
                        [6] updateSimpleMemoComponent     <- 更新 MemoFnComp
                            [7] bailoutOnAlreadyFinishedWork
    [1] commitRoot

可以看到,只要父组件(FnComp)有状态(state)变化,它的子组件都会进行更新(update)操作。

[HostRoot] {tag: 3}
  [FunctionComponent] (App) {tag: 0}
    [HostComponent] (div) {tag: 5}
      [FunctionComponent] (FnComp) {tag: 0}             <- state 变更
        [HostComponent] (div) {tag: 5}                  <- 更新
          [SimpleMemoComponent] (MemoFnComp) {tag: 15}  <- 更新(因为属性未变化,所以不更新子组件)
            [HostText] ('memo fn comp') {tag: 6}        <- 不更新

state 发生变化的组件,会经过 renderWithHooks 重新调用组件函数 FnComp(不然无法拿到变更后的 state),
然后这个函数(FnComp)就返回了(返回的是 React Element)。

接着 React 通过 reconcileChildren 来处理返回的 React Element 创建/更新 child Fiber Node。
最后把这个 child Fiber Node 挂载到 Fiber Tree 中。

[5] beginWork
    [6] updateFunctionComponent       <- 更新 FnComp
        [7] renderWithHooks
            [8] Component             <- 调用组件函数 FnComp
        [7] reconcileChildren         <- 将 返回的 React Element 转换成 Fiber Node

从这里可以看出 React 在 render 阶段 从上到下处理 Fiber Tree 时,是根据 React Element 来进行的。
只要 React Element 没变,Fiber Tree 就不会发生变化,
这一点在 React 处理 SimpleMemoComponent(MemoFnComp)时,也能体现出来。

因为 MemoFnCompReact.memo 返回的带记忆功能的组件(属性未变时不会更新,普通函数组件属性未变也会更新),
MemoFnComp 函数并没有被再次调用(对比 FnComp 通过 renderWithHooks 调用了),
所以 React 在处理它(Fiber Node)的子节点时,只能沿用上次的结果(指向第一棵 Fiber Tree 中的节点)。

可参考 updateSimpleMemoComponent L17762 的业务逻辑,

6. 总结

  • 处理页面渲染 React 用了两个步骤:
    一个称为 render 阶段(更新 Fiber Tree【应该就是所谓的 Virtual DOM 了】)
    另一个称为 commit 阶段(实际更新 DOM),而电脑绘图领域的 “渲染” 通常指的是后者。
  • 组件的 重新渲染(re-render)指的是,组件函数 被重新调用(例如,FnComp
    React 用了一套机制来判定 组件函数 是否需要被调用,这套机制应该就是所谓的 “Diff 算法” 了,但是 React v17.0.2 中该算法并没有抽取到一个方法中,而是散布在 render 阶段 各处。
    组件被重新渲染,就会返回新的 React Element,这些 React Element 所表示的组件都会被更新,不管其属性是否发生了变化。
  • 函数组件 可以使用 React.memo 来实现属性未变时,不进行更新。

参考

维基百科:渲染
React 初窥门径(七):hook 状态创建/更新原理
github: thzt/react-tour/example-project
8. 组件的重新渲染

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容