美文网首页Front End
[FE] React 源码阅读(八):组件的重新渲染

[FE] React 源码阅读(八):组件的重新渲染

作者: 何幻 | 来源:发表于2021-11-03 19:24 被阅读0次

    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. 组件的重新渲染

    相关文章

      网友评论

        本文标题:[FE] React 源码阅读(八):组件的重新渲染

        本文链接:https://www.haomeiwen.com/subject/vzlczltx.html