兩個 effect hook 是 React 提供給用戶處理反作用邏輯的一個窗口,好比改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其餘各類渲染過程當中不容許出現的操做。前端
在使用上,兩個 hook 的函數簽名是同樣的:react
useEffect(() => {
// 執行一些反作用
// ...
return () => {
// 清理函數
}
})
複製代碼
這樣會每次組件更新後都會執行,有點相似於 componentDidUpdate,但請不要用 class 組件的生命週期思惟方式來看待 hooks,只是看起來能夠先這麼理解。若是想要像 componentDidMount 那樣只執行一次的話,第二個參數傳入空數組:jquery
useEffect(() => {
// 執行一些反作用
// ...
return () => {
// 清理函數
}
}, [])
複製代碼
但有的時候須要根據 props 的變化來條件執行 effect 函數,要實現這一點,能夠給 useEffect 傳遞第二個參數,它是 effect 所依賴的值數組:編程
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
複製代碼
此時,只有當 props.source
改變後纔會從新建立訂閱。數組
這裏就要說到 useEffect 和 useLayoutEffect 區別了。瀏覽器
官網中的提示,絕大部分場景只用到 useEffect 就能夠,只有當它出問題的時候再嘗試使用 useLayoutEffect。markdown
但什麼樣的狀況下 useLayoutEffect 才能體現不一樣之處呢?閉包
首先咱們知道,瀏覽器中 JS 線程和渲染線程(注意是線程)是互斥的,對於 React 的函數組件來講,其更新過程大體分爲如下步驟:app
(這裏假設 React 組件已初次渲染成功)異步
前三步都是 React 在處理,也就是 JS 線程執行咱們所寫的代碼,都是在內存中進行一系列操做,而第四步纔是真正將更新後數據交給渲染線程進行處理。
那這時候的 useEffect 只會在第四步後纔會調用,也就是在瀏覽器繪製完後才調用,並且 useEffect 仍是異步執行的,所謂的異步就是被 React 使用 requestIdleCallback 封裝的,只在瀏覽器空閒時候纔會執行,這就保證了不會阻塞瀏覽器的渲染過程。
而 useLayoutEffect 就不同,它會在第三第四步之間執行,並且是同步阻塞後面的流程。
這二者的差距會在某些 DOM 變化的場景下體現出來:
如下面的代碼舉例:
export default function FuncCom () {
const [counter, setCounter] = useState(0);
useEffect(() => {
if (counter === 12) {
// 爲了演示,這裏同步設置一個延時函數 500ms
delay()
setCounter(2)
}
});
return (
<div style={{ fontSize: '100px' }}> <div onClick={() => setCounter(12)}>{counter}</div> </div>
)
}
複製代碼
能夠觀察到,初始屏幕上是 0,當點擊觸發 setCounter
後,屏幕上先是出現了 12,最後變爲了 2:
想象一下,這就是有些動畫場景會出現的閃屏現象,緣由在於 useEffect 執行的時候 setCounter(12)
已經觸發一次渲染了。這在體驗上很很差。
換成了 useLayoutEffect 後,屏幕上只會出現 0 和 2,這是由於 useLayoutEffect 的同步特性,會在瀏覽器渲染以前同步更新 DOM 數據,哪怕是屢次的操做,也會在渲染前一次性處理完,再交給瀏覽器繪製。這樣不會致使閃屏現象發生。
這裏簡單總結一下:
進一步分析,咱們但願在函數組件中使用 hook 函數替換 class 組件中的生命週期,那麼這裏是如何對應的?
一樣舉一個 class 組件的例子:
class ClassCom extends React.Component {
state = {
value: 'a'
}
componentDidMount() {
// 延時觸發
delay()
this.setState({
value: 'fasd'
})
}
componentDidUpdate() {
if (this.state.value === 'b') {
// 延時觸發
delay()
this.setState({
value: 'c'
})
}
}
render() {
return (
<div onClick={() => this.setState({ value: 'b' })} > Class Components {`${this.state.value}`} </div>
)
}
}
複製代碼
在瀏覽器中,初次渲染用戶不會看到 Class Components a 這個值,而是直接出現 mount 狀態以後的值 Class Components fasd,當觸發點擊事件後,只會顯示 didupdate 以後的值 Class Components c。
這說明了 componentDidMount 和 componentDidUpdate 都是同步阻塞的,並且是在 React 提交給瀏覽器渲染步驟以前。
因此從表現(以及源碼中的流程)來看,useLayoutEffect 和 componentDidMount,componentDidUpdate 調用時機是一致的,且都是被 React 同步調用,都會阻塞瀏覽器渲染。
同上,useLayoutEffect 返回的 clean 函數的調用位置、時機與 componentWillUnmount 一致,且都是同步調用。useEffect 的 clean 函數從調用時機上來看,更像是 componentDidUnmount (儘管 React 中並無這個生命週期函數)。
雖然 useLayoutEffect 更像 class 中的生命週期函數,但官方的建議是大多數正常狀況下,並不須要使用它,而是使用 useEffect,由於 useEffect 不會阻塞渲染,只有在涉及到修改 DOM、動畫等場景下考慮使用 useLayoutEffect,全部的修改會一次性更新到瀏覽器中,減小用戶體驗上的不適。
在使用 effect 的過程當中,有一個隱形的 bug 要注意。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
複製代碼
這段代碼的意圖很簡單,每隔 1000ms 更新 count,但事實上,count 永遠只會增長到 1!
一樣的代碼用 class 組件來實現,就不會有這個問題:
class Counter extends Components {
state = {
count: 0
}
id = null;
componentDidMount() {
this.id = setInterval(() => {
this.setState(({
count: this.state.count + 1
}));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.id)
}
render() {
return <h1>{this.state.count}</h1>
}
}
複製代碼
上面 class 組件和函數組件的代碼的差別在於,class 組件中的 this.state 是可變的!每一次的更新都是對 state 對象的一個更新,一次又一次的 setInterval 中引用的都會是新 state 中的值。這在使用 class 組件中很常見,咱們對於 state 對象也是這麼期待的。
然而在函數組件中狀況就不同了。函數組件因爲每次更新都會經歷從新調用的過程,useEffect(callback) 中的回調函數都是全新的,這樣其中引用到的 state 值將只跟當次渲染綁定。這是很神奇嗎?不,這就是閉包!這只是 JavaScript 的語言特性而已。
useEffect(() => {
// 回調函數只運行一次,這裏的 count 只記住初次渲染的那個值
// 因此致使每一次的 setInterval 中用到的永遠都不會變!
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
複製代碼
這點在使用函數組件要當心,寫慣了 class 組件後,咱們對於變量的一些使用上很容易產生誤解。把函數組件當成純粹的函數,每一次的組件更新渲染當前的頁面,也會記住當前環境下的變量值。這就是 React Hooks 所推崇的邏輯和狀態的同步,這跟 class 組件以生命週期爲劃分的思惟有着使人迷惑的差距,雖然同是 React,但這是全新的一個思惟方式,甚至我以爲更接近 JavaScript 語言的本質,更有函數式的氣質。
要想解決這個 setInterval 帶來的困惑,能夠深刻看一下這篇 post: Making setInterval Declarative with React Hooks
解決方案很簡單,但解決思考的過程很驚奇。
React 從剛推出來的時候就宣揚單向數據流的特色,根據 state 和 props 對象的變化來更新組件,這帶來了前端的一次革命,讓開發者擺脫了 jquery 這樣命令式的思惟編程方式,擁抱聲明式編程。
但經典的 calss 組件也不是沒有問題,複雜難懂的生命週期 API將咱們的狀態邏輯拆分到各個階段,這就給咱們設計組件多了一個時間維度思考。
而 Hooks 是進一步的革命,完全拋棄時間這一思考負重,從思考「個人狀態邏輯應該放在組件哪些生命週期中」到思考「隨着狀態變化,個人頁面應該展現成什麼樣」 和 「隨着狀態變化,什麼樣的反作用應該被觸發」。
這種「邏輯狀態和與頁面的同步」纔是真正的 React 數據流思惟方式,這是一種巨大的思惟減負。
useEffect 和 useLayoutEffect 相對於 componentDidMount 這樣的 API 來講,儘管能夠替代模仿,但本質上是不一樣的。 對於 effect hook API,咱們思考的是* UI 狀態完成後,咱們須要作一些什麼的反作用操做* ?而在 componentMount API 中咱們思考的是這個時間階段中咱們能夠作些什麼反作用操做?
componentMount API 思考的是各個時間階段中的操做,effect hook API 不須要考慮時間這一因素,只須要考慮組件狀態變化後的處理。