精讀:10個案例讓你完全理解React hooks的渲染邏輯

寫在開頭: 前端

因爲項目全面技術轉型,項目裏會大量啓用到hooks,因而有了此次寫做react

做爲一個class組件的重度愛好者,被迫走向了hooks,閱讀hook的源碼(慘) 算法

原創:從零實現一個簡單版React (附源碼) 數組

如何優化你的超大型React應用 【原創精讀】 緩存

這些都是我以前的文章性能優化


正式開始,今天要寫什麼呢,本來我對react原理很是清楚,本身寫過簡單的react,帶diff算法和異步更新隊列的,可是對hooks源碼只知其一;不知其二,因而就要深究他的性能相關問題了   - 重複渲染的邏輯微信


因爲項目環境比較複雜,若是是純class組件,那麼就是component、pureComponent、shouldComponentUpdate之類的控制一下是否從新渲染,可是hooks彷佛更多場景,接下來一一攻破。dom

  • 場景一 ,父組件使用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>
        );
    }
}

結果每次點擊圖中的測試按鈕,子組件Demo都會從新render:

總結:父組件(hook)每次更新,都會導出一個新的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>
        );
    }
}

結果子組件依舊會每次都從新render:

總結:結論同上,確實是依賴的props改變了,由於父組件是hook模式,每次更新都是直接導出新的value和state.


  • 場景三,搞懂hook的setState跟class組件setState有什麼不同

理論: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是直接替換,而不是合併


  • 場景四 , 父組件使用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組件)調用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的效果,淺比較了一次


  • 場景六,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>
        );
    }
}

結果:因爲每次設置的值都是同樣的(都是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>;
}

結果:每次點擊測試,都會致使子組件從新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.因此此時不更新


  • 場景八,父組件hook,子組件hook,使用useCallback緩存函數

父組件:

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

**這個道理你們都懂,不解釋了
**


  • 場景九,去掉依賴數組中的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的值永遠都是0,那麼這個組件就不會重導出setCount2這個方法,handleClickButton2這個函數永遠不會變化,Button只會更新一次,就是Demo組件接受到的props從0到1到的時候.繼續點擊,count2也是0,可是props有一次從0-1的過程致使Demo子組件被更新,不過count2始終是0,這很是關鍵


  • 場景十,使用useMemo,緩存對象,達到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.

可是結果每次父組件修改了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),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端巔峯」公衆號,認真學前端,作個有專業的技術人...

點個在看支持我吧,轉發就更好了

好文我在看👇

相關文章
相關標籤/搜索