专栏名称: 奇舞精选
《奇舞精选》是由奇舞团维护的前端技术公众号。除周五外,每天向大家推荐一篇前端相关技术文章,每周五向大家推送汇总周刊内容。
目录
相关文章推荐
爱平度  ·  ​2月5日,拍到了! ·  17 小时前  
题材挖掘君  ·  DeepSeek,最新核心标的+延伸方向(精 ... ·  2 天前  
题材挖掘君  ·  DeepSeek,最新核心标的+延伸方向(精 ... ·  2 天前  
辽沈晚报  ·  官宣:解散!曾风靡全球,几乎每家都有! ·  2 天前  
湖北日报  ·  刚刚,彻底爆了!凌晨3点挤满人 ·  2 天前  
湖北日报  ·  刚刚,彻底爆了!凌晨3点挤满人 ·  2 天前  
安徽省人民政府网  ·  ​科技创新引领新质生产力发展大会召开 ·  3 天前  
安徽省人民政府网  ·  ​科技创新引领新质生产力发展大会召开 ·  3 天前  
51好读  ›  专栏  ›  奇舞精选

解析 React 渲染原理

奇舞精选  · 公众号  · 科技自媒体  · 2024-12-09 18:00

主要观点总结

本文介绍了React框架的渲染流程,包括组件的初次渲染、更新渲染以及优化手段。文章详细阐述了React的渲染原理,如何避免不必要的渲染,以及如何使用React.memo、useMemo和useCallback等优化手段。

关键观点总结

关键观点1: React渲染流程

React通过构建虚拟DOM树来描述组件结构,并与旧的虚拟DOM树对比,找到需要更新的部分,从而修改页面。

关键观点2: state与组件渲染

只有state的改变会引起组件的重新渲染,且其所有子组件也会重新渲染。

关键观点3: 避免不必要的渲染

使用React.memo、useMemo和useCallback等优化手段可以避免无谓的组件渲染。React.memo适用于缓存组件返回值,useMemo适用于缓存复杂计算的结果,useCallback适用于缓存函数实例。


正文

本文作者为 360 奇舞团前端开发工程师

简介

当我们使用React框架编写代码时,无论是组件的更新、状态的改变,还是父子组件之间的交互,都会涉及到 React 的渲染流程。你可能会有以下疑问:

  • 组件渲染的具体流程是什么?
  • 引起组件重新渲染的因素有哪些?
  • React.memo、useMemo和useCallback等优化手段的原理是什么?如何合理使用它们?

带着这些疑问,让我们开始探索React的渲染过程吧。

一、渲染过程

初次渲染

首先,我们定义一个函数组件:

function Home() {
  return (
    <>
      

Home


      

这是一个函数组件


    >
  );
}

export default Home;

当页面初次渲染时,React会先创建一个根节点,用来绑定组件:

const root = createRoot(document.getElementById('root'))

接下来,会使用root函数渲染具体的组件:

root.render()

由于我在 App 组件中引入了 Home 组件,React 调用Home函数,得到这段JSX:

<>
  

Home


  

这是一个函数组件


>

React会把JSX转换为虚拟DOM,用JavaScript对象的方式描述DOM元素,具体如下:

{
  type: React.Fragment,
  props: {
    children: [
      { type"h1", props: { children: "Home" } },
      { type"p", props: { children: "这是一个函数组件" } }
    ]
  },
}

首次渲染时,React 会将虚拟 DOM 转换为真实 DOM,最终得到结果:

"root">
  

Home


  

这是一个函数组件



1. 为什么Home组件的元素要放在<>>里?

Home组件是用JSX编写的,浏览器无法直接识别。React的构建工具(例如Babel或者Vite)会把JSX编译为JavaScript代码。如果我把Home组件的代码改为:

function Home() {
  return (
      

Home


      

这是一个函数组件


  );
}

运行代码,就会产生报错:Adjacent JSX elements must be wrapped in an enclosing tag.

因为编译结果是:

function Home() {
  return (
    React.createElement("h1", null, "Home"),
    React.createElement("p", null, "这是一个函数组件")
  );
}

而在JS中,函数只能返回一个值。所以如果要在一个组件中渲染多个元素,需要把它们放在一个公共的容器中。

更新渲染

React的运行机制可以用一个函数表示: view = f(state) view 表示页面, state 表示数据。在初次渲染后,state更新会引起组件的重新渲染,并且只有state可以引发重新渲染。当一个组件重新渲染时,它的所有子组件也会重新渲染。例如下面这段代码:

import { useState } from 'react'

function Content(props: { num: number }) {
  console.log('Content组件渲染')
  const { num } = props
  return 
{num}

}

function Home() {
  console.log('Home组件渲染')
  const [num, setNum] = useState(0)

  return (
    <>
      
      
    >
  )
}

function App() {
  return (
    <>
      
    >
  )
}

export default App

当我点击Home组件中的按钮时,num的状态会发生改变,从而使得Home组件重新渲染,观察控制台,发现它的子组件Content也渲染了。

图1

1. 父组件的重新渲染,为什么会引起子组件的渲染?

通过示例代码,我们可以观察到,当Home组件的state被修改后,也引发了Content组件的重新渲染。是因为Content的props变化引起的吗?我们可以给Home组件添加一个不使用props的子组件,以验证这个结论是否正确。

import { useState } from 'react'




    


function Content(props: { num: number }) {
  console.log('Content组件渲染')
  const { num } = props
  return 
{num}

}

function Test() {
  console.log('Test组件渲染')
  return 
Test

}

function Home() {
  console.log('Home组件渲染')
  const [num, setNum] = useState(0)

  return (
    <>
      
      
      
    >
  )
}

function App() {
  return (
    <>
      
    >
  )
}

export default App
图2

可以看出,子组件的重新渲染与是否使用props无关。那么问题来了,Test组件的数据并没有改变,为什么还要重新渲染它呢?因为React无法保证Test组件是否使用了变化的state。例如Test组件使用了一个随机数,这种情况下,即使Test组件没有使用props,也需要重新渲染。

function Test() {
  console.log('Test组件渲染')

  const randomNumber = Math.random(); 

  return (
    <>
      
Test

      
随机数:{randomNumber}

    >
  )
}

二、如何避免不必要的渲染?

1. React.memo

如果我们想优化应用,只让props值变化的组件重新渲染,可以使用React.memo 。具体用法如下:

const MemoizedComponent = React.memo(Test);

当把Test组件用 React.memo 包裹后,相当于开启了React的记忆功能。React在初次渲染后会记住当前组件的返回值,当父组件重新渲染时,如果传递过来的props值没有改变,该组件就无需重新渲染。

重新运行代码,会发现Test组件不会重新渲染了。

图3

你可能会想,为什么要手动开启React的记忆功能呢?默认只更新props改变的子组件,岂不是更有利于网站的性能?

需要说明的是,当我们使用Reac.memo后,React每次渲染前,都会比较props值是否发生了变化,假设一个父组件拥有许多子组件,那么在每次渲染该组件时,要单独对比每个子组件的props值是否更新。此外,React还需记住每个组件的返回值。因此,虽然节省了重新渲染的开销,但在无形中也增加了许多负担。所以React并没有默认开启这一策略。

2. useMemo和useCallback

我们再看一段示例代码:

import { useState } from "react";
import React from "react";

function Home(props: { arr: string[] }) {
  console.log("Home组件渲染");
  const { arr } = props;

  return (
    <>
      

home


      

            {arr.map((item, index) => (
              
  • {item}

  •         ))}
          

    >
  );
}

const MemoizedHome = React.memo(Home);

function App() {
  console.log("App组件渲染");
  const arr = ['one''two''three''four''five'];
  const [num, setNum] = useState(0);

  return (
    <>
      
      
{num}

      
    >
  );
}

export default App;

在这段代码中,使用 React.memo() 缓存了 Home 组件的返回值,该组件通过props接收了一个从父组件传递过来的数组。当我们点击按钮,修改num的值后,按道理Home不会重新渲染。但结果却是:

图4

为什么 React.memo() 失效了呢?我们先来看一段JavaScript代码:

function createArr() {
  const arr = ["one""two""three""four""five"];
  return arr;
}

let res1 = createArr();
let res2 = createArr();
console.log(res1 === res2); // false

可以看出同一个函数返回的数组并不相同。因为使用 === 比较2个对象时,对比的并不是它们的值,而是引用地址。每当我们调用 createArr 函数时,都会创建一个新的数组实例,这个数组会被分配到不同的内存地址。即使这些数组的值完全相同,它们仍然是不同的对象。

而React中的组件本质上是一个JavaScript函数,当渲染组件时,其实是在调用函数,因此在组件中定义的所有东西都会被重新创建一遍。回到示例代码,每当App组件重新渲染时,都会创建一个新的数组,Home组件从props拿到的也是新数据,所以它才会重新刷新。为了避免Home组件无谓的渲染,我们可以使用 useMemo 来避免该问题。

useMemo

在下面的代码中,我把数组用 useMemo 包裹了起来:

function App() {
  console.log("App组件渲染");
  const arr = useMemo(() => ["one""two""three""four""five"], []);
  const [num, setNum] = useState(0);

  return (
    <>
      
      
{num}

      
    >
  );
}

export default App;

再次点击按钮,发现Home组件不会重新渲染了。

图5

useMemo 可以接受2个参数,第1个参数是函数,第2个参数是一个依赖项数组,在组件首次渲染时,React会把useMemo的返回值记录下来,此后在组件重新渲染时,都会使用之前记录的返回值,除非依赖项发生变化。

useCallback

useMemo 功能类似的一个Hook是 useCallback ,它可以用来缓存函数。在组件中,我们会定义许多函数,有时需要把某些函数传递给子组件,如果不对函数进行缓存,也可能导致子组件进行无谓的重新渲染。因为子组件在比较传递过来的函数时,也是通过比较引用地址的。

import { useState } from "react";
import React from "react";

function Home(props: { onClick: () => void }) {
  console.log("Home组件渲染");
  const { onClick } = props;

  return (
    <>
      
    >
  );
}

const MemoizedHome = React.memo(Home);

function App() {
  console.log("App组件渲染");
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("点击按钮了");
  }

  return (
    

      

        

Count: {count}


        






请到「今天看啥」查看全文