在 React 中,随着 Hook 的广泛使用,函数组件逐渐代替 class 组件,通过官方提供的 Hook API,可以优化性能:1、在函数组件中,经常会碰到大计算量的函数 fn,如果 fn 不是每次重新渲染都必须执行,可以使用 useMemo 减少 fn 的执行次数;2、在 React 中,父组件的重新渲染会导致子组件的重新渲染,但是如果子组件的数据没有发生改变,子组件的重新渲染是一种性能的浪费,可以使用 React.memo + useCallback + useMemo 的方法阻止子组件不必要的重新渲染;3、执行 useState 会返回当前 state 以及更新 state 的函数,执行更新函数,会引发组件及其子组件的重新渲染,如果本轮渲染,更新函数返回的值与上一次返回的相同,子组件的重新渲染是一种性能的浪费,可以通过使用 useReducer 代替 useState 来避免这种情况发生。
一、使用 useMemo 减少大计算量函数的执行次数
通过代码,了解 useMemo 的用法。
let preValue = null;
function App() {
const [count, setCount] = useState(0);
const nowValue = useMemo(() => {
console.log('参数函数执行了')
return { name: 'superlu' }
}, []);
console.log('nowValue: ', nowValue);
console.log('nowValue === preValue ', nowValue === preValue);
preValue = nowValue;
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
</>
);
}
结果如下:

代码的思路是比较本次渲染和上次渲染,useMemo 返回的对象是否为同一个对象。从结果可以看出,本次渲染和上次渲染,useMemo 返回的是同一个对象。useMemo 接受 2 个参数,第一个参数是函数,第二个参数是数组,返回参数函数的执行结果,当传入空数组时,useMemo 只会执行一次参数函数,下次渲染时,useMemo 不会执行参数函数,而是直接将上一次渲染,参数函数的执行结果返回。数组中也可以有依赖项,只有在下一次渲染,依赖项的值发生改变,useMemo 才会执行参数函数,返回参数函数的执行结果,如果在下一次渲染,依赖项的值未发生改变,useMemo 不会执行参数函数,而是直接将上一次渲染,参数函数的执行结果返回。如果一个函数 fn 耗时较长,并且不必在每次渲染时都执行,那么我们可以使用 useMemo 进行优化,减少函数 fn 的执行次数。
二、使用 React.memo 阻止子组件不必要的重新渲染
React.memo 可以阻止子组件不必要的重新渲染,可以和 useCallback、useMemo 一起使用。下面通过代码,了解 useCallback 和 React.memo 的用法。
2.1、useCallback
通过 2 段代码,了解 useCallback 的用法。
第一段代码:
let preFunc = null;
function App() {
const [count, setCount] = useState(0);
const nowFunc = useCallback(() => console.log('我是 useCallback 返回的函数'), []);
nowFunc();
console.log('preFunc === nowFunc ', nowFunc === preFunc);
preFunc = nowFunc;
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
</>
);
}
结果如下:

代码的思路是比较本次渲染和上次渲染,useCallback 返回的函数是否为同一个函数。从结果可以看出,本次渲染和上次渲染,useCallback 返回的是同一个函数。useCallback 接受 2 个参数,第一个参数是函数,第二个参数是数组,返回一个函数,当传入空数组时,useCallback 在每次渲染时返回的函数是相同的。数组中也可以有依赖项,只有本次渲染和上次渲染,依赖项的值未发生改变,useCallback 返回的才是同一个函数,一旦依赖项的值发生改变,useCallback 将会返回一个新的函数,下面通过代码了解这一点。
第二段代码:
let preFunc = null;
function App() {
const [count, setCount] = useState(0);
const nowFunc = useCallback(() => console.log('我是 useCallback 返回的函数'), [count]);
nowFunc();
console.log('preFunc === nowFunc ', nowFunc === preFunc);
preFunc = nowFunc;
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
</>
);
}
结果如下:

从结果可以看出,由于依赖项 count 在每次渲染时,值已经发生改变,所以 useCallback 在每次渲染时都返回一个新的函数。
2.2、React.memo
首先 React.memo 是高阶组件,也就是 React.memo 函数接受一个组件参数,返回一个组件。下面通过 4 段代码,了解 React.memo 的用法。
第一段代码:
function AppChild({ text }) {
console.log('AppChild render');
return (
<div>{ text }</div>
);
}
function App() {
const [count, setCount] = useState(0);
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<br />
<br />
<AppChild
text='我是 App 的子组件'
/>
</>
);
}
结果如下:

从图中可以看出,父组件 App 的数据发生变化,导致父组件重新渲染,导致子组件 AppChild 重新渲染,然而子组件的数据并没有发生变化,子组件的重新渲染造成性能浪费。
第二段代码:
function AppChild({ text }) {
console.log('AppChild render');
return (
<div>{ text }</div>
);
}
const AppChildMemo = React.memo(AppChild);
function App() {
const [count, setCount] = useState(0);
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<br />
<br />
<AppChildMemo
text='我是 App 的子组件'
/>
</>
);
}
结果如下:

从图中可以看出,父组件 App 的重新渲染,没有引起子组件 AppChildMemo 的重新渲染,从而优化了性能。从这里可以看出,React.memo 接受一个组件参数,返回一个组件,返回的组件,在父组件发生重新渲染时,可以不进行重新渲染,但这是有条件的,接下来将通过代码了解这个条件。
第三段代码:
function AppChild({ text }) {
console.log('AppChild render');
return (
<div>{ text }</div>
);
}
const AppChildMemo = React.memo(AppChild);
function App() {
const [count, setCount] = useState(0);
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<br />
<br />
<AppChildMemo
text='我是 App 的子组件'
testFunc={ () => {} }
testObj={ {} }
/>
</>
);
}
结果如下:

从图中可以看出,父组件 App 的重新渲染,造成了子组件 AppChildMemo 的重新渲染,这是因为,React.memo 阻止 AppChildMemo 重新渲染的条件是,在本轮渲染中,AppChildMemo 的 props 必须和上一次渲染 props 的值相同。通过代码可以看出,前后 2 次渲染,text 的值是相同的,但是 testFunc、testObj 的值是不相同的,所以,React.memo 不能阻止子组件 AppChildMemo 的重新渲染。对于 testFunc、testObj 这类复杂类型,可以使用 useCallback、useMemo 让 textFunc、textObj 每次渲染时,值都相同。
第四段代码:
function AppChild({ text }) {
console.log('AppChild render');
return (
<div>{ text }</div>
);
}
const AppChildMemo = React.memo(AppChild);
function App() {
const [count, setCount] = useState(0);
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<br />
<br />
<AppChildMemo
text='我是 App 的子组件'
testFunc={ useCallback(() => {}, []) }
testObj={ useMemo(() => ({}), []) }
/>
</>
);
}
结果如下:

从结果可以看出,即使子组件 AppChildMemo 的 props 中存在复杂类型,也可以通过 React.memo + useCallback + useMemo 的方法阻止子组件的不必要的重新渲染。
2.3、React.memo 无法阻止 Context 引起的渲染
React.memo 无法阻止 Context 引起的重新渲染,通过代码了解这一点。
const ThemeContext = React.createContext('red');
function App() {
const [color, setColor] = useState('blue');
return (
<ThemeContext.Provider value={color}>
<button onClick={ () => setColor('green') }>change color</button>
<br />
<br />
<AppChildMemo />
</ThemeContext.Provider>
)
}
const AppChildMemo = React.memo(AppChild);
function AppChild() {
console.log('AppChild render');
const color = useContext(ThemeContext);
return (
<div style={{ background: color }}>我是 App 的子组件</div>
);
}
结果如下:

子组件 AppChild 通过 useContext 获取 context 的值,当 context 的值发生变化时,从图中可以看出,React.memo 也无法阻止子组件 AppChild 的重新渲染。
三、使用 useReducer 替代 useState 提高性能
通过 2 段代码,了解 useReducer 的使用。
第一段代码:
function reducer(state, action) {
switch(action.type) {
case 'increment':
return state + 1;
}
}
function App() {
// 副作用
useEffect(() => {
console.log('App effect');
});
const [state, dispatch] = useReducer(reducer, 0);
return (
<>
Count: {state}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<br />
<br />
<AppChild />
</>
);
}
function AppChild() {
console.log('CounterChild render');
return (
<div>我是 App 的子组件</div>
);
}
结果如下:

useReducer 接受 2 个参数,第一个参数是函数,第二个参数是初始值,返回一个 state 和 dispatch 函数,每次渲染,useReducer 返回的 dispatch 函数是相同的,在第一次渲染时,state 为传给 useReducer 的初始值,从图中的结果可知,执行 dispatch 会引起组件、子组件的重新渲染,以及组件副作用的执行,重新渲染时,state 的值为 reducer 函数执行返回的结果。执行 dispatch 引起子组件重新渲染及组件副作用的执行是有条件的,下面将通过代码了解这个条件。
function reducer(state, action) {
switch(action.type) {
case 'keep':
return state;
}
}
function App() {
console.log('App render')
// 副作用
useEffect(() => {
console.log('App effect');
});
const [state, dispatch] = useReducer(reducer, 0);
return (
<>
Count: {state}
<button onClick={() => {
console.log('click happen')
dispatch({type: 'keep'})
}}>
keep
</button>
<br />
<br />
<AppChild />
</>
);
}
function AppChild() {
console.log('CounterChild render');
return (
<div>我是 App 的子组件</div>
);
}
结果如下:

从结果中可以看出,dispatch 执行,App 的副作用没有执行,App 的子组件 AppChild 没有重新渲染,dispatch 执行,引起组件副作用执行和子组件重新渲染的条件是,reducer 函数返回的结果与上一次返回的结果不同。可以发现,执行 dispatch,App 也没有重新渲染,但是这是不确定事件,App 仍然有可能渲染。
四、Hook 总结
初学 Hook,将 Hook 的知识点总结为 3 方面:1、Hook 规则;2、自定义 Hook;3、Hook API。
4.1、Hook 规则
- 在 React 函数组件的最顶层调用 Hook(Hook API 或自定义 Hook)。
- 在自定义 Hook 函数的最顶层调用其他 Hook(Hook API 或自定义 Hook)。
4.2、自定义 Hook
下面通过 2 段代码了解自定义 Hook。
第一段代码:
function App() {
useEffect(() => {
console.log('App render');
})
return (
<>
</>
);
}
第二段代码:
// 自定义 Hook
function useCustomize() {
useEffect(() => {
console.log('App render');
})
}
function App() {
useCustomize();
return (
<>
</>
);
}
通过对比 2 段代码,可知,自定义 Hook 只是将函数组件中的部分逻辑抽离出来,封装成函数,达到代码复用的目的。封装自定义 Hook 函数需要注意 2 点:1、自定义 Hook 函数名以 use 开头;2、自定义 Hook 函数可以调用 Hook API 或其他自定义 Hook 函数,但是必须在自定义 Hook 函数的最顶层调用。
4.3、Hook API
目前有 10 个 Hook API,接下来将了解 useEffect 和 useRef 这 2 个 API。
4.3.1、useEffect
useEffect 和 useMemo 一样,接受 2 个参数,第一个参数是函数,第二个参数是数组,数组中的项为依赖项。了解 useEffect,需要了解参数函数的执行时机,下面通过 4 段代码,了解参数函数的执行时机。
第一段代码:
function App() {
console.log('App render')
const [count, setCount] = useState(0);
useEffect(() => {
console.log('App effect happen');
return () => {
console.log('App destroy happen');
};
})
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
</>
);
}
结果如下:

参数函数可以返回一个函数,这个函数称为清除函数,从结果可知,在首次渲染时,参数函数会在组件渲染后执行,重新渲染时,先执行清除函数,再执行参数函数。在这段代码中,没有给 useEffect 传入第二个参数,在下段代码中,给 useEffect 传入空数组。
第二段代码:
function App() {
const [count, setCount] = useState(0);
const [visible, setVisible] = useState(true);
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<button onClick={
() => setVisible(false)
}>
destory
</button>
<br />
<br />
{
visible && <AppChild />
}
</>
);
}
function AppChild() {
useEffect(() => {
console.log('AppChild effect happen');
return () => {
console.log('AppChild destory happen');
};
}, []);
return (
<div>我是 App 的子组件</div>
);
}
结果如下:

从图中结果可知,当 useEffect 传入空数组时,参数函数只会在组件首次渲染时执行,清除函数只会在组件销毁时执行。当数组有依赖项时,只有当依赖项的值改变,组件重新渲染才会执行清除函数(先)和参数函数(后)。通过下一段代码了解父子组件间,参数函数的执行顺序。
第三段代码:
function App() {
const [count, setCount] = useState(0);
const [visible, setVisible] = useState(true);
useEffect(() => {
console.log('App effect happen');
return () => {
console.log('App destory happen');
};
})
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<button onClick={
() => setVisible(false)
}>
destory
</button>
<br />
<br />
{
visible && <AppChild />
}
</>
);
}
function AppChild() {
useEffect(() => {
console.log('AppChild effect happen');
return () => {
console.log('AppChild destory happen');
};
});
return (
<div>我是 App 的子组件</div>
);
}
结果如下:

通过结果可知,每次渲染,子组件 useEffect 中的参数函数先于父组件的执行。当组件树销毁时,组件树中组件清除函数的执行顺序通过下面一段代码了解。
第四段代码:
function App() {
const [count, setCount] = useState(0);
const [visible, setVisible] = useState(true);
useEffect(() => {
console.log('App effect happen');
return () => {
console.log('App destory happen');
};
})
return (
<>
{ count }
<button onClick={
() => setCount(count + 1)
}>
+ 1
</button>
<button onClick={
() => setVisible(false)
}>
destory
</button>
<br />
<br />
{
visible && <AppChild />
}
</>
);
}
function AppChild() {
useEffect(() => {
console.log('AppChild effect happen');
return () => {
console.log('AppChild destory happen');
};
});
return (
<>
<div>我是 App 的子组件</div>
<AppChildChild />
</>
);
}
function AppChildChild() {
useEffect(() => {
console.log('AppChildChild effect happen');
return () => {
console.log('AppChildChild destory happen');
};
});
return (
<div>我是 App 子组件的子组件</div>
);
}
结果如下:

通过结果可知,组件树销毁时,组件树中父组件的清除函数先于子组件的执行。通过这 4 段代码可以总结出:1、对于一个组件,首次渲染后,执行传入 useEffect 的参数函数,重新渲染,如果传入 useEffect 的数组的依赖项的值发生改变或没有给 useEffect 传入第二个参数,清除函数先于函数参数执行,组件销毁,执行清除函数;2、对于一个组件树,每次渲染,子组件的参数函数先于父组件的参数函数执行,组件树销毁时,组件树中的父组件的清除函数先于子组件的执行。
4.3.2、useRef
useRef 可以用来获取 DOM 节点或 class 组件实例,另外,因为每次渲染,useRef 返回同一个对象,可以在这个对象上添加变量,用于一个组件渲染间通信。
五、总结
本文介绍了用 Hook 优化性能的方法及总结了 Hook 的知识点,对 Hook 进一步的学习理解将会更新在本文中。
六、更新日志
2020-05-31 发布