react暇谈(1)——当我们再谈论render时,我们在谈论什么

225 阅读5分钟

主旨

本文旨在讨论 React 中的 render(渲染) 一词的含义?

术语定义

  • render 函数,本文中指类组件的 render 方法,或者函数组件本身。
  • render渲染,我会在文中交替的使用这两个词语,但你应该明白,你随时可以把文中的 render 换成 渲染,反之亦然。

react的render到底指什么?

如果你已经接触了 React 一段时间,你经常会看到 render(渲染),rerender(重渲染),等各种与渲染相关的字眼,当我作为一个菜鸟时,我时常为这些字眼发愁,尽管我知道这些词语的本意,但是却不明白它们在 React 中代表什么。现在,经过一定量的学习,我觉得自己终于可以看清一部分,借此分享出来,以期帮助别人,同时也是借此梳理下自己掌握的知识。

渲染发生的两个时机

为了理解 render 在 React 中代表什么,我们需要稍微深入下 React 的工作原理,也许你已经知道虚拟DOM,Diff,等各种新奇的词语,不过还是让我们按顺序梳理以下。

React 进行渲染的 时机 有两种(其实也可以算一种,其中一种只是另一种的变体):

  • 挂载,React 调用类组件 DidComponentMount 生命周期方法时,或者函数组件的useEffect中回调函数第一次执行的时候,可以认为已经发生了挂载。

  • 更新,React 调用类组件的 DidComponentUpdate 的时候,或者函数组件被重新执行时,都可以认为是发生了重渲染。

我们首先来看组件更新的情况,然后再回头看挂载的情况,你会发现,后者和前者的差别其实很小,这也是为什么 React 在函数组件里提供的 useEffect hook 不再区分挂载和更新。

让我们使用经典的计数器例子

// 警告:为了代码的精简的,我省略了 useCallback 包裹回调函数的行为
function Counter() {
    const [count, setCount] = useState();

    return (
        <div>
            <span className="count">{count}</span>
            <button onClick={() => setCount(prev => prev + 1)}>INC<button>
        </div>
    );
}

在 Counter 挂载后,当你点击了 INC 按钮时,就会触发一次 React 的组件的更新 或者说是重渲染

三个渲染阶段

这里为了讲解的方便,我自作主张的将 React 的一个完整的渲染周期的分为三个阶段,注意不要联想到生命周期,这里没有任何关系。

三个阶段是:

  1. 渲染函数阶段render函数阶段,这个阶段会执行render函数,生成虚拟DOM。

  2. tree diff 阶段,这个阶段 React 会对比新旧的的虚拟DOM,看看是否有变化发生。

  3. commit 阶段如果确实发生了变化,才会进入这个阶段,这个阶段会将虚拟DOM中发生的变化同步到真实DOM上。

一般来说,前两个阶段可以视为一个阶段,因为它们都发生在虚拟DOM层面,并且一旦触发渲染函数阶段,则一定会触发 tree diff 阶段,但是 commit 阶段只在虚拟DOM发生变化的时才可以被触发。

在了解组件更新的的流程后,我们对比着更新的流程,回过头来看组件挂载的情况:

  • 挂载有虚拟函数阶段吗?

答案: 肯定有!必须要执行 render 函数,生成虚拟DOM。

  1. 需要tree diff吗?

答案:这里视场景有不同的答案,不严谨的说,ReactDOM.render() 是没有这个阶段的,因为首次执行还不存在可以进行对比的旧虚拟DOM,会直接跳到 commit 阶段。而其他挂载的情况,被挂载的组件自身也是不会进行 tree diff 的。

这里为了避免误解多说一点,父组件更新导致某个子组件被挂载或卸载,子组件挂载时自身是不会进行 tree diff 的,视情况会直接跳到 commit 阶段或就此结束。但是父组件进行 tree diff 时会用到这个子组件。你可以认为 null 指是一个特殊的虚拟DOM,React 将它作为占位符使用,在进行 tree diff 的时候也有相关的作用。

  1. commit阶段

答案:这点毋庸置疑。

结论

现在,让我们通过确定 “这个组件发生了渲染/重渲染/无用的重渲染” 是什么意思来解答本小节的问题。

对于 ...发生了渲染/重渲染 来说都是指阶段123,也就是整个渲染阶段。生成变化后的虚拟DOM,进行 tree diff,将变化 commit 到真实 DOM 上。如果不是在指明讨论虚拟 DOM 的话,将渲染一词局限1、2阶段时没有意义的。

而对于 发生了无效的重渲染 来说,是指阶段1、2。一般发生在父组件发生了更新,子组件的props却没有变化,所以子组件生成的虚拟dom没有发生改变,在 tree diff 阶段后直接断掉了,没有进入 commit 阶段。

以后再碰到与 React 渲染相关的问题,可以直接用上文讲解的三个阶段往上套,通过理解上下文,应该可以很容易理解作者想要的强调有关渲染的什么方面。