使用useCallback优化代码
useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。
1.原理分析
useCallback是React Hooks中的一个函数,用于优化函数组件的性能。它的作用是返回一个memoized(记忆化的)函数,这个函数只有在依赖项发生变化时才会重新计算,否则会直接返回上一次计算的结果。
useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。
2.案例分析:
父组件定义一个请求函数fetchData,和一个状态query,将query当作fetchData的参数,将该函数传递进子组件,当父组件query发生变化时,让子组件调用该函数发起请求。
class Parent extends Component {state = {query: 'react'};fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query; // ... Fetch data and do something ... }; render() {return <Child fetchData={this.fetchData} />;}
}class Child extends Component {state = {data: null};componentDidMount() {this.props.fetchData();}componentDidUpdate(prevProps) {// 🔴 This condition will never be trueif (this.props.fetchData !== prevProps.fetchData) {this.props.fetchData();}}render() {// ...}
}
在本代码中,fetchData是一个class方法!(或者你也可以说是class属性)它不会因为状态的改变而不同,所以this.props.fetchData和 prevProps.fetchData始终相等,因此不会重新请求。
2.1旧思维–优化该案例:
子组件使用:
componentDidUpdate(prevProps) {this.props.fetchData();
}
这样可以发起请求,但是会在每次渲染后都去请求。
或者改变父组件:
render() {return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
}
但这样一来,this.props.fetchData !== prevProps.fetchData 表达式永远是true,即使query并未改变。这会导致我们总是去请求。(bind() 方法会创建一个新的函数对象)
唯一现实可行的办法是把query本身传入 Child 组件。 Child 虽然实际并没有直接使用这个query的值,但能在它改变的时候触发一次重新请求:
class Parent extends Component {state = {query: 'react'};fetchData = () => {const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;// ... Fetch data and do something ...};render() {return <Child fetchData={this.fetchData} query={this.state.query} />; }
}class Child extends Component {state = {data: null};componentDidMount() {this.props.fetchData();}componentDidUpdate(prevProps) {if (this.props.query !== prevProps.query) { this.props.fetchData(); } }render() {// ...}
}
在class组件中,函数属性本身并不是数据流的一部分。使用useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。
2.2开始使用hooks:
场景一: 使用函数组件
但父组件不使用useCallback处理函数
import React, { useCallback, useState, useEffect } from 'react';
import './App.css';function App() {const [query, setQuery] = useState(1);const [queryOther, setQueryOther] = useState(1);const fecthData = () => {console.log('新的fetch');return query;}const add = () => {console.log('点击add');setQuery(query + 1);}const addOther = () => {console.log('点击addOther');setQueryOther(queryOther + 1);}return (<><Child fecthData={fecthData} /><button onClick={add}>+1</button><button onClick={addOther}>other+1</button><div>{ query }</div></>);
}function Child({ fecthData }: { fecthData: any }) {console.log('子组件相关内容');useEffect(() => {const querN = fecthData();console.log('子组件调用该函数获取到相关内容', querN);}, [fecthData])return <div>123</div>
}export default App;
初始化的时候:
点击按钮:
但是从图里面可以看到,点击addOther时,并没有使得query发生变化,但是子组件仍然调用了该函数发起请求。可以看到这种方法需求可以使得子组件在父组件的状态query发生变化时,成功发起了请求,但是还是存在副作用。
问题的原因在于状态queryOther的改变,使得父组件重新渲染,重新生成了fecthData函数,并返回了该函数新的地址,导致子组件刷新。
场景二:父组件使用useCallback处理函数
import React, { useCallback, useState, useEffect } from 'react';
import './App.css';function App() {const [query, setQuery] = useState(1);const [queryOther, setQueryOther] = useState(1);const fecthData = useCallback(() => {console.log('新的fetch');return query;}, [query])const add = () => {console.log('点击add');setQuery(query + 1);}const addOther = () => {console.log('点击addOther');setQueryOther(queryOther + 1);}return (<><Child fecthData={fecthData} /><button onClick={add}>+1</button><button onClick={addOther}>other+1</button><div>{ query }</div></>);
}function Child({ fecthData }: { fecthData: any }) {console.log('子组件相关内容');useEffect(() => {const querN = fecthData();console.log('子组件调用该函数获取到相关内容', querN);}, [fecthData])return <div>123</div>
}export default App;
初始状态
点击按钮:
可以看到只有点击+1按钮改变query才会使得子组件发起请求,点击other+1已经没有处罚上文副作用。
原因分析:
使用了useCallback,useCallback的工作原理是什么?useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建。在本例子中点击按钮other+1并没有使得query发生变化,所以并没有返回新的fetchData函数地址,又因为在子组件中使用useEffect对fetchData监听时,所以子组件不会发起请求。但是,点击按钮other+1时,子组件虽然没发起请求,但是还是刷新了,这是什么原因呢?这是因为子组件直接在父组件中挂载,没有做过任何优化,当父组件重新渲染时,会导致子组件也跟着渲染。所以单纯的使用useCallback可以监听到相应变化,使得子组件做出变化,但是并不能优化性能。所以当我们不用监听某个状态使得函数发生改变时,不要轻易使用useCallback,因为使用 useCallBack后每次执行到这里内部比对是否变化,还有存一下之前的函数,消耗更大了。
场景三:优化上述问题,搭配React.memo使用
import React, { useCallback, useState, useEffect } from 'react';
import './App.css';function App() {const [query, setQuery] = useState(1);const [queryOther, setQueryOther] = useState(1);const fecthData = useCallback(() => {console.log('新的fetch');return query;}, [query])const add = () => {console.log('点击add');setQuery(query + 1);}const addOther = () => {console.log('点击addOther');setQueryOther(queryOther + 1);}return (<><Child fecthData={fecthData} /><button onClick={add}>+1</button><button onClick={addOther}>other+1</button><div>{ query }</div> ,,mconst Child = React.memo(({ fecthData }: { fecthData: any }) => {console.log('子组件相关内容');useEffect(() => {const querN = fecthData();console.log('子组件调用该函数获取到相关内容', querN);}, [fecthData])return <div>123</div>
})export default App;
初始状态:
点击按钮:
一切问题都解决了。点击other+1按钮,没有使得子组件发起请求,也没有使得子组件因为这个无关变量的变化,导致重新渲染。
原因分析:
- 使用useCallback使得无关变量变化时,阻止了新创建的fetchData的新地址返回,传给子组件的还是原本的函数地址(useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址)
- React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变(useCallback的存在使得props没变化),则不会重新渲染此组件。
场景四:单纯使用React.memo会发生什么
import React, { useCallback, useState, useEffect } from 'react';
import './App.css';function App() {const [query, setQuery] = useState(1);const [queryOther, setQueryOther] = useState(1);const fecthData = () => {console.log('新的fetch');return query;}const add = () => {console.log('点击add');setQuery(query + 1);}const addOther = () => {console.log('点击addOther');setQueryOther(queryOther + 1);}return (<><Child fecthData={fecthData} /><button onClick={add}>+1</button><button onClick={addOther}>other+1</button><div>{ query }</div></>);
}const Child = React.memo(({ fecthData }: { fecthData: any }) => {console.log('子组件相关内容');useEffect(() => {const querN = fecthData();console.log('子组件调用该函数获取到相关内容', querN);}, [fecthData])return <div>123</div>
})export default App;
初始状态:
点击按钮
React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。所以,在上面的代码示例里面,子组件是要被重新渲染的。上文中的fetchData因为失去了useCallback的保护使得子组件的props发生了变化,从而React.memo也失去了作用,而且因为fetchData因为失去了useCallback的保护,使得点击other+1按钮改变无关的变量时,子组件也调用了请求函数。
3.useCallback使用总结:
- 可以使用useCallback可以监听到相应状态变化,使得父/子组件做出响应。
- 但是滥用useCallback会影响性能,需搭配React.memo进行使用,否则适得其反。