若是你最近一年出去面過試,極可能面臨這些問題:html
第一個問題若是你提到了Fiber reconciler,fiber,鏈表,新的什麼週期,可能在面試官眼裏這僅僅是一個及格的回答。如下是我整理的,自我感受還良好的回答:前端
分三步:react
命令式
編程轉變爲聲明式
編程,即所謂的數據驅動視圖,但若是簡單粗暴的操做,好比講生成的html直接採用innerHtml替換,會帶來重繪重排
之類的性能問題。爲了儘可能提升性能,React團隊引入了虛擬dom,即採用js對象來描述dom樹,經過對比先後兩次的虛擬對象,來找到最小的dom操做(vdom diff),以此提升性能。stack reconciler
,它是一個遞歸的過程,在樹很深的時候,單次diff時間過長會形成JS線程持續被佔用,用戶交互響應遲滯,頁面渲染會出現明顯的卡頓,這在現代前端是一個致命的問題。因此爲了解決這種問題,react 團隊對整個架構進行了調整,引入了fiber架構,將之前的stack reconciler替換爲fiber reconciler
。採用增量式渲染
。引入了任務優先級(expiration)
和requestIdleCallback
的循環調度算法,簡單來講就是將之前的一根筋diff更新,首先拆分紅兩個階段:reconciliation
與commit
;第一個reconciliation
階段是可打斷的,被拆分紅一個個的小任務(fiber),在每一偵的渲染空閒期作小任務diff。而後是commit階段,這個階段是不拆分且不能打斷的,將diff節點的effectTag一口氣更新到頁面上。workInprogressTree
(future vdom) 與 oldTree
(current vdom)兩個鏈表,兩個鏈表相互引用。這無形中又解決了另外一個問題,當workInprogressTree生成報錯時,這時也不會致使頁面渲染崩潰,而只是更新失敗,頁面仍然還在。以上就是我上半年面試本身不斷總結迭代出的答案,但願能對你有所啓發。git
接着來回答第二個問題,hooks本質是什麼?github
當咱們在談論React這個UI庫時,最早想到的是,數據驅動視圖,簡單來說就是下面這個公式:面試
view = fn(state)
咱們開發的整個應用,都是不少組件組合而成,這些組件是純粹,不具有擴展的。由於React不能像普通類同樣直接繼承,從而達到功能擴展的目的。算法
在用react實現業務時,咱們複用一些組件邏輯去擴展另外一個組件,最多見好比Connect,Form.create, Modal。這類組件一般是一個容器,容器內部封裝了一些通用的功能(非視覺的佔多數),容器裏面的內容由被包裝的組件本身定製,從而達到必定程度的邏輯複用。編程
在hooks 出現以前,解決這類需求最經常使用的就兩種模式:HOC高階組件
和 Render Props
。redux
高階組件相似於JS中的高階函數,即輸入一個函數,返回一個新的
函數, 好比React-Redux中的Connect:性能優化
class Home extends React.Component { // UI } export default Connect()(Home);
高階組件因爲每次都會返回一個新的組件,對於react來講,這是不利於diff和狀態複用的,因此高階組件的包裝不能在render 方法中進行,而只能像上面那樣在組件聲明時包裹,這樣也就不利於動態傳參。而Render Props模式的出現就完美解決了這個問題,其原理就是將要包裹的組件做爲props屬性傳入,而後容器組件調用這個屬性,並向其傳參, 最多見的用props.children
來作這個屬性。舉個🌰:
class Home extends React.Component { // UI } <Route path = "/home" render= {(props) => <Home {...props} } />
更多關於render 與 Hoc,能夠參見之前寫的一片弱文:React進階,寫中後臺也能寫出花
上面提到的高階組件和RenderProps, 看似解決了邏輯複用的問題,但面對複雜需求時,即一個組件須要使用多個複用包裹時,兩種方案都會讓咱們的代碼陷入常見的嵌套地獄
, 好比:
class Home extends React.Component { // UI } export default Connect()(Form.create()(Home));
除了嵌套地獄的寫法讓人困惑,但更致命的深度會直接影響react組件更新時的diff性能。
Hooks 出現前的函數式組件只是以模板函數存在,而前面兩種方案,某種程度都是依賴類組件來完成。而提到了類,就不得不想到下面這些痛點:
因此React團隊迴歸view = fn(state)
的初心,但願函數式組件也能擁有狀態管理的能力,讓邏輯複用變得更簡單,更純粹。
爲何在React 16前,函數式組件不能擁有狀態管理?其本質是由於16之前只有類組件在更新時存在實例,而16之後Fiber 架構的出現,讓每個節點都擁有對應的實例,也就擁有了保存狀態的能力,下面會詳講。
有可能,你聽到過Hooks的本質就是閉包
。可是,若是滿分100的話,這個說法最多隻能得60分。
哪滿分答案是什麼呢?閉包 + 兩級鏈表
。
下面就來一一分解, 下面都以useState來舉例剖析。
JS 中閉包是難點,也是必考點,歸納的講就是:
閉包是指有權訪問另外一個函數做用域中變量或方法
的函數,建立閉包的方式就是在一個函數內建立閉包函數,經過閉包函數訪問這個函數的局部變量, 利用閉包能夠突破做用鏈域的特性,將函數內部的變量和方法
傳遞到外部。
export default function Hooks() { const [count, setCount] = useState(0); const [age, setAge] = useState(18); const self = useRef(0); const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []); console.log('self', self.current); return ( <div> <h2>年齡: {age} <a onClick={onClick}>增長</a></h2> <h3>輪次: {count} <a onClick={() => setCount(count => count + 1)}>增長</a></h3> </div> ); }
以上面的示例來說,閉包就是setAge這個函數,何以見得呢,看組件掛載階段hook執行的源碼:
// packages/react-reconciler/src/ReactFiberHooks.js function mountReducer(reducer, initialArg, init) { const hook = mountWorkInProgressHook(); let initialState; if (init !== undefined) { initialState = init(initialArg); } else { initialState = initialArg; } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { last: null, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: initialState, }); // 重點 const dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ))); return [hook.memoizedState, dispatch]; }
因此這個函數就是mountReducer,而產生的閉包就是dispatch函數(對應上面的setAge),被閉包引用的變量就是currentlyRenderingFiber
與 queue
。
The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook
);這個閉包將 fiber節點與action, action 與 state很好的串聯起來了,舉上面的例子就是:
ok,到這,閉包就講完了。
在ReactFiberHooks文件開頭聲明currentHook變量的源碼有這樣一段註釋。
/* Hooks are stored as a linked list on the fiber's memoizedState field. hooks 以鏈表的形式存儲在fiber節點的memoizedState屬性上 The current hook list is the list that belongs to the current fiber. 當前的hook鏈表就是當前正在遍歷的fiber節點上的 The work-in-progress hook list is a new list that will be added to the work-in-progress fiber. work-in-progress hook 就是即將被添加到正在遍歷fiber節點的hooks新鏈表 */ let currentHook: Hook | null = null; let nextCurrentHook: Hook | null = null;
從上面的源碼註釋能夠看出hooks鏈表與fiber鏈表是極其類似的;也得知hooks 鏈表是保存在fiber節點的memoizedState屬性的, 而賦值是在renderWithHooks函數具體實現的;
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any { renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; // 獲取當前節點的hooks 鏈表; nextCurrentHook = current !== null ? current.memoizedState : null; // ...省略一萬行 }
有可能代碼貼了這麼多,你還沒反應過來這個hooks 鏈表具體指什麼?
其實就是指一個組件包含的hooks, 好比上面示例中的:
const [count, setCount] = useState(0); const [age, setAge] = useState(18); const self = useRef(0); const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []);
造成的鏈表就是下面這樣的:
因此在下一次更新時,再次執行hook,就會去獲取當前運行節點的hooks鏈表;
const hook = updateWorkInProgressHook(); // updateWorkInProgressHook 就是一個純鏈表的操做:指向下一個 hook節點
到這 hooks 鏈表是什麼,應該就明白了;這時你可能會更明白,爲何hooks不能在循環,判斷語句中調用,而只能在函數最外層使用,由於掛載或則更新時,這個隊列須要是一致的,才能保證hooks的結果正確。
其實state 鏈表不是hooks獨有的,類操做的setState也存在,正是因爲這個鏈表存在,因此有一個經(sa)典(bi)React 面試題:
setState爲何默認是異步,何時是同步?
結合實例來看,當點擊增長會執行三次setAge
const onClick = useCallback(() => { setAge(19); setAge(20); setAge(21); }, []);
第一次執行完dispatch後,會造成一個狀態待執行任務鏈表:
若是仔細觀察,會發現這個鏈表仍是一個環
(會在updateReducer後斷開), 這一塊設計至關有意思,我如今也還沒搞明白爲何須要環,值得細品,而創建這個鏈表的邏輯就在dispatchAction函數中。
function dispatchAction(fiber, queue, action) { // 只貼了相關代碼 const update = { expirationTime, suspenseConfig, action, eagerReducer: null, eagerState: null, next: null, }; // Append the update to the end of the list. const last = queue.last; if (last === null) { // This is the first update. Create a circular list. update.next = update; } else { const first = last.next; if (first !== null) { // Still circular. update.next = first; } last.next = update; } queue.last = update; // 觸發更新 scheduleWork(fiber, expirationTime); }
上面已經說了,執行setAge 只是造成了狀態待執行任務鏈表,真正獲得最終狀態,實際上是在下一次更新(獲取狀態)時,即:
// 讀取最新age const [age, setAge] = useState(18);
而獲取最新狀態的相關代碼邏輯存在於updateReducer中:
function updateReducer(reducer, initialArg,init?) { const hook = updateWorkInProgressHook(); const queue = hook.queue; // ...隱藏一百行 // 找出第一個未被執行的任務; let first; // baseUpdate 只有在updateReducer執行一次後纔會有值 if (baseUpdate !== null) { // 在baseUpdate有值後,會有一次解環的操做; if (last !== null) { last.next = null; } first = baseUpdate.next; } else { first = last !== null ? last.next : null; } if (first !== null) { let newState = baseState; let newBaseState = null; let newBaseUpdate = null; let prevUpdate = baseUpdate; let update = first; let didSkip = false; // do while 遍歷待執行任務的狀態鏈表 do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // 優先級不足,先標記,後面再更新 } else { markRenderEventTimeAndConfig( updateExpirationTime, update.suspenseConfig, ); // Process this update. if (update.eagerReducer === reducer) { // 簡單的說就是狀態已經計算過,那就直接用 newState = update.eagerState; } else { const action = update.action; newState = reducer(newState, action); } } prevUpdate = update; update = update.next; // 終止條件是指針爲空 或 環已遍歷完 } while (update !== null && update !== first); // ...省略100行 return [newState, dispatch]; } }
最後來看,狀態更新的邏輯彷佛是最繞的。但若是看過setState,這一塊可能就比較容易。至此,第二個鏈表state就理清楚了。
讀到這裏,你就應該明白hooks 究竟是怎麼實現的:
閉包加兩級鏈表
雖然我這裏只站在useState這個hooks作了剖析,但其餘hooks的實現基本相似。
另外分享一下在我眼中的hooks,與類組件到底究竟是什麼聯繫:
子組件的Effect先執行
), Update須要deps依賴來喚起;閉包
的坑Rerender
第一次寫源碼解析,出發點主要兩點:
文章中如有不詳或不對之處,歡迎斧正;