这是我参与更文挑战的第 10 天,活动详情查看:更文挑战
前言
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
Hook 和函数式组件让我们更加方便地开发 React 应用。本文将介绍函数式组件性能优化的几个方法。
减少 render 次数
使用 React.memo
React v16.6.0 提供了 React.memo()
这个 API 来解决前后 props 相同组件却仍然 render 的问题。
如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在
React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
使用示例如下(示例来自官方文档):
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
复制代码
React.memo 的注意点:
React.memo
仅检查 props 变更。如果函数式组件内部有 useState、useReducer 和 useReducer 的 Hook,当 context 发生变化时,它仍会重新渲染。React.memo
只对 props 复杂对象做浅比较。如果要自定义比较,需要传入比较函数作为第二个参数。
使用示例如下(示例来自官方文档):
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
复制代码
使用 useCallBack、useMemo 缓存传给子组件的 props
现有下面的代码:
// 父组件
export default function App() {
const [appCount, setAppCount] = useState(0);
const [childCount, setChildCount] = useState(0);
const handleAppAdd = () => {
setAppCount(appCount + 1);
};
const handleChildAdd = () => {
setChildCount(childCount + 1);
};
console.log('app render');
return (
<div>
<h2>App</h2>
<button onClick={handleAppAdd}>appCount + 1</button>
<p>appCount: {appCount}</p>
<h2>child</h2>
<button onClick={handleChildAdd}>childCount + 1</button>
<Child value={childCount} />
</div>
);
}
// child 组件
import React from 'react';
const Child = props => {
console.log('child render');
const { value } = props;
return <div>Child: {value}</div>;
};
export default React.memo(Child);
复制代码
上面的 Child
组件已经使用 React.memo
优化,当仅有父组件 appCount
变化时, Child
组件不会重新渲染。
现在我们在改造 Child
组件:
import React from 'react';
const Child = props => {
console.log('child render');
const { value, handleAdd } = props;
return <div>
<p>Child: {value}</p>
<button onClick={handleAdd}>btn in child: childCount + 1</button>
</div>;
};
export default React.memo(Child);
复制代码
上面的代码中我们在子组件也加一个可以增减 ChildCount
的按钮,点击它时调用父组件中传入的 props.handleAdd
。
// app.jsx
export default function App() {
const [appCount, setAppCount] = useState(0);
const [childCount, setChildCount] = useState(0);
const handleAppAdd = () => {
setAppCount(appCount + 1);
};
const handleChildAdd = () => {
setChildCount(childCount + 1);
};
console.log('app render');
return (
<div>
<h2>App</h2>
<button onClick={handleAppAdd}>appCount + 1</button>
<p>appCount: {appCount}</p>
<h2>child</h2>
<button onClick={handleChildAdd}>childCount + 1</button>
<Child value={childCount} handleAdd={handleChildAdd} />
</div>
);
}
复制代码
上面的代码给 Child
传入了 handleAdd
的 props。然而当我们再次点击 appCount + 1
时,虽然 childCount
未变化,但是 Child
却刷新了,日志如下:
从上面的情况我们可以分析出,是新传入 Child
的props.handleAdd
导致了重新渲染。因为 handleAdd
在 App
组件中每次都是重新生成的一个新函数,React.memo
浅比较 props 改变,Child
重新 render。
那么我们怎么解决 props 上存在函数的问题呢?React.useCallback 可以解决这个问题。
把内联回调函数及依赖项数组作为参数传入
useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
。
由官方文档可以看出,只要 deps 未改变, useCallback
返回的还是同一个函数。同样地 ,如果 props 是复杂对象, 我们也可以使用 useMemo
来处理。
上面 child
重复 render 的问题, 我们如下处理:
// App.js
const handleChildAdd = useCallback(() => {
setChildCount(childCount + 1);
}, [childCount]);
复制代码
占位组件的 render 优化
我们通常有一这样的需求,比如子组件有一个占位区域,占位区域的组件需要通过 props 传入,我们可以通过在父组件中创建好 React.Element
来减少占位组件的渲染次数。
代码如下:
const Content = () => {
console.log('Content render');
return <div>content</div>;
};
// 优化前
export default function App() {
return <Child content={Content} />;
}
// 优化后
export default function App() {
return <Child content={<Content />} />;
}
复制代码
上面的代码中, 我们要注意 Child
组件传入 content
的方式。
- 优化前:传入的是 Content 组件
- 优化后:传入的是
<Content />
形式的React.Element
通过在父组件件内部使用 <Content />
创建 React.Element
,这样可以避免子组件自身 render 重新创建 Content
的 React.Element
。
React.Context 读写分离
未分离前:
const LogContext = React.createContext();
function LogProvider({ children }) {
const [logs, setLogs] = useState([]);
const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]);
return (
<LogContext.Provider value={{ logs, addLog }}>
{children}
</LogContext.Provider>
);
}
作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/6889247428797530126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码
当 Provider 的
value
值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
优化前的代码中,只要 setLogs
更新了状态, value 就会传入新的值。所有用到 logContext
对应的函数都会被更新。
优化后的代码:
function LogProvider({ children }) {
const [logs, setLogs] = useState([]);
const addLog = useCallback((log) => {
setLogs((prevLogs) => [...prevLogs, log]);
}, []);
return (
<LogDispatcherContext.Provider value={addLog}>
<LogStateContext.Provider value={logs}>
{children}
</LogStateContext.Provider>
</LogDispatcherContext.Provider>
);
}
作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/6889247428797530126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码
上面我们通过 LogDispatcherContext
和 LogStateContext
实现读写分离。只需要使用 addLog
的组件就不会因为 logs
改变而刷新了。