問題引入
子組件的只依賴傳入的name屬性,但是父組件name屬性和text屬性變化都會導致Parent函數重新執行,所以即使傳入子組件props沒有任何變化,甚至子組件沒有依賴於任何props屬性,都會導致子組件重新渲染
const Child = ((props: any) => {
console.log("子組件更新...");
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
memo
使用memo包裹子組件時,只有props發生改變子組件纔會重新渲染。使用memo可以提升一定的性能。
const Child = memo((props: any) => {
console.log("子組件更新..."); // 只有當props屬性改變,集name屬性改變時,子組件纔會重新渲染
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
但如果傳入的props包含函數,父組件每次重新渲染都是創建新的函數,所以傳遞函數子組件還是會重新渲染,即使函數的內容還是一樣。如何解決這一問題,我們希望把函數也緩存起來,於是引入useCallback
const Child = memo((props: any) => {
console.log("子組件更新..."); // 即使點擊按鈕count變化,但函數重新執行,handleInputChange已經指向新的函數實例,所以子組件依然會刷新
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div> <input onChange={props.handleInputChange} /></div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name={text} handleInputChange={handleInputChange}/>
</div>)
}
useCallback
useCallback用用於緩存函數,只有當依賴項改變時,函數纔會重新執行返回新的函數
對於父組件中的函數作爲props傳遞給子組件時,只要父組件數據改變,函數重新執行,作爲props的函數也會產生新的實例,導致子組件的刷新
使用useCallback可以緩存函數。需要搭配memo使用
const Child = memo((props: any) => {
console.log("子組件更新...");
return (
<div>
<h3>子組件</h3>
<div>text:{props.name}</div>
<div> <input onChange={props.handleInputChange} /></div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange =useCallback((e) => {
setText(e.target.value )
},[])
return (<div>
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name={text} handleInputChange={handleInputChange}/>
</div>)
}
useCallback第二個參數依賴項什麼情況下使用呢,看下面的例子
//修改handleInputChange
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[])
以上例子count改變會發生什麼事呢?
count改變,但handleInputChange不依賴與任何項,所以handleInputChange只在初始化的時候調用一次函數就被緩存起來,當文本改變時或者count改變時函數內部的count始終爲0,至於爲什麼需要看useCallback源碼後解答。
所以需要將count加入到依賴項,count變化後重新生成新的函數,改變函數內部的count值
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[count])
useMemo
useMemo使用場景請看下面這個例子,getTotal假設是個很昂貴的操作,但該函數結果僅依賴於count和price,但是由於color變化,DOM重新渲染也會導致該函數的執行,
useMemo便是用於緩存該函數的執行結果,僅當依賴項改變後纔會重新計算
const Parent = () => {
const [count, setCount] = useState(0);
const [color,setColor] = useState("");
const [price,setPrice] = useState(10);
const handleClick = () => {
setCount(count + 1);
}
const getTotal = ()=>{
console.log("getTotal exec ...") // 該函數依賴於count和price,但color變化也會導致該函數的執行
return count * price
}
return (<div>
<div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div>
<div> 單價: <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div>
<div> 數量:{count} <button onClick={handleClick}>+1</button></div>
<div>總價:{getTotal()}</div>
</div>)
}
修改後如下,注意useMemo緩存的是函數執行的結果
const Parent = () => {
console.log("parent exec...")
const [count, setCount] = useState(0);
const [color,setColor] = useState("");
const [price,setPrice] = useState(10);
const handleClick = () => {
setCount(count + 1);
}
const getTotal = useMemo(()=>{
console.log("getTotal exec ...")
return count * price
},[count, price])
return (<div>
<div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div>
<div> 單價: <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div>
<div> 數量:{count} <button onClick={handleClick}>+1</button></div>
<div>總價:{getTotal}</div>
</div>)
}
useMemo和useEffect區別
以上useMemo例子也可以使用useEffect實現,使用useEffect改寫的結果
const Parent = () => {
console.log("parent exec...")
const [count, setCount] = useState(0);
const [color,setColor] = useState("");
const [price,setPrice] = useState(10);
const [total,setTotal] = useState(0)
useEffect(()=>{
setTotal(price * count)
},[price,count])
const handleClick = () => {
setCount(count + 1);
}
return (<div>
<div> 顏色: <input onChange={(e) => setColor(e.target.value)}/></div>
<div> 單價: <input value={price} onChange={(e) => setPrice(Number(e.target.value))}/></div>
<div> 數量:{count} <button onClick={handleClick}>+1</button></div>
<div>總價:{total}</div>
</div>)
}
注意觀察useEffect是在price,count其中任何一個變化後觸發,然後total改變,total改變又會觸發函數的重新執行,導致組件重新渲染,所以在控制套可以看到“parent exec...”被打印了兩次,而第一個例子中,“parent exec...”只被打印了一次
所以可以得出如下總結:
useEffect是在DOM改變之後觸發,useMemo在DOM重新渲染之前就觸發
useEffect設置值會再次重新渲染,但useMemo不會