React 函数式组件优化
Posted 大前端菁英荟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 函数式组件优化相关的知识,希望对你有一定的参考价值。
前言
React
推出后,先后出现了三种定义组件的方式,分别是:
函数式组件
React.createClass
创建组件React.Component
创建组件
相信大家在日常中使用的最多的还是函数式组件和 React.Component
组件吧,今天就简单的说下函数式组件的两个优化方法。
函数式组件
什么是函数式组件
在谈到函数式组件之前我们先看一个概念 - 纯函数
。
何为纯函数?
引用一段维基百科的概念。
在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:
此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
可以看到,纯函数有着相同的输入必定产生相同的输出,没有副作用的特性。
同理,函数式组件的输出也只依赖于 props
和 context
,与 state
无关。
函数式组件的特点
没有生命周期
无组件实例,没有
this
(相信很多同学被this
烦过)没有内部状态(
state
)
函数式组件的优点
不需要声明
class
,没有constructor
、extends
等代码,代码简洁,占用内存小。不需要使用
this
可以写成无副作用的纯函数。
更佳的性能。函数式组件没有了生命周期,不需要对这部分进行管理,从而保证了更好地性能。
函数式组件的缺点
没有生命周期方法。
没有实例化。
没有
shouldComponentUpdate
,不能避免重复渲染。
React.memo
一个例子
这里先看一个例子,代码如下,也可点击这里https://codesandbox.io/s/memo-demo1-hcdq3进行查看。
import * as React from "react";
import { render } from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>React.memo demo-1</div>
<button onClick={onClick}>update {n}</button>
<Child />
</div>
);
}
const Child = () => {
console.log("render child");
return <div className="child">Child Component</div>;
};
const rootElement = document.getElementById("root");
render(<App />, rootElement);
它实现了什么?就一个很简单的东西,一个父组件包了一个子组件。
这个大家判断一下,当我点击父组件的 button
更新 N
的时候,子组件中的 log
会不会执行?
按照一般的思维来看,你父组件更新关我子组件什么事?我子组件的 DOM
又不用更新,既然没有更新,那还打什么 log
。
但实际效果是,每点击一次 button
,子组件的 log
就会执行一次,虽然子组件的 DOM
没更新,但并不代表子组件的渲染函数没有执行。
以下是执行的效果图。
优化
针对上述情况,class
组件可以使用 shouldComponentUpdate
来进行优化,但是函数式组件呢?React
同样也提供了一个优化方法,那就是 React.memo
。
memo
即 memorized
,意思是记住。如果输入的参数没有变,依据纯函数的定义,输出也不会变,那么直接返回之前记忆的结果不就行了。
const Child = React.memo(() => {
console.log("render child");
return <div className="child">Child Component</div>;
});
将 Child
使用 React.memo
处理一下,这样的话,无论你点击多少次父组件的 button
,子组件的 log
都不会执行。
完整代码点这里 https://codesandbox.io/s/memo-demo2-38h58。
效果图如下:
React.useCallback
我们将上述代码稍微改一下, 让 Child
接受一个匿名函数,看看会产生什么后果。完整代码点这里 https://codesandbox.io/s/usecallback-demo1-m6exh 。
import * as React from "react";
import { render } from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>React.useCallback demo-1</div>
<button onClick={onClick}>update {n}</button>
<Child onClick={() => {}} />
</div>
);
}
const Child = React.memo((props: { onClick: () => void }) => {
console.log("render child");
return <div className="child">Child Component</div>;
});
const rootElement = document.getElementById("root");
render(<App />, rootElement);
观察代码可以看到也没啥变化嘛,只是子组件接受了一个空函数。
那么问题又来了,这次点击 button
子组件的 log
会执行吗?
看到这里各位同学应该会想,每次都传一个空的匿名函数,props
也没变啊,那就不用重新渲染呗。具体结果如何,来看下效果:
可以看到每次点击 button
时,子组件的 log
依旧会再次执行。那么这是为什么呢?
因为每次点击 button
更新父组件的时候,会重新生成一个空的匿名函数,虽然它们都是空的匿名函数,但是它们不是同一个函数。
优化
那么怎么保证子组件每次都接受同一个函数呢?
很简单。既然父组件在更新的时候会重新生成一个函数,那么我把函数放到父组件外面不就可以了嘛,这样父组件在更新的时候子组件就会接受同一个函数。
代码如下。也可点击这里 https://codesandbox.io/s/usecallback-demo2-ly4c4 查看。
import * as React from "react";
import { render } from "react-dom";
import "./styles.css";
const childOnClick = () => {};
function App() {
const [n, setN] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>React.useCallback demo-2</div>
<button onClick={onClick}>update {n}</button>
<Child onClick={childOnClick} />
</div>
);
}
const Child = React.memo((props: { onClick: () => void }) => {
console.log("render child");
return <div className="child">Child Component</div>;
});
const rootElement = document.getElementById("root");
render(<App />, rootElement);
效果图如下:
这样子看起来好像解决了子组件每次都接受不同的函数导致重新渲染的问题,但是好像哪里不对劲,实现也不优雅。
缺点
如果子组件的函数依赖父组件里面的值,那么这种方式就不可行。
怎么办呢?如果能将函数也 memorized
就好了。
Hook
React
在 16.8.0
的版本中正式推出了 Hooks
,其中有一个 Hook
叫做 useCallback
,它能将函数也 memorized
化。
useCallback
接受两个参数,第一个参数是一个函数,第二个参数是一个依赖数组,返回一个 memorized
后的函数。只有当依赖数组中的值发生了变化,它才会返回一个新函数。
看看使用 useCallback
后的代码,也可以点击这里 https://codesandbox.io/s/usecallback-demo3-77wf6 查看。
import * as React from "react";
import { render } from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const childOnClick = React.useCallback(() => {
console.log(`m: ${m}`);
}, [m]);
return (
<div className="App">
<div>React.useCallback demo-3</div>
<button
onClick={() => {
setN(n + 1);
}}
>
update n: {n}
</button>
<button
onClick={() => {
setM(m + 1);
}}
>
update m: {m}
</button>
<Child onClick={childOnClick} />
</div>
);
}
const Child = React.memo((props: { onClick: () => void }) => {
console.log("render child");
return (
<div className="child">
<div>Child Component</div>
<button onClick={props.onClick}>log m</button>
</div>
);
});
const rootElement = document.getElementById("root");
render(<App />, rootElement);
在上述代码中,子组件接受一个使用了 useCallback
的函数,它的依赖参数是 m
,只有当 m
发生了变化,子组件接受的函数才会是一个重新生成的函数。也就是说,无论点击多少次更新 n
的 button
,子组件都不会更新,只有点击更新 m
的 button
时,子组件才会更新。
看看效果如何:
实际效果符合我们的预期。
歪心思
看到这里有些同学就会想了,如果使用 useCallback
的时候,传一个空数组作为依赖数组,那么子组件就不再受父组件的影响了,即使你父组件的 m
变化了,我子组件依旧不会重新渲染,这样子岂不是性能更好?话不多说,我们来测试一下就好了。代码点击这里 https://codesandbox.io/s/usecallback-demo4-r5bqt 查看,效果如下:
可以看到,虽然子组件确实没重复渲染了,但同样的也导致一个问题,打印出来的 m
永远都是 0,再也读取不到更新后的 m
的值。
由此可以得出结论,传一个空数组作为依赖数组的后果就是,子组件接受的函数里面的参数永远都是初始化使用 useCallback
时的值,这样的结果并不是我们想要的。
所以歪心思还是少来了。
总结
随着 React
正式推出 Hooks
,带来一系列新的特性,极大地增强了函数式组件的功能,利用这些新特性可以实现和 class
组件一样的效果。
有了 React Hooks
,我们可以抛弃沉重的 class
组件,使用更加轻便,性能更加优异的函数式组件,因此掌握一些函数式组件的优化方法对我们使用函数式组件开发是非常有用处的。
React Hooks
不管你香不香,反正我是先香了。
默默说一句,Vue 3.0
也会推出函数式组件。
以上是关于React 函数式组件优化的主要内容,如果未能解决你的问题,请参考以下文章