携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
1.React更新机制
1.1数据更新,组件重新加载
我们都清楚,React响应式数据发生改变时,组件会从上至下被重新加载。如下所示
import {useState} from "react"
function App() {
console.log("组件被渲染")
const [num,setNum] = useState(1)
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
</div>
}
export default App;
很明显当组件的num数据发生改变时,组件会被重新从上到下执行一次。
1.2父组件数据更新,子组件也更新
我们清楚了当组件数据更新时,默认组件会被重新执行。因此可以推理当父组件数据更新时,子组件肯定也会重新执行。如下所示
App.js
import Son from "./views/Son"
import {useState} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son/>
</div>
}
export default App;
Son.js
export default function Son(props) {
console.log("儿子组件被渲染")
return <div>
</div>
}
首次加载,显示父组件加载,子组件加载。当我们修改父组件的数据时,发现父子组件都重新加载了。
2.memo
上面提到了,当父组件的数据更新时,父组件重新加载的同时,子组件也会被重新加载。想想是不是很不合理啊。父亲犯法,儿子连坐。合理的做法应该是父亲如果和儿子一起参与了犯罪,那么二者都违法,如果只是父亲参与了犯罪,那父亲一人承担,与儿子没关系。
使用memo包裹子组件就可以做到上面的理想状态。
- 如果父组件传递给儿子组件的数据无变化时,当父组件因数据更新而重新加载时,子组件不会二次加载。
- 如果父组件传递给儿子组件的数据变化了,此时父组件因数据更新而重新加载时,子组件也二次加载
2.1父组件传递给儿子组件数据无变化时
App.js
import Son from "./views/Son"
import {useState} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son title='son title'/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
{props.title}
</div>
})
可以发现,经过memeo优化的子组件,当父组件传递给子组件的数据始终没有变化时,虽然父组件二次加载,但是子组件始终没有二次加载。
2.2父组件传递给子组件的数据变化时
App.js
import Son from "./views/Son"
import {useState} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son num={num}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
{props.num}
</div>
})
此次父组件在修改数据num时,重新加载父组件时,给儿子组件传递的数据num发生了改变,符合父子联合犯罪的要求,因此二者都要承受制裁.二者都要重新加载。
2.3疑问点:react如何判断父亲给儿子组件传递的数据是否变化了?
这个问题面试90%会考,许多小盆友看面筋会一口回答,浅比较。对,没错,判断是否更改数据的机制就是浅比较,那什么是浅比较?
浅度比较:两个对象,永远只对比第一层,如果第一层的键值相同,对应的值相同,则两个对象浅度相同
例子1
很明显,obj1和obj2的info的对象是不同的,它们地址值是不同的,因此浅比较返回false
let obj1 = {
name:"dzp"
age:"22",
info:{}
}
let obj2 = {
name:"dzp"
age:"22",
info:{}
}
例子2
obj1和obj2的对象浅度比较是相同的。许多人可能懵了,这是相同?自己看上面浅度比较的定义。永远只比较第一层,obj1和obj2的info对象地址始终指向一个对象。所以浅度比较是true
let info = {a:1}
let obj1 = {
name:"dzp"
age:"22",
info:info
}
info.b = 2
let obj2 = {
name:"dzp"
age:"22",
info:info
}
2.4 实际例子
例子1
App.js
import Son from "./views/Son"
import {useState} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
let name = "dzp"
let obj = {other:"other"}
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son name={name} obj={obj}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
{props.num}
</div>
})
我们发现父组件数据更新时,父子组件都被重新加载了。一部分人可能骂娘了。这子组件数据不是没变化?为什么还会重新加载。仔细分析,当父组件的数据num更新时,父组件被重新加载,父组件的name和对象obj也重新创建。子组件在进行浅度比较时,发现obj对象的地址已经改变,因此返回false.触发子组件的更新。
例子2
App.js
import Son from "./views/Son"
import {useState} from "react"
let obj = {other:"other"}
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
let name = "dzp"
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son name={name} obj={obj}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
{props.num}
</div>
})
我对例子1的数据做了小小改变,将obj对象放到了父组件的头部,因此父组件因数据更新重新加载时,传递给儿子组件的obj始终是上一份的obj.浅度比较返回的就是true。因此父组件的更新,不会触发儿子组件的更新
3.useCallback
上面我们已经优化了父组件数据更新,子组件二次渲染问题。我们继续看一个栗子。
3.1 父组件给子组件传递函数
App.js
import Son from "./views/Son"
import {useState} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
const fn = () => {
console.log("fn")
}
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son fn={fn}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
</div>
})
其实看懂了上面的浅比较,此处你就明白,为什么传递给子组件是一个函数时,父组件更新,子组件始终会更新。因为父组件重新加载时,函数对象是新创建的,和上一次进行浅比较返回的是false.因此子组件必定重新渲染。如何解决这个问题?我们继续看
3.2 方案一:传递子组件的函数放到外面
方案一:既然是浅度比较,我们就可以将传递函数单独放到函数组件的外面,这样我们就可以始终保证传递的函数是始终不变的
App.js
import Son from "./views/Son"
import {useState} from "react"
const fn = () => {
console.log("fn")
}
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son fn={fn}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
</div>
})
继续考虑,如果这个函数是有关修改useState数据的?那函数放到组件外面肯定是报错的,因为这个函数外面没有useState的相关定义啊。如何继续设计? 终于终于等到今天的主角了,那就是useCallback。
方案二:useCallback
此时,你已经清楚了useCallback的作用了。useCallback就是解决父组件给儿子组件传递函数时,父组件因数据改变重新加载导致子组件二次渲染问题。
使用
-
第一个参数传入回调函数
-
第二个参数是回调函数的依赖项。当依赖项改变时,回调函数重新加载
useCallback(()=>{},[])
示例
App.js
import Son from "./views/Son"
import {useState,useCallback} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
const memory = useCallback(()=>{
console.log("fn")
},[])
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son fn={memory}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
</div>
})
我们发现,父组件因为数据修改导致父组件重新加载,但是此处useCallback根据依赖项判断无变化,因此传递给子组件的函数始终是同一份函数,子组件不更新。
栗子1
App.js
import Son from "./views/Son"
import {useState,useCallback} from "react"
function App() {
console.log("父组件被渲染")
const [num,setNum] = useState(1)
const memory = useCallback(()=>{
},[num])
return <div className="app">
<div>{num}</div>
<div onClick={()=>setNum(num+1)}>修改num</div>
<Son fn={memory}/>
</div>
}
export default App;
Son.js
import { memo } from "react"
export default memo(function Son(props) {
console.log("儿子组件被渲染")
return <div>
</div>
})
我们对上面的依赖项重新添加了num,也就是说只要num改变,这个函数就会被重新创建一份。因此父组件因数据改变导致父组件重新加载时,给儿子组件传递的函数发生了改变。所以儿子组件也会二次加载。
注意点
- useCallback必须和memo结合,因为只有memo才让组件提供了浅比较判断是否更新,离开了memeo的useCallback是毫无作用的。
- 当子组件数据和结构不是很复杂时,尽量不要使用useCallback去优化,facebook核心开发人员指出过,useCallback本身也是耗费性能的。对于简单的应用和组件,使用useCallback可能会反向优化。
4.注意
useCallback使用时尤其要注意,第二个参数依赖项。规律:useCallback内部修改state的地方都要加入到依赖数组中,否则会由于闭包产生bug。
function App() {
const [num,setNum] = useState(1);
const addNum = useCallback(()=>{
setNum(num+1);
},[])
return (
<div className="App">
num:{num}
<p onClick={addNum}>add</p>
</div>
);
}
export default App;
上面的代码,useCallback依赖数组是空,表示函数始终加载1次,当我们触发addNum时数据在第一次发生更新,后面点击修改都是无效的。正确的姿势就是在依赖数组中添加num
总结
- 父组件更新,子组件默认也会更新
- 对子组件使用memo包裹的函数组件,当父组件更新时,会根据props浅比较判断是否更新子组件
- 对子组件使用memo包裹的函数组件,并且父组件给子组件传递父组件内部函数时,当父组件更新时,子组件一定会更新(浅比较,父组件更新时,内部函数地址也会变化)
- 对子组件使用memeo包裹的函数组件,并且父组件通过useCallback包裹内部函数传递给子组件时,如果useCallback的依赖未发生变化,传递的函数始终是同一份函数地址,此时子组件根据浅比较不会发生更新。