React(16.13.1)中useEffect依賴改變時的渲染順序以及性能提升學習


目錄

一、 單個tsx文件依賴改變時渲染順序

1、useEffect簡單情況

這是最簡單的情況;每次組件render的時候,最先要明白的是useEffect第二個參數,一個依賴項的數組;分以下 3 * 2 種情況:

 

組件首次渲染

組件非首次渲染,在有state改變時渲染

useEffect無第二個參數

執行

執行

useEffect第二個參數是空數組

執行

不執行

useEffect第二個參數是非空數組

執行

改變的state在數組內 執行 否則 不執行 ;

注:改變采用淺比較,此處不深究比較原理;不建議傳入引用類型作為依賴項,如果在useEffect中修改了引用類型,則會引發無限渲染的問題

注:useEffect中的回調函數不只是在組件銷毀前調用,而是在每一輪的下一次render前也會調用,所以回調函數的執行邏輯同上表;

 

const Index1 = () => {
    const [test, setTest] = useState(1)
    console.log(1)

    useEffect(() => {
        console.log(4)
    })

    useEffect(() => {
        console.log(5)
    }, [])

    useEffect(() => {
        console.log(6)
    }, [test])

    console.log(2)

    return (
        <>
            {console.log(3)}
            <h1>這里是Index1</h1>
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

首先就是不論什么時候渲染;均是從上往下執行代碼;(此處重點討論useEffect,不討論同步、異步、宏任務和微任務;有興趣可以閱讀),

直到執行完return內的代碼;最后回過頭從上往下執行 每個useEffect內的代碼;我們稱之為一個渲染循環;打印順序為:1、2、3、4、5、6;可以分批理解為1、2、3為掛載前執行,4、5、6為掛載后執行;

點擊Button改變狀態值,從上到下會依然打印 1、2、3、4、6;

需要注意的是,如果是在執行代碼過程當中觸發的state改變;則需要先執行完當前渲染循環;然后執行下一個渲染循環;以此類推;

const Index2 = () => {
    const [test, setTest] = useState(1)
    console.log(1)

    useEffect(() => {
        console.log(4)
        return () => {
            console.log(11)
        }
    })

    useEffect(() => {
        console.log(5)
        const timer = setInterval(() => {
            console.log('每秒打印一次')
        }, 1000)
        return () => {
            console.log(12)
            clearInterval(timer)
        }
    }, [])

    useEffect(() => {
        console.log(6)
        return () => {
            console.log(13)
        }
    }, [test])

    console.log(2)

    return (
        <>
            {console.log(3)}
            <h1>這里是Index2</h1>
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

2、useEffect回調執行邏輯

在Index1的基礎上,我們加上useEffect的回調函數,其實回調不僅僅是在組件willunmount的時候執行那么簡單;

當然對於上面的例子,組件卸載時,11、12、13會依次打印;

但其實 點擊Button時;會依次打印1、2、3、11、13、4、6;也就是對於11、13 這2個回調而言,所在的useEffect再次render前會執行上次回調;12所在useEffect不會影響,故不會打印;

12所在的useEffect只會在組件卸載時候 執行回調;所以我們平時把清除定時器、清除onscroll等放在依賴為[]的useEffect內;

關於回調,其實在卸載組件之后不會立馬執行,而是在即將掛載的組件掛載前和掛載后的中間執行;(這點不重要;感興趣的讀者可以下去探索)

二、 tsx文件嵌套tsx文件時渲染順序

const Index = () => {
    const [test, setTest] = useState(1)
    console.log(1)

    useEffect(() => {
        console.log(4)
    })

    useEffect(() => {
        console.log(5)
    }, [])

    useEffect(() => {
        console.log(6)
    }, [test])

    console.log(2)

    return (
        <>
            <Child1 />
            {console.log(3)}
            <h1>這里是Index</h1>
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
            <hr />
        </>
    )
}

 

const Child1 = () => {
    const [test, setTest] = useState(1)
    console.log('child1', 1)


    useEffect(() => {
        console.log('child1', 4)
    })
    
      useEffect(() => {
        console.log('child1', 5)
    }, [])

    useEffect(() => {
        console.log('child1', 6)
    }, [test])

    console.log('child1', 2)

    return (
        <>
            {console.log('child1', 3)}
            <h1>這里是Child1</h1>
        </>  
    )
}

1、初次渲染

執行順序其實和第一大點(單個jsx(tsx)文件依賴改變時渲染順序)一致;每執行一次渲染的時候,從組件最上方執行到最下方,依然是按照從上到下,執行完之后,再按照上表的6種情況執行useEffect內的代碼;

這里 需要注意的是;若嵌套子組件;則Child屬於當前組件的 return內容;執行完之后,才會執行當前組件的useEffect;也就是先打印2才會打印1;

所以上面代碼打印順序為:1、2、3、child1 1、child1 2、child1 3、child1 4、child1 5、child1 6、4、5、6;這里比較特殊的是:console.log(3)在<Child1 />下面;但是卻先打印了3;

這里僅僅是嵌套一個Child1;讀者可以腦補一個Child2和Child1平級;打印則是:1、2、3、child1 1、child1 2、child1 3、child2 1、child2 2、child2 3、child1 4、child1 5、child1 6、child2 4、child2 5、child2 6、4、5、6;

可以看出來要先把2個子組件的掛載前執行完畢才去執行useEffect;

2、state改變時

若是子組件state改變,和父組件無關,毋需討論;

若是父組件state改變,打印順序為:1、2、3、child1 1、child1 2、child1 3、child1 4、4、6;這里我們得到 在渲染過程中,只是把依賴數組內無關的過濾掉了;其余沒有什么特別之處;

三、 tsx文件嵌套tsx文件時提升性能

1、利用React.memo()

從上面的分析學習我們不難發現;組件嵌套,初次渲染和父組件state改變時;無論子組件有沒有變動,只要父組件render,所有的子組件都會重新render!

初次渲染無法提升性能;但是父組件state改變時,我們可以做一些事情;這就請出我們的另一個主要討論對象:React.memo(); 簡單代碼如下:

 

const Index = () => {
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2 = {'props2'} />
        </>
    )
}

 

 

const Child1 = (props) => {
    return (
        <>
            {console.log('child1')}
            <h1>這里是Child1</h1>
            <hr />
        </>
    )
}
export default Child1

 

const Child2 = (props) => {
    return (
        <>
            {console.log('child2')}
            <h1>這里是Child2</h1>
            <hr />
        </>
    )
}

export default React.memo(Child2)

如上 Child1為參照;當index中state更新,新一輪render后,打印了child1,child2並未打印;證明memo起了作用;

index每一次render時候,child1無論如何都會每次render;這時候我們把重點放在child2上;我們將<Child2 /> 的props2變一下;變成非定值;而是和state相關的;Child1和Child2不變;如下:

const Index = () => {
		const [test, setTest] = useState(1)
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2={test} />
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

這時候我們發現 改變state時,child1和child2都打印了;證明2個子組件都刷新了;所以我們有了個初步的結論:當memo不傳入第二個參數時,父組件render時;當props不改變;則該子組件不會render;props有改變時,則該子組件會render!

但是到這還沒完;不禁猜想若 child2的props是個父組件本輪render不相關的state會怎么樣;而非一個相關state;如下:

const Index = () => {
		const [test, setTest] = useState(1)
    const [definiteV, setDefiniteV] = useState(1)
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2={definiteV} />
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

我們注意到此時;即便test改變;definiteV不改變(即便是definiteV是一個引用類型;但我們平時開發中,不建議state使用引用類型);Child2也不會重新render;所以我們可以有了提升性能第一個小措施:當子組件不論是否接收props,都用React.memo()包裹,會在無關props變化改變時減少子組件刷新次數;提升性能;

提到基本類型和引用類型;雖然在不相關state中是無差別的;但作為一個普通變量就不一樣了;我們看一下如下代碼:

const Index = () => {
		const [test, setTest] = useState(1)
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2={[1,2,3]} />
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

我們把child2的props2換成數組;再次改變test;發現child1和child2都打印了;證明child1和child2都重新render;我們猜想是因為數組的引用地址不一樣,造成每次diff不同(memo的diff有點類似useEffect依賴的淺比較);

這顯然是不符合我們預期的;我們就引出有一個hook;叫:useMemo;我們試着這樣寫:

2、結合使用useMemo和useCallback

const Index = () => {
		const [test, setTest] = useState(1)
     const memoValue = useMemo(() => [1, 2, 3], [])
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2={memoValue} />
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

我們發現再次改變test;child2不打印了;證明useMemo起了效果;所以我們又有了一個提升性能的小措施:給子組件傳的引用類型props最好可以結合使用useMemo以減少子組件不必要刷新,當然子組件需要結合使用React.memo;

還有一種特殊類型;也是我們工作中經常傳給子組件的;那就是方法;

const Index = () => {
		const [test, setTest] = useState(1)
    const fn1 = () => {}
    const fn2 = useCallback(() => {},[])
    return (
        <>
            <Child1  props1 = {'props1'} />
            <Child2  props2={fn1} />
            <Button
                onClick={() => {
                    setTest(v => v + 1)
                }}
            >
                改變狀態值
            </Button>
        </>
    )
}

當改變test,父組件一輪render中;props2分別傳入fn1和fn2的時候,傳入fn1時候child2會重新render,傳入fn2的時候則不會;useCallback和useMemo類似;區別是useMemo接收的是函數返回的值;而useCallback返回的即第一個參數,一個方法;

還有一個點值得我們注意:useMemo和useCallback都有第二個參數;一個依賴項數組;類似於useEffect的第二個參數;我們在文章前面分析了不少;此處不再贅述;

3、提一下React.memo()的第二個參數

當我們把Child2的代碼稍作改動;加上memo()的第二個參數;如下:

const Child2 = (props) => {
    return (
        <>
            {console.log('child2')}
            <h1>這里是Child2</h1>
            <hr />
        </>
    )
}

export default React.memo(Child2, (prev, next) => {
    return false
})

在Child2每一輪render中; prev就是上一輪的props,next為下一輪即將接收的props;類似於React類組件的shouldComponentUpdate()類似;

我們可以人為的對比前后props,進行對組件(Child2)刷新控制;個人認為React.memo第二個參數實用性不大;經過上面的分析學習不難發現React已經幫我們做的很好了;根本不需要我們自己去控制;僅在特別情況下我們可以利用,大部分情況用不到;

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM