寫在開頭: 前端
因爲項目全面技術轉型,項目裏會大量啓用到hooks,因而有了此次寫做react
做爲一個class組件的重度愛好者,被迫走向了hooks,閱讀hook的源碼(慘) 算法
這些都是我以前的文章性能優化
正式開始,今天要寫什麼呢,本來我對react原理很是清楚,本身寫過簡單的react,帶diff算法和異步更新隊列的,可是對hooks源碼只知其一;不知其二,因而就要深究他的性能相關問題了 - 重複渲染的邏輯微信
因爲項目環境比較複雜,若是是純class組件,那麼就是component、pureComponent、shouldComponentUpdate之類的控制一下是否從新渲染,可是hooks彷佛更多場景,接下來一一攻破。dom
父組件異步
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2, b: 1 }); //@ts-ignore setState({ a: 2, b: 2 }); console.log(state, 'state'); }} > 測試 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 測試 </Button> <Demo value={state} /> </div> ); }
子組件函數
export default class App extends React.Component<Props> { render() { const { props } = this; console.log('demo render'); return ( <div> {props.value.a},{props.value.b} </div> ); } }
結果每次點擊圖中的測試按鈕,子組件Demo都會從新render:
總結:父組件(hook)每次更新,都會導出一個新的state和value對象,子組件確定會更新(若是不作特殊處理)
父組件代碼跟上面同樣,子組件使用PureComponent:
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2, b: 1 }); //@ts-ignore setState({ a: 2, b: 2 }); console.log(state, 'state'); }} > 測試 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 測試 </Button> <Demo value={state} /> </div> ); }
子組件使用PureComponent:
export default class App extends React.PureComponent<Props> { render() { const { props } = this; console.log('demo render'); return ( <div> {props.value.a},{props.value.b} </div> ); } }
結果子組件依舊會每次都從新render:
總結:結論同上,確實是依賴的props改變了,由於父組件是hook模式,每次更新都是直接導出新的value和state.
理論:class的setState,若是你傳入的是對象,那麼就會被異步合併,若是傳入的是函數,那麼就會立馬執行替換,而hook的setState是直接替換,那麼setState在hook中是異步仍是同步呢?
**實踐:
**
組件A:
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b},{state.c} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2 }); //@ts-ignore setState({ b: 2 }); console.log(state, 'state'); }} > 測試 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 測試 </Button> <Demo value={state} /> </div> ); }
**我將setState裏兩次分別設置了state的值爲{a:2},{b:2},那麼是合併,那麼我最終獲得state應該是{a:2,b:2,c:1},若是是替換,那麼最後獲得的state是{b:2}
**
**結果:
**
點擊測試按鈕後,state變成了{b:2},整個value被替換成了{b:2}
結論:hook的setState是直接替換,而不是合併
父組件:
export default class App extends React.PureComponent { state = { count: 1, }; onClick = () => { const { count } = this.state; this.setState({ count: count + 1, }); }; render() { const { count } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> <Button onClick={this.onClick}>測試</Button> </div> ); } }
子組件:
interface Props { count: number; } export default function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; }
邏輯:父組件(class組件)調用setState,刷新自身,而後傳遞給hooks子組件,而後自組件從新調用,更新
可是我此時須要想實現一個class 組件的 PureComponent同樣的效果,須要用到React.memo
修改父組件代碼爲:
export default class App extends React.PureComponent { state = { count: 1, value: 1, }; onClick = () => { const { value } = this.state; this.setState({ count: value + 1, }); }; render() { const { count, value } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> {value} <Button onClick={this.onClick}>測試</Button> </div> ); } }
子組件加入memo,代碼修改成:
import React, { useState, memo } from 'react'; interface Props { count: number; } function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; } export default memo(App);
此時邏輯:class組件改變了自身的state,本身刷新本身,由上而下,傳遞了一個沒有變化的props給hooks組件,hooks組件使用了memo包裹本身。
結果:
咱們使用了memo實現了PureComponent的效果,淺比較了一次
export default class App extends React.PureComponent { state = { count: 1, value: 1, }; onClick = () => { const { value } = this.state; this.setState({ value: 1, }); }; render() { const { count, value } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> {value} <Button onClick={this.onClick}>測試</Button> </div> ); } }
結果:因爲每次設置的值都是同樣的(都是1),hooks不會更新,同class
父組件傳入count給子組件
export default function Father() { const [count, setCount] = useState(1); const [value, setValue] = useState(1); console.log('father render') return ( <div> <Demo count={count} /> <div>value{value}</div> <Button onClick={() => { setValue(value + 1); }} > 測試 </Button> </div> ); }
子組件使用count
export default function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; }
結果:每次點擊測試,都會致使子組件從新render
子組件加入memo
function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; } export default memo(App);
結果:
子組件並無觸發更新
⚠️:這裏跟第一個案例class的PureComponent不同,第一個案例class的PureComponent子組件此時會從新render,是由於父組件hooks確實每次更新都會導出新的value和state。這裏是調用了一次,設置的都是相同的state.因此此時不更新
父組件:
export default function App() { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const handleClickButton1 = () => { setCount1(count1 + 1); }; const handleClickButton2 = useCallback(() => { setCount2(count2 + 1); }, [count2]); return ( <div> <div> <Button onClickButton={handleClickButton1}>Button1</Button> </div> <div> <Button onClickButton={handleClickButton2}>Button2</Button> </div> </div> ); }
子組件:
import React from 'react'; const Button = (props: any) => { const { onClickButton, children } = props; return ( <> <button onClick={onClickButton}>{children}</button> <span>{Math.random()}</span> </> ); }; export default React.memo(Button)
結果:雖然咱們使用了memo.可是點擊demo1,只有demo1後面的數字改變了,demo2沒有改變,點擊demo2,兩個數字都改變了。
那麼咱們不使用useCallback看看
父組件修改代碼,去掉useCallback
export default function App() { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const handleClickButton1 = () => { setCount1(count1 + 1); }; const handleClickButton2 = () => { setCount2(count2+ 1); }; return ( <div> <div> <Demo onClickButton={handleClickButton1}>Demo1</Demo> </div> <div> <Demo onClickButton={handleClickButton2}>Demo</Demo> </div> </div> ); }
**子組件代碼不變,結果此時每次都會兩個數字都會跟着變。
**
官方對useCallback的解釋:
就是返回一個函數,只有在依賴項發生變化的時候纔會更新(返回一個新的函數)
結論:
咱們聲明的handleClickButton1是直接定義了一個方法,這也就致使只要是父組件從新渲染(狀態或者props更新)就會致使這裏聲明出一個新的方法,新的方法和舊的方法儘管長的同樣,可是依舊是兩個不一樣的對象,React.memo 對比後發現對象 props 改變,就從新渲染了。
const a =()=>{} const b =()=>{} a===b //false
**這個道理你們都懂,不解釋了
**
import React, { useState, useCallback } from 'react'; import Demo from './Demo'; export default function App() { const [count2, setCount2] = useState(0); const handleClickButton2 = useCallback(() => { setCount2(count2 + 1); }, []); return ( <Demo count={count2} onClickButton={handleClickButton2} >測試</Demo> ); }
這樣count2的值永遠都是0,那麼這個組件就不會重導出setCount2這個方法,handleClickButton2這個函數永遠不會變化,Button只會更新一次,就是Demo組件接受到的props從0到1到的時候.繼續點擊,count2也是0,可是props有一次從0-1的過程致使Demo子組件被更新,不過count2始終是0,這很是關鍵
使用前
export default function App() { const [count, setCount] = useState(0); const [value, setValue] = useState(0); const userInfo = { age: count, name: 'Jace', }; return ( <div> <div> <Demo userInfo={userInfo} /> </div> <div> {value} <Button onClick={() => { setValue(value + 1); }} ></Button> </div> </div> ); }
子組件使用了memo,沒有依賴value,只是依賴了count.
可是結果每次父組件修改了value的值後,雖然子組件沒有依賴value,並且使用了memo包裹,仍是每次都從新渲染了
import React from 'react'; const Button = (props: any) => { const { userInfo } = props; console.log('sub render'); return ( <> <span>{userInfo.count}</span> </> ); }; export default React.memo(Button);
使用後useMemo
const [count, setCount] = useState(0); const obj = useMemo(() => { return { name: "Peter", age: count }; }, [count]); return <Demo obj={obj}>
*很明顯,第一種方式,若是每次hook組件更新,那麼hook就會導出一個新的count,const 就會聲明一個新的obj對象,即便用了memo包裹,也會被認爲是一個新的對象。。*
看看第二種的結果:
父組件更新,沒有再影響到子組件了。
寫在最後:
爲何花了將近4000字來說React hooks的渲染邏輯,React的核心思想,就是拆分到極致的組件化。拆得越細緻,性能越好,避免沒必要要的更新,就是性能優化的基礎,但願此文能真正幫助到你瞭解hook的渲染邏輯
CALASFxiaotan
),拉你進技術羣,長期交流學習...前端巔峯
」公衆號,認真學前端,作個有專業的技術人...點個在看支持我吧,轉發就更好了
好文我在看👇