大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。
React Hooks
Hook 是什么
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
Hook 是 React 團隊在 React 16.8 版本中提出的新特性,在遵循函數式組件的前提下,為已知的 React 概念提供了更直接的 API:props,state,context,refs 以及聲明周期,目的在于解決常年以來在 class 組件中存在的各種問題,實現更高效的編寫 react 組件
class 組件的不足
難以復用組件間狀態邏輯:組件狀態邏輯的復用,需要 props render和高階組件等解決方案,但是此類解決方案的抽象封裝將會導致層級冗余,形成“嵌套地獄”
難以維護復雜組件:
許多不相干的邏輯代碼被混雜在同一個生命周期中,相關聯的邏輯代碼被拆分到不同聲明周期當中,容易遺忘導致產生bug
組件常常充斥著狀態邏輯的訪問和處理,不能拆分為更小的粒度,可通過狀態管理庫集中管理狀態,但耦合了狀態管理庫又會導致組件復用性降低
this 指向問題:在 JavaScript 中,class 的方法默認不會綁定 this,當調用 class 的方法時 this 的值為 undefined,為了在方法中訪問 this 則必須在構造器中綁定或使用 class fields 語法(實驗性語法)
class?Example?extends?React.Component?{constructor(props)?{...//?方式1:?在構造函數中綁定?thisthis.handleClick?=?this.handleClick.bind(this);}handleClick()?{this.setState({...})}//?方式2:?使用?class?fields?語法handleClick?=?()?=>?{this.setState({...})} }
難以對 class 進行編譯優化:由于 JavaScript 歷史設計原因,使用 class 組件會讓組件預編譯過程中變得難以進行優化,如 class 不能很好的壓縮,并且會使熱重載出現不穩定的情況
Hook 的優勢
Hook 使你在無需改變組件結構的情況下復用狀態邏輯(自定義 Hook)
Hook 將組件中互相關聯的部分拆分成更小的函數(比如設置訂閱或請求數據)
Hook 使你在非 class 的情況下可以使用更多的 React 特性
Hook 使用規則
Hook 就是 Javascript 函數,使用它們時有兩個額外的規則:
只能在函數外層調用 Hook,不要在循環、條件判斷或者子函數中調用
只能在 React 的函數組件和自定義 Hook 中調用 Hook。不要在其他 JavaScript 函數中調用
在組件中 React 是通過判斷 Hook 調用的順序來判斷某個 state 對應的 useState
的,所以必須保證 Hook 的調用順序在多次渲染之間保持一致,React 才能正確地將內部 state 和對應的 Hook 進行關聯
useState
useState
用于在函數組件中調用給組件添加一些內部狀態 state,正常情況下純函數不能存在狀態副作用,通過調用該 Hook 函數可以給函數組件注入狀態 state
useState
唯一的參數就是初始 state,會返回當前狀態和一個狀態更新函數,并且 useState
返回的狀態更新函數不會把新的 state 和舊的 state 進行合并,如需合并可使用 ES6 的對象結構語法進行手動合并
const?[state,?setState]?=?useState(initialState);
方法使用
import?React,?{?useState?}?from?'react';export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);return?(<div><button?onClick={()?=>?setCount(count?-?1)}>-</button><input?type="text"?value={count}?onChange={(e)?=>?setCount(e.target.value)}?/><button?onClick={()?=>?setCount(count?+?1)}>+</button></div>);
}
等價 class 示例
useState
返回的狀態類似于 class 組件在構造函數中定義 this.state
,返回的狀態更新函數類似于 class 組件的 this.setState
import?React?from?'react';export?default?class?Counter?extends?React.Component?{constructor(props)?{super(props);this.state?=?{count:?0};}render()?{return?(<div><button?onClick={()?=>?this.setState({?count:?this.state.count?-?1?})}>-</button><input?type="text"?value={this.state.count}?onChange={(e)?=>?this.setState({?count:?e.target.value?})}?/><button?onClick={()?=>?this.setState({?count:?this.state.count?+?1?})}>+</button></div>);}
}
函數式更新
如果新的 state 需要通過使用先前的 state 計算得出,可以往 setState
傳遞函數,該函數將接收先前的 state,并返回一個更新后的值
import?React,?{?useState?}?from?'react'export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);const?lazyAdd?=?()?=>?{setTimeout(()?=>?{//?每次執行都會最新的state,而不是使用事件觸發時的statesetCount(count?=>?count?+?1);},?3000);}?return?(<div><p>the?count?now?is?{count}</p><button?onClick={()?=>?setCount(count?+?1)}>add</button><button?onClick={lazyAdd}>lazyAdd</button></div>);
}
惰性初始 state
如果初始 state 需要通過復雜計算獲得,則可以傳入一個函數,在函數中計算并返回初始的 state,此函數只會在初始渲染時被調用
import?React,?{?useState?}?from?'react'export?default?function?Counter(props)?{//?函數只在初始渲染時執行一次,組件重新渲染時該函數不會重新執行const?initCounter?=?()?=>?{console.log('initCounter');return?{?number:?props.number?};};const?[counter,?setCounter]?=?useState(initCounter);return?(<div><button?onClick={()?=>?setCounter({?number:?counter.number?-?1?})}>-</button><input?type="text"?value={counter.number}?onChange={(e)?=>?setCounter({?number:?e.target.value})}?/><button?onClick={()?=>?setCounter({?number:?counter.number?+?1?})}>+</button></div>);
}
跳過 state 更新
調用 State Hook 的更新函數時,React 將使用 Object.is
來比較前后兩次 state,如果返回結果為 true,React 將跳過子組件的渲染及 effect 的執行
import?React,?{?useState?}?from?'react';export?default?function?Counter()?{console.log('render?Counter');const?[counter,?setCounter]?=?useState({name:?'計時器',number:?0});//?修改狀態時傳的狀態值沒有變化,則不重新渲染return?(<div><p>{counter.name}:?{counter.number}</p><button?onClick={()?=>?setCounter({?...counter,?number:?counter.number?+?1})}>+</button><button?onClick={()?=>?setCounter(counter)}>++</button></div>);
}
useEffect
在函數組件主體內(React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日志以及執行其他包含副作用的操作都是不被允許的,因為這可能會產生莫名其妙的 bug 并破壞 UI 的一致性
useEffect
Hook 的使用則是用于完成此類副作用操作。useEffect
接收一個包含命令式、且可能有副作用代碼的函數
useEffect
函數會在瀏覽器完成布局和繪制之后,下一次重新渲染之前執行,保證不會阻塞瀏覽器對屏幕的更新
useEffect(didUpdate);
方法使用
import?React,?{?useState,?useEffect?}?from?'react';export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);//?useEffect?內的回調函數會在初次渲染后和更新完成后執行//?相當于?componentDidMount?和?componentDidUpdateuseEffect(()?=>?{document.title?=?`You?clicked?${count}?times`;});return?(<div><p>count?now?is?{count}</p><button?onClick={()?=>?setCount(count?+?1)}>+</button></div>);
}
等價 class 示例
useEffect
Hook 函數執行時機類似于 class 組件的 componentDidMount
、componentDidUpdate
生命周期,不同的是傳給 useEffect
的函數會在瀏覽器完成布局和繪制之后進行異步執行
import?React?from?'react';export?default?class?Counter?extends?React.Component?{constructor(props)?{super(props);this.state?=?{count:?0,};}componentDidMount()?{document.title?=?`You?clicked?${this.state.count}?times`;}componentDidUpdate()?{document.title?=?`You?clicked?${this.state.count}?times`;}render()?{return?(<div><p>count?now?is?{this.state.count}</p><button?onClick={()?=>?this.setState({?count:?this.state.count?+?1?})}>+</button></div>);}
}
清除 effect
通常情況下,組件卸載時需要清除 effect 創建的副作用操作,useEffect
Hook 函數可以返回一個清除函數,清除函數會在組件卸載前執行。組件在多次渲染中都會在執行下一個 effect 之前,執行該函數進行清除上一個 effect
清除函數的執行時機類似于 class 組件componentDidUnmount
生命周期,這的話使用 useEffect
函數可以將組件中互相關聯的部分拆分成更小的函數,防止遺忘導致不必要的內存泄漏
import?React,?{?useState,?useEffect?}?from?'react';export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);useEffect(()?=>?{console.log('start?an?interval?timer')const?timer?=?setInterval(()?=>?{setCount((count)?=>?count?+?1);},?1000);//?返回一個清除函數,在組件卸載前和下一個effect執行前執行return?()?=>?{console.log('destroy?effect');clearInterval(timer);};},?[]);return?(<div><p>count?now?is?{count}</p><button?onClick={()?=>?setCount(count?+?1)}>+</button></div>);
}
優化 effect 執行
默認情況下,effect 會在每一次組件渲染完成后執行。useEffect
可以接收第二個參數,它是 effect 所依賴的值數組,這樣就只有當數組值發生變化才會重新創建訂閱。但需要注意的是:
確保數組中包含了所有外部作用域中會發生變化且在 effect 中使用的變量
傳遞一個空數組作為第二個參數可以使 effect 只會在初始渲染完成后執行一次
import?React,?{?useState,?useEffect?}?from?'react';export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);useEffect(()?=>?{document.title?=?`You?clicked?${count}?times`;},?[count]);?//?僅在?count?更改時更新return?(<div><p>count?now?is?{count}</p><button?onClick={()?=>?setCount(count?+?1)}>+</button></div>);
}
useContext
Context 提供了一個無需為每層組件手動添加 props ,就能在組件樹間進行數據傳遞的方法,useContext
用于函數組件中訂閱上層 context 的變更,可以獲取上層 context 傳遞的 value
prop 值
useContext
接收一個 context 對象(React.createContext
的返回值)并返回 context 的當前值,當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider>
的 value
prop 決定
const?value?=?useContext(MyContext);
方法使用
import?React,?{?useContext,?useState?}?from?'react';const?themes?=?{light:?{foreground:?"#000000",background:?"#eeeeee"},dark:?{foreground:?"#ffffff",background:?"#222222"}
};//?為當前?theme?創建一個?context
const?ThemeContext?=?React.createContext();export?default?function?Toolbar(props)?{const?[theme,?setTheme]?=?useState(themes.dark);const?toggleTheme?=?()?=>?{setTheme(currentTheme?=>?(currentTheme?===?themes.dark??themes.light:?themes.dark));};return?(//?使用?Provider?將當前?props.value?傳遞給內部組件<ThemeContext.Provider?value={{theme,?toggleTheme}}><ThemeButton?/></ThemeContext.Provider>);
}function?ThemeButton()?{//?通過?useContext?獲取當前?context?值const?{?theme,?toggleTheme?}?=?useContext(ThemeContext);return?(<button?style={{background:?theme.background,?color:?theme.foreground?}}?onClick={toggleTheme}>Change?the?button's?theme</button>);
}
等價 class 示例
useContext(MyContext)
相當于 class 組件中的 static contextType = MyContext
或者 <MyContext.Consumer>
useContext
并沒有改變消費 context 的方式,它只為我們提供了一種額外的、更漂亮的、更漂亮的方法來消費上層 context。在將其應用于使用多 context 的組件時將會非常有用
import?React?from?'react';const?themes?=?{light:?{foreground:?"#000000",background:?"#eeeeee"},dark:?{foreground:?"#ffffff",background:?"#222222"}
};const?ThemeContext?=?React.createContext(themes.light);function?ThemeButton()?{return?(<ThemeContext.Consumer>{({theme,?toggleTheme})?=>?(<button?style={{background:?theme.background,?color:?theme.foreground?}}?onClick={toggleTheme}>Change?the?button's?theme</button>)}</ThemeContext.Consumer>);
}export?default?class?Toolbar?extends?React.Component?{constructor(props)?{super(props);this.state?=?{theme:?themes.light};this.toggleTheme?=?this.toggleTheme.bind(this);}toggleTheme()?{this.setState(state?=>?({theme:state.theme?===?themes.dark??themes.light:?themes.dark}));}render()?{return?(<ThemeContext.Provider?value={{?theme:?this.state.theme,?toggleTheme:?this.toggleTheme?}}><ThemeButton?/></ThemeContext.Provider>)}
}
優化消費 context 組件
調用了 useContext
的組件都會在 context 值變化時重新渲染,為了減少重新渲染組件的較大開銷,可以通過使用 memoization 來優化
假設由于某種原因,您有 AppContext
,其值具有 theme
屬性,并且您只想在 appContextValue.theme
更改上重新渲染一些 ExpensiveTree
方式1: 拆分不會一起更改的 context
function?Button()?{//?把?theme?context?拆分出來,其他?context?變化時不會導致?ExpensiveTree?重新渲染let?theme?=?useContext(ThemeContext);return?<ExpensiveTree?className={theme}?/>;
}
當不能拆分 context 時,將組件一分為二,給中間組件加上
React.memo
function?Button()?{let?appContextValue?=?useContext(AppContext);let?theme?=?appContextValue.theme;?//?獲取?theme?屬性return?<ThemedButton?theme={theme}?/>
}const?ThemedButton?=?memo(({?theme?})?=>?{//?使用?memo?盡量復用上一次渲染結果return?<ExpensiveTree?className={theme}?/>;
});
返回一個內置
useMemo
的組件
function?Button()?{let?appContextValue?=?useContext(AppContext);let?theme?=?appContextValue.theme;?//?獲取?theme?屬性return?useMemo(()?=>?{//?The?rest?of?your?rendering?logicreturn?<ExpensiveTree?className={theme}?/>;},?[theme])
}
useReducer
useReducer
作為 useState
的代替方案,在某些場景下使用更加適合,例如 state 邏輯較復雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。
使用 useReducer
還能給那些會觸發深更新的組件做性能優化,因為父組件可以向自組件傳遞 dispatch 而不是回調函數
const?[state,?dispatch]?=?useReducer(reducer,?initialArg,?init);
方法使用
import?React,?{?useReducer?}?from?'react'const?initialState?=?{?count:?0?};function?reducer(state,?action)?{switch?(action.type)?{case?'increment':return?{count:?state.count?+?1};case?'decrement':return?{count:?state.count?-?1};default:throw?new?Error();}
}export?default?function?Counter()?{const?[state,?dispatch]?=?useReducer(reducer,?initialState);return?(<><p>Count:?{state.count}</p><button?onClick={()?=>?dispatch({type:?'decrement'})}>-</button><button?onClick={()?=>?dispatch({type:?'increment'})}>+</button></>);
}
初始化 state
useReducer 初始化 sate 的方式有兩種
//?方式1
const?[state,?dispatch]?=?useReducer(reducer,{count:?initialCount}
);//?方式2
function?init(initialClunt)?{return?{count:?initialClunt};
}const?[state,?dispatch]?=?useReducer(reducer,?initialCount,?init);
useRef
useRef
用于返回一個可變的 ref 對象,其 .current
屬性被初始化為傳入的參數(initialValue
)
useRef
創建的 ref 對象就是一個普通的 JavaScript 對象,而 useRef()
和自建一個 {current: ...}
對象的唯一區別是,useRef
會在每次渲染時返回同一個 ref 對象
const?refContainer?=?useRef(initialValue);
綁定 DOM 元素
使用 useRef
創建的 ref 對象可以作為訪問 DOM 的方式,將 ref 對象以 <div ref={myRef} />
形式傳入組件,React 會在組件創建完成后會將 ref 對象的 .current
屬性設置為相應的 DOM 節點
import?React,?{?useRef?}?from?'react'export?default?function?FocusButton()?{const?inputEl?=?useRef(null);const?onButtonClick?=?()?=>?{inputEl.current.focus();};return?(<><input?ref={inputEl}?type="text"?/><button?onClick={onButtonClick}>Focus?the?input</button></>);
}
綁定可變值
useRef
創建的 ref 對象同時可以用于綁定任何可變值,通過手動給該對象的.current
屬性設置對應的值即可
import?React,?{?useState,?useRef,?useEffect?}?from?'react';export?default?function?Counter()?{const?[count,?setCount]?=?useState(0);const?currentCount?=?useRef();//?使用?useEffect?獲取當前?countuseEffect(()?=>?{currentCount.current?=?count;},?[count]);const?alertCount?=?()?=>?{setTimeout(()?=>?{alert(`Current?count?is:?${currentCount.current},?Real?count?is:?${count}`);},?3000);}return?(<><p>count:?{count}</p><button?onClick={()?=>?setCount(count?+?1)}>Count?add</button><button?onClick={alertCount}>Alert?current?Count</button></>);
}
性能優化(useCallback & useMemo)
useCallback
和 useMemo
結合 React.Memo
方法的使用是常見的性能優化方式,可以避免由于父組件狀態變更導致不必要的子組件進行重新渲染
useCallback
useCallback
用于創建返回一個回調函數,該回調函數只會在某個依賴項發生改變時才會更新,可以把回調函數傳遞給經過優化的并使用引用相等性去避免非必要渲染的子組件,在 props 屬性相同情況下,React 將跳過渲染組件的操作并直接復用最近一次渲染的結果
import?React,?{?useState,?useCallback?}?from?'react';function?SubmitButton(props)?{const?{?onButtonClick,?children?}?=?props;console.log(`${children}?updated`);return?(<button?onClick={onButtonClick}>{children}</button>);
}
//?使用?React.memo?檢查?props?變更,復用最近一次渲染結果
SubmitButton?=?React.memo(submitButton);export?default?function?CallbackForm()?{const?[count1,?setCount1]?=?useState(0);const?[count2,?setCount2]?=?useState(0);const?handleAdd1?=?()?=>?{setCount1(count1?+?1);}//?調用?useCallback?返回一個?memoized?回調,該回調在依賴項更新時才會更新const?handleAdd2?=?useCallback(()?=>?{setCount2(count2?+?1);},?[count2]);return?(<><div><p>count1:?{count1}</p><SubmitButton?onButtonClick={handleAdd1}>button1</SubmitButton></div><div><p>count2:?{count2}</p><SubmitButton?onButtonClick={handleAdd2}>button2</SubmitButton></div></>)
}
useCallback(fn, deps)
相當于 useMemo(() => fn, deps)
,以上 useCallback
可替換成 useMemo
結果如下:
const?handleAdd2?=?useMemo(()?=>?{return?()?=>?setCount2(count2?+?1);
},?[count2]);
useMemo
把“創建”函數和依賴項數組作為參數傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助于避免在每次渲染時都進行高開銷的計算
使用注意:
傳入
useMemo
的函數會在渲染期間執行,不要在這個函數內部執行與渲染無關的操作如果沒有提供依賴項數組,
useMemo
在每次渲染時都會計算新的值
import?React,?{?useState,?useMemo?}?from?'react';function?counterText({?countInfo?})?{console.log(`${countInfo.name}?updated`);return?(<p>{countInfo.name}:?{countInfo.number}</p>);
}
//?//?使用?React.memo?檢查?props?變更,復用最近一次渲染結果
const?CounterText?=?React.memo(counterText);export?default?function?Counter()?{const?[count1,?setCount1]?=?useState(0);const?[count2,?setCount2]?=?useState(0);const?countInfo1?=?{name:?'count1',number:?count1};//?使用?useMemo?緩存最近一次計算結果,會在依賴項改變時才重新計算const?countInfo2?=?useMemo(()?=>?({name:?'count2',number:?count2}),?[count2]);return?(<><div><CounterText?countInfo={countInfo1}?/><button?onClick={()?=>?setCount1(count1?+?1)}>Add?count1</button></div><div><CounterText?countInfo={countInfo2}?/><button?onClick={()?=>?setCount2(count2?+?1)}>Add?count2</button></div></>);
}
其他 Hook
useImperativeHandle
useImperativeHandle
可以讓你在使用 ref
時自定義暴露給父組件的實例值。在大多數情況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle
應當與 React.forwardRef
一起使用:
import?React,?{?useRef,?useImperativeHandle,?useState?}?from?'react'function?FancyInput(props,?ref)?{const?inputRef?=?useRef();//?自定義暴露給父組件的?ref?實例值useImperativeHandle(ref,?()?=>?({focus:?()?=>?{inputRef.current.focus();}}));return?<input?ref={inputRef}?type="text"?{...props}?/>;
}
//?通過?forwardRef?向父組件傳遞暴露的?ref
const?ForwardFancyInput?=?React.forwardRef(FancyInput);export?default?function?Counter()?{const?[text,?setText]?=?useState('');const?inputRef?=?useRef();const?onInputFocus?=?()?=>?{inputRef.current.focus();};return?(<><ForwardFancyInput?ref={inputRef}?value={text}?onChange={e?=>?setText(e.target.value)}?/><button?onClick={onInputFocus}>Input?focus</button></>);
}
useLayoutEffect
useLayoutEffect
與 useEffect
類似,與 useEffect
在瀏覽器 layout 和 painting 完成后異步執行 effect 不同的是,它會在瀏覽器布局 layout 之后,painting 之前同步執行 effect
useLayoutEffect
的執行時機對比如下:
import?React,?{?useState,?useEffect,?useLayoutEffect?}?from?'react';export?default?function?LayoutEffect()?{const?[width,?setWidth]?=?useState('100px');//?useEffect?會在所有?DOM?渲染完成后執行?effect?回調useEffect(()?=>?{console.log('effect?width:?',?width);});//?useLayoutEffect?會在所有的?DOM?變更之后同步執行?effect?回調useLayoutEffect(()?=>?{console.log('layoutEffect?width:?',?width);});return?(<><div?id='content'?style={{?width,?background:?'red'?}}>內容</div><button?onClick={()?=>?setWidth('100px')}>100px</button><button?onClick={()?=>?setWidth('200px')}>200px</button><button?onClick={()?=>?setWidth('300px')}>300px</button></>);
}//?使用?setTimeout?保證在組件第一次渲染完成后執行,獲取到對應的?DOM
setTimeout(()?=>?{const?contentEl?=?document.getElementById('content');//?監視目標?DOM?結構變更,會在?useLayoutEffect?回調執行后,useEffect?回調執行前調用const?observer?=?new?MutationObserver(()?=>?{console.log('content?element?layout?updated');});observer.observe(contentEl,?{attributes:?true});
},?1000);
自定義Hook
通過自定義 Hook,可以將組件邏輯提取到可重用的函數中,在 Hook 特性之前,React 中有兩種流行的方式來共享組件之間的狀態邏輯:render props和高階組件,但此類解決方案會導致組件樹的層級冗余等問題。而自定義 Hook 的使用可以很好的解決此類問題
創建自定義 Hook
自定義 Hook 是一個函數,其名稱以 “use
” 開頭,函數內部可以調用其他的 Hook。以下就是實時獲取鼠標位置的自定義 Hook 實現:
import?{?useEffect,?useState?}?from?"react"export?const?useMouse?=?()?=>?{const?[position,?setPosition]?=?useState({x:?null,y:?null});useEffect(()?=>?{const?moveHandler?=?(e)?=>?{setPosition({x:?e.screenX,y:?e.screenY});};document.addEventListener('mousemove',?moveHandler);return?()?=>?{document.removeEventListener('mousemove',?moveHandler);};},?[]);return?position;
}
使用自定義 Hook
自定義 Hook 的使用規則與 Hook 使用規則基本一致,以下是 useMouse
自定義 Hook 的使用過程:
import?React?from?'react';
import?{?useMouse?}?from?'../hooks/useMouse';export?default?function?MouseMove()?{const?{?x,?y?}?=?useMouse();return?(<><p>Move?mouse?to?see?changes</p><p>x?position:?{x}</p><p>y?position:?{y}</p></>);
}
每次使用自定義 Hook 時,React 都會執行該函數來獲取獨立的 state 和執行獨立的副作用函數,所有 state 和副作用都是完全隔離的
參考文獻
[React Hooks 官方文檔](https://reactjs.org/docs/hooks-intro.html)
[詳解 React useCallback & useMemo](https://juejin.cn/post/6844904101445124110)
[Preventing rerenders with React.memo and useContext hook](https://github.com/facebook/react/issues/15156)
[MutationObserver MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)
[useLayoutEffect和useEffect的區別](https://zhuanlan.zhihu.com/p/348701319)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~