React函数式组件性能优化指南
前⾔
⽬的
本⽂只介绍函数式组件特有的性能优化⽅式,类组件和函数式组件都有的不介绍,⽐如 key 的使⽤。另外本⽂不详细的介绍 API 的使⽤,后⾯也许会写,其实想⽤好 hooks 还是蛮难的。
⾯向读者
有过 React 函数式组件的实践,并且对 hooks 有过实践,对 useState、useCallback、useMemo API ⾄少看过⽂档,如果你有过对类组件的性能优化经历,那么这篇⽂章会让你有种熟悉的感觉。
React 性能优化思路
我觉得 React 性能优化的理念的主要⽅向就是这两个:
1. 减少重新 render 的次数。因为在 React ⾥最重(花时间最长)的⼀块就是 reconction(简单的可以理解为 diff),如果不 render,就
不会 reconction。
2. 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执⾏函数调⽤。
在使⽤类组件的时候,使⽤的 React 优化 API 主要是:shouldComponentUpdate和 PureComponent,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少⽗组件更新⽽⼦组件也更新的情况,虽然也可以在 state 更新的时候阻⽌当前组件渲染,如果要这么做的话,证明你这个属性不适合作为 state,⽽应该作为静态属性或者放在 class 外⾯作为⼀个简单的变量 。
但是在函数式组件⾥⾯没有声明周期也没有类,那如何来做性能优化呢?
<
⾸先要介绍的就是 ,这个 API 可以说是对标类组件⾥⾯的 PureComponent,这是可以减少重新 render 的次数的。
可能产⽣性能问题的例⼦
举个 ,⾸先我们看两段代码:
在根⽬录有⼀个 index.js,代码如下,实现的东西⼤概就是:上⾯⼀个 title,中间⼀个 button(点击 button 修改 title),下⾯⼀个⽊偶组件,传递⼀个 name 进去。
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from './child'
function App() {
const [title, setTitle] = useState("这是⼀个 title")
return (
<div className="App">
<h1>{ title }</h1>
<button onClick={() => setTitle("title 已经改变")}>改名字</button>
<Child name="桃桃"></Child>
</div>
);
}
const rootElement = ElementById("root");
在同级⽬录有⼀个 child.js
// child.js
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default Child
当⾸次渲染的时候的效果如下:
image-20191030221223045
并且控制台会打印"桃桃”,证明 Child 组件渲染了。
接下来点击改名字这个 button,页⾯会变成:
image-20191030222021717
title 已经改变了,⽽且控制台也打印出"桃桃",可以看到虽然我们改的是⽗组件的状态,⽗组件重新渲
染了,并且⼦组件也重新渲染了。你可能会想,传递给 Child 组件的 props 没有变,要是 Child 组件不重新渲染就好了,为什么会这么想呢?
我们假设 Child 组件是⼀个⾮常⼤的组件,渲染⼀次会消耗很多的性能,那么我们就应该尽量减少这个组件的渲染,否则就容易产⽣性能问题,所以⼦组件如果在 props 没有变化的情况下,就算⽗组件重新渲染了,⼦组件也不应该渲染。
那么我们怎么才能做到在 props 没有变化的时候,⼦组件不渲染呢?
答案就是⽤ 在给定相同 props 的情况下渲染相同的结果,并且通过记忆组件渲染结果的⽅式来提⾼组件的性能表现。 的基础⽤法
把声明的组件通过包⼀层就好了,其实是⼀个⾼阶函数,传递⼀个组件进去,返回⼀个可以记忆的组件。
function Component(props) {
/* 使⽤ props 渲染 */
}
const MyComponent = (Component);
那么上⾯例⼦的 Child 组件就可以改成这样:
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export (Child)
通过 包裹的组件在 props 不变的情况下,这个被包裹的组件是不会重新渲染的,也就是说上⾯那个例⼦,在我点击改名字之后,仅仅是 title 会变,但是 Child 组件不会重新渲染(表现出来的效果就是 Child ⾥⾯的 log 不会在控制台打印出来),会直接复⽤最近⼀次渲染的结果。
这个效果基本跟类组件⾥⾯的 PureComponent效果极其类似,只是前者⽤于函数组件,后者⽤于类组
件。
< ⾼级⽤法
默认情况下其只会对 props 的复杂对象做浅层对⽐(浅层对⽐就是只会对⽐前后两次 props 对象引⽤是否相同,不会对⽐对象⾥⾯的内容是否相同),如果你想要控制对⽐过程,那么请将⾃定义的⽐较函数通过第⼆个参数传⼊来实现。
function MyComponent(props) {
/* 使⽤ props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传⼊ render ⽅法的返回结果与
将 prevProps 传⼊ render ⽅法的返回结果⼀致则返回 true,
否则返回 false
*/
}
export (MyComponent, areEqual);
此部分来⾃于 React 官⽹[1]。
如果你有在类组件⾥⾯使⽤过 `shouldComponentUpdate()`[2] 这个⽅法,你会对 的第⼆个参数⾮常的熟悉,不过值得注意的是,如果 props 相等,areEqual会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate⽅法的返回值相反。
useCallback
现在根据上⾯的例⼦,再改⼀下需求,在上⾯的需求上增加⼀个副标题,并且有⼀个修改副标题的 button,然后把修改标题的 button 放到 Child 组件⾥。
把修改标题的 button 放到 Child 组件的⽬的是,将修改 title 的事件通过 props 传递给 Child 组件,然后观察这个事件可能会引起性能问题。
⾸先看代码:
⽗组件 index.js
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("这是⼀个 title");
const [subtitle, setSubtitle] = useState("我是⼀个副标题");
const callback = () => {
setTitle("标题改变了");
};
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
<Child onClick={callback} name="桃桃" />
</div>
);
}
const rootElement = ElementById("root");
⼦组件 child.js
import React from "react";
function Child(props) {
console.log(props);
return (
<>
<button onClick={Click}>改标题</button>
<h1>{props.name}</h1>
</>
);
function怎么记忆}
export (Child);
⾸次渲染的效果
image-20191031235605228
这段代码在⾸次渲染的时候会显⽰上图的样⼦,并且控制台会打印出桃桃。
然后当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出桃桃,这就证明了⼦组件⼜重新渲染了,但是⼦组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?
原因
我们在解决问题的之前,⾸先要知道这个问题是什么原因导致的?
咱们来分析,⼀个组件重新重新渲染,⼀般三种情况:
1. 要么是组件⾃⼰的状态改变
2. 要么是⽗组件重新渲染,导致⼦组件重新渲染,但是⽗组件的 props 没有改版
3. 要么是⽗组件重新渲染,导致⼦组件重新渲染,但是⽗组件传递的 props 改变
接下来⽤排除法查出是什么原因导致的:
第⼀种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;
第⼆种情况好好想⼀下,是不是就是在介绍 的时候情况,⽗组件重新渲染了,⽗组件传递给⼦组件的 props 没有改变,但是⼦组件重新渲染了,我们这个时候⽤ 来解决了这个问题,所以这种情况也排除。
那么就是第三种情况了,当⽗组件重新渲染的时候,传递给⼦组件的 props 发⽣了改变,再看传递给 Child 组件的就两个属性,⼀个
是 name,⼀个是 onClick ,name是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发⽣改变呢?在⽂章的开头就已经说过了,在函数式组件⾥每次重新渲染,函数组件都会重头开始重新执⾏,那么这两次创建的 callback 函数肯定发⽣了改变,所以导致了⼦组件重新渲染。
如何解决
到问题的原因了,那么解决办法就是在函数没有改变的时候,重新渲染的时候保持两个函数的引⽤⼀致,这个时候就要⽤
到 useCallback 这个 API 了。
useCallback 使⽤⽅法
const callback = () => {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论