Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

减少 React 组件的重新渲染 #15

Open
bouquetrender opened this issue Nov 1, 2023 · 0 comments
Open

减少 React 组件的重新渲染 #15

bouquetrender opened this issue Nov 1, 2023 · 0 comments
Labels

Comments

@bouquetrender
Copy link
Owner

bouquetrender commented Nov 1, 2023

在 React 中渲染阶段有 initial render 初始化渲染和 re-render 重新渲染,了解是什么原因导致触发重新渲染比较重要,虽然整理时思路清晰,但在平时写代码的时候还是得想下如何合理划分组件才行。

触发 re-render 的原因

  1. 当组件的 state 发生变化,例如调用 setState
  2. 父组件重新渲染,导致子组件渲染
  3. context provider 变化导致该 context 内所有组件渲染

相关 state 定义在子组件中

基本的子组件用的 state 如果定义在父组件会触发同级组件 FooCompoent 重新渲染

const ChildComponent = () => {
  const [title, setTitle] = useState("");
  return <div>{title}</div>;
};

const MainComponent = () => {
  // title 不应该定义在这里

  return (
    <>
      <ChildComponent />
      <FooCompoent />
    </>
  );
};

避免在组件中创建组件

子组件不应该定义内部,否则每次父组件 re-render 都会将 ChildComponent 销毁再创建

const ChildComponent = () => <ACompoennt />;

const MainComponent = () => {
  // ChildComponent 应该定义在外部而不是在这里
  // const childComponent = ...

  return <ChildComponent />;
};

将组件作为 children 传递或 props

当修改父组件的一些状态但不想影响子组件时,可以写一个新的组件,将子组件当作 ChildComponent 传递进这个新组件,并将状态修改加在这个新组件中。

例如下面例子中每次修改 title 就会触发 ChildComponet 渲染:

const Component = () => {
  const [title, setTitle] = useState("");
  // ...
  return (
    <div onClick={() => { setTitle("xxx"); }}>
      <ChildComponent />
    </div>
  );
};

但实际上 ChildComponent 并不需要重新渲染,可以将 ChildComponent 当作一个 children:

const ComponentWithTitle = ({ children }) => {
  const [title, setTitle] = useState("");

  // ...

  return (
    <div onClick={() => { setTitle("xxx"); }}>
      {/* children 不会触发渲染 */}
      {children}
    </div>
  )
}


const Component = () => {

  return (
    <ComponentWithTitle>
      <ChildComponent />
    </ComponentWithTitle>
  )
}

这样当setValue时 ComponentWithTitle 就会触发渲染,而传入的 children 组件并不会受到影响。

除了当做 children 还可以当作 props 传入,也不会触发渲染:

const ComponentWithTitle = ({ customComponet }) => {
  const [title, setTitle] = useState("");

  // ...

  return (
    <div onClick={() => { setTitle("xxx"); }}>
      {/* customComponet 不会触发渲染 */}
      {customComponet}
    </div>
  )
}


const Component = () => {
  return (
    <ComponentWithTitle customComponet={<ChildComponent />} />
  )
}

React.memo

使用 React.memo 可以阻止组件重新渲染,除非组件的 props 发生了变化

const ChildComponent = () => <div></div>
const ChildMemo = useMemo(ChildComponent)

const MainComponent = () => {
  return <ChildMemo />
}

如果被 memo 包裹的组件传入一个未被 memo 的组件则不起作用,对象类型会在每次父组件渲染时都会变化,进而引起子组件重新渲染,例如下面代码当 MainComponent 触发渲染时 MyAComponent 和 MyBComponent 都会触发渲染:

const ChildComponent = () => <div></div>
const ChildMemo = React.memo(ChildComponent)

const MainComponent = () => {
  return (
    // 当 MainComponent 重新渲染 MyAComponent 和 MyBComponent 也会重新渲染
    // 所以 ChildMemo 不应该用 memo 包裹
    <ChildMemo customComponet={<MyAComponent />}>
      <MyBComponent />
    </ChildMemo>
  )
}

所以需要同样将 MyAComponent 和 MyBComponent 用 memo 包裹,这样 MainComponent 重新渲染时,MyAMemo 和 MyBMemo 就不会触发:

const MyAMemo = React.memo(MyAComponent)
const MyBMemo = React.memo(MyBComponent)

const MainComponent = () => {
  return (
    <ChildComponent customComponet={<MyAMemo />}>
      <MyBMemo />
    </ChildComponent>
  )
}

useMemo

将子组件的 props 用 usememo 包装是不能避免 ChildComponent 重新渲染的:

const Component = () => {
  const value = useMemo(() => { someKey: someValue })
  // MainComponent 重新渲染时 ChildComponent 也会触发重新渲染 
  return <ChildComponent value={value} />
}

如果子组件被 React.memo 包装,那么子组件所有的引用类型值可以做缓存处理,否则 MainComponent 重新渲染时也会触发子组件渲染:

const ChildMemo = useMemo(ChildComponent)

const Component = () => {
  const objValue = useMemo(() => { someKey: someValue })
  return <ChildMemo value={objValue} />
}

如果父组件的hooks依赖引用类型,也可以做 memo 处理否则每次都会触发 effect:

const Component = () => {
  const objValue = useMemo(() => { someKey: someValue })

  useEffect(() => {
    // ...  
  }, [objValue])

  return <ChildComponent />
}

可以在组件内部用 useMemo 缓存子组件:

const Component = () => {
  const ChildMemo = useMemo(() => {
    return <ChildComponent />
  }, [])
  
  return <ChildMemo />
}

Context 引起的重新渲染

在父组件里定义了 context 时,当父组件重新渲染,所有 context 下的组件都会重新渲染,可以通过划分数据方法避免部分重新渲染:

const ContextData1 = createContext<number>(0);
const ContextData2 = createContext<string>(0);

const Provider = () => {
  const [dataA, setDataA] = useState(0)
  const [dataB, setDataB] = useState(0)

  return (
    <DataAContext.Provide value={dataA}>
      <DataBContext.Provide value={dataB}>
        <button onClick={() => setDataA(dataA + 1)}>
          setDataA
        </button>
        <button onClick={() => setDataA(dataA + 2)}>
          setDataB
        </button>

        {children}
      </DataBContext.Provide>
    </DataAContext.Provide>
  )
}

const App = () => {
  return (
    <Provider>
      <Child1 />
      <Child2 />
    </Provider>
  );
};

const data1Store = () => useContext(ContextData1);
const data2Store = () => useContext(ContextData2);

// 只有修改 dataA 时才会触发渲染
const Child1 = () => {
  const num = data1Store();
  return <>{num}</>;
};

// 只有修改 dataB 时才会触发渲染
const Child2 = () => {
  const num = data2Store();
  return <>{num}</>;
};

当点击 setDataA 修改值时,Child2 组件不会触发重新渲染,当点击 setDataB 修改值时,Child1 组件不会触发重新渲染。

防抖和节流

可以利用 ahooks 的 useThrottleEffect 或 useDebounceEffect 代替 useEffect 减少多次触发渲染:

const Component = () => {
	useDebounceEffect(
    () => {
      // ...
    },
    [objValue],
    { wait: 100 }
  )

	return <div></div>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant