10個案例讓你徹底理解React hooks的渲染邏輯
正式開始,今天要寫什么呢,原本我對react原理非常清楚,自己寫過簡單的react,帶diff算法和異步更新隊列的,但是對hooks源碼一知半解,于是就要深究他的性能相關(guān)問題了 - 重復(fù)渲染的邏輯
由于項目環(huán)境比較復(fù)雜,如果是純class組件,那么就是component、pureComponent、shouldComponentUpdate之類的控制一下是否重新渲染,但是hooks似乎更多場景,接下來一一攻破。
- 場景一 ,父組件使用hooks,子組件使用class Component
父組件
- 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>
- );
- }
- }
結(jié)果每次點擊圖中的測試按鈕,子組件Demo都會重新render:
總結(jié):父組件(hook)每次更新,都會導(dǎo)出一個新的state和value對象,子組件肯定會更新(如果不做特殊處理)
- 場景二,父組件使用hooks,子組件使用class PureComponent
父組件代碼跟上面一樣,子組件使用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>
- );
- }
- }
結(jié)果子組件依舊會每次都重新render:
總結(jié):結(jié)論同上,確實是依賴的props改變了,因為父組件是hook模式,每次更新都是直接導(dǎo)出新的value和state.
- 場景三,搞懂hook的setState跟class組件setState有什么不一樣
理論:class的setState,如果你傳入的是對象,那么就會被異步合并,如果傳入的是函數(shù),那么就會立馬執(zhí)行替換,而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里兩次分別設(shè)置了state的值為{a:2},{b:2},那么是合并,那么我最終得到state應(yīng)該是{a:2,b:2,c:1},如果是替換,那么最后得到的state是{b:2}
結(jié)果:
點擊測試按鈕后,state變成了{(lán)b:2},整個value被替換成了{(lán)b:2}
結(jié)論:hook的setState是直接替換,而不是合并
- 場景四 , 父組件使用class,子組件使用hook
父組件:
- 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組件)調(diào)用setState,刷新自身,然后傳遞給hooks子組件,然后自組件重新調(diào)用,更新
- 場景五
但是我此時需要想實現(xiàn)一個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包裹自己。
結(jié)果:
我們使用了memo實現(xiàn)了PureComponent的效果,淺比較了一次
- 場景六,hook,setState每次都是相同的值
- 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>
- );
- }
- }
結(jié)果:由于每次設(shè)置的值都是一樣的(都是1),hooks不會更新,同class
- 場景七,父組件和子組件都使用hook
父組件傳入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>;
- }
結(jié)果:每次點擊測試,都會導(dǎo)致子組件重新render
子組件加入memo
- function App(props: Props) {
- console.log(props, 'props');
- return <div>{props.count}</div>;
- }
- export default memo(App);
結(jié)果:
子組件并沒有觸發(fā)更新
這里跟第一個案例class的PureComponent不一樣,第一個案例class的PureComponent子組件此時會重新render,是因為父組件hooks確實每次更新都會導(dǎo)出新的value和state。這里是調(diào)用了一次,設(shè)置的都是相同的state.所以此時不更新
- 場景八,父組件hook,子組件hook,使用useCallback緩存函數(shù)
父組件:
- 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);
結(jié)果:雖然我們使用了memo.但是點擊demo1,只有demo1后面的數(shù)字改變了,demo2沒有改變,點擊demo2,兩個數(shù)字都改變了。
那么我們不使用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>
- );
- }
子組件代碼不變,結(jié)果此時每次都會兩個數(shù)字都會跟著變。
官方對useCallback的解釋:
就是返回一個函數(shù),只有在依賴項發(fā)生變化的時候才會更新(返回一個新的函數(shù))
結(jié)論:
我們聲明的 handleClickButton1 是直接定義了一個方法,這也就導(dǎo)致只要是父組件重新渲染(狀態(tài)或者props更新)就會導(dǎo)致這里聲明出一個新的方法,新的方法和舊的方法盡管長的一樣,但是依舊是兩個不同的對象,React.memo 對比后發(fā)現(xiàn)對象 props 改變,就重新渲染了。
- const a =()=>{}
- const b =()=>{}
- a===b //false
這個道理大家都懂,不解釋了
- 場景九,去掉依賴數(shù)組中的count2字段
- 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的值永遠(yuǎn)都是0,那么這個組件就不會重導(dǎo)出setCount2這個方法,handleClickButton2這個函數(shù)永遠(yuǎn)不會變化,Button只會更新一次,就是Demo組件接受到的props從0到1到的時候.繼續(xù)點擊,count2也是0,但是props有一次從0-1的過程導(dǎo)致Demo子組件被更新,不過count2始終是0,這非常關(guān)鍵
- 場景十,使用useMemo,緩存對象,達(dá)到useCallback的效果
使用前
- 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.
但是結(jié)果每次父組件修改了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就會導(dǎo)出一個新的count,const 就會聲明一個新的obj對象,即使用了memo包裹,也會被認(rèn)為是一個新的對象。
看看第二種的結(jié)果:
父組件更新,沒有再影響到子組件了。
寫在最后:
為什么花了將近4000字來講React hooks的渲染邏輯,React的核心思想,就是拆分到極致的組件化。拆得越細(xì)致,性能越好,避免不必要的更新,就是性能優(yōu)化的基礎(chǔ),希望此文能真正幫助到你了解hook的渲染邏輯