useEffect和useLayoutEffect是React官方推出的兩個hooks,都是用來執行反作用的鉤子函數,名字相似,功能相近,惟一不一樣的就是執行的時機有差別,今天這篇文章主要是從這兩個鉤子函數的執行時機入手,來剖析一下React的運行原理和瀏覽器的渲染流程。javascript
useLayoutEffect
其函數簽名與 useEffect
相同,但它會在全部的 DOM 變動以後同步調用 effect。可使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製以前, useLayoutEffect
內部的更新計劃將被同步刷新,儘量使用標準的 useEffect
以免阻塞視覺更新。java
簡單來說,就是:useEffect是異步的,useLayoutEffect是同步的,異(同)步是相對於瀏覽器執行刷新屏幕Task來講的。react
下面將經過一個簡單的demo示例來講明具體的執行過程,其中React是16.13.1版本,首先是示例代碼:web
import React, { useState, useEffect, useLayoutEffect } from 'react'; const EffectDemo = () => { const [count, setCount] = useState(0); useEffect(function useEffectDemo() { console.log('useEffect:', count); }, [count]); useLayoutEffect(function useLayoutEffectDemo() { console.log('useLayoutEffect:', count); }, [count]); return ( <div> <button onClick={() => { setCount(count + 1); }} >click me</button> </div> ); }; export default EffectDemo;
功能很簡單,就不作界面展現,這裏主要是看一下瀏覽器控制檯Performance的監控圖:
經過兩個hooks的執行圖能夠看出,useLayoutEffect發生在頁面渲染到屏幕(用戶可見)以前,useEffect發生在那以後,中間還經歷了DCL,FCP,FMP,LCP階段,除開DCL(DomContentLoaded)以外,這些指標是RAIL模型衡量頁面性能的標準,總的來講,渲染到屏幕的階段是一個分水嶺,那麼渲染包含什麼呢,仍是看圖吧:
此階段完成了樣式的計算(Recalculate Style)和佈局(Layout),緊接着是一個Task,完成Update Layer Tree,Paint,Composite Layers,通過這一系列的任務後,頁面最終呈現給用戶,能夠用一張圖來表示瀏覽器的渲染過程:
後面會有相關學習資料,這裏就不展開細說了。瀏覽器
在深刻了解React的運行以前,首先在本地寫一個簡單的示例,大體模擬文章開始的例子:數據結構
<body> <div id="app"></div> <script type="text/javascript"> (function iife(){ function render() { var appNode = document.querySelector('#app'); var textNode = document.createElement('span'); textNode.id = 'tip'; textNode.textContent = 'hello'; appNode.appendChild(textNode); } function useLayoutEffectDemo() { console.log('useLayoutEffectDemo', document.querySelector('#tip')); } function useEffectDemo() { console.log('useEffectDemo'); } render(); useLayoutEffectDemo(); setTimeout(useEffectDemo, 0); })(); </script> </body>
而後啓用Performance監控渲染狀況:
架構
總結一下:
1.首先運行render,完成後當即執行useLayoutEffectDemo函數(雖然已經插入DOM,可是界面尚未渲染出來);
2.註冊異步回調函數useEffectDemo,該函數將在0ms事後加入EventLoop中的宏任務隊列;
3.頁面開始渲染:Recalculate Style->Layout->Update Layer Tree->Paint->Composite Layers->GPU繪製;
4.取出宏任務useEffectDemo,執行回調;app
React的執行比這個模擬示例複雜不少,可是抽象出的流程節點大同小異,瞭解以後,咱們能夠繼續深刻挖掘React的運行機制了。frontend
React渲染頁面分爲兩個階段:
1.調度階段(reconciliation):找出須要更新的節點元素
2.渲染階段(commit):將須要更新的元素插入DOM
接下來就跟着React的運行流程來具體看下不一樣階段的執行狀況:dom
簡單總結一下:
1.react-dom負責Fiber節點的建立,最終造成一個Fiber節點樹,其中每一個Fiber包含須要執行的反作用和渲染到屏幕的DOM對象;
2.調用scheduler暴露的方法註冊須要調度的事件;
3.執行DOM插入;
4.執行useLyaoutEffect或者ClassComponent的生命週期函數;
5.瀏覽器接過控制權,執行渲染;
6.scheduler執行調度任務,執行useEffectDemo;
以上就是總體流程,接下來再深刻一點,看看useEffect和useLayoutEffect是怎麼解析和執行的:
從上圖可知,uesEffect和useLayoutEffect最終都會調用mountEffectImpl函數,而後初始化/更新Fiber的updateQueue,能夠看一下mountEffectImpl函數是怎樣的:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber$1.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps); }
都認識,可是不知道是幹嗎的,好吧,仍是用一張圖來講明吧:
這個函數的功能以下:
1.建立hook對象,放入到workInProgressHook鏈表中;
2.Fiber的updateQueue和上一步建立的hook關聯,這樣每個Fiber對象上就知道要執行Effect了;
那麼workInProgressHook是幹嗎的呢,看下源代碼的解釋吧:
var workInProgressHook = null; // Whether an update was scheduled at any point during the render phase. This // does not get reset if we do another render pass; only when we're completely // finished evaluating this component. This is an optimization so we know // whether we need to clear render phase updates after a throw.
上面說到updateQueue,最終咱們寫的useEffectDemo和useLayoutEffectDemo都會放在這裏,那麼是怎麼一個結構存儲的呢,能夠打印看一下:
其實就是一個收尾相連的環形結構,爲何要這麼設計呢,你們看下commitHookEffectListMount執行函數的遍歷方式就知道了:
function commitHookEffectListMount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Mount var create = effect.create; effect.destroy = create(); { var destroy = effect.destroy; if (destroy !== undefined && typeof destroy !== 'function') { var addendum = void 0; if (destroy === null) { addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).'; } else if (typeof destroy.then === 'function') { addendum = '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching'; } else { addendum = ' You returned: ' + destroy; } error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork)); } } } effect = effect.next; } while (effect !== firstEffect); } }
這裏根據effect的tag不一樣決定執行哪種effect,這裏咱們的useEffectDemo和useLayoutEfectDemo的tag分別是5和3,所以須要執行useEffect中的反作用函數時,commitHookEffectListMount的tag確定就是5了,執行useLayoutEffect中的反作用函數時,commitHookEffectListMount的tag確定就是3。
總的來講全部的useEffect和useLayoutEffect的反作用函數都是在這裏執行的,經過tag來控制他們的執行時機。
其實上面已經講了commitHookEffectListMount的執行,這裏再看下具體的執行過程:
執行useEffect的入口:
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } ...... }
執行useLayoutEffect的入口:
function commitPassiveHookEffects(finishedWork) { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { ...... commitHookEffectListMount(Passive$1 | HasEffect, finishedWork); break; } } } }
能夠看出兩個執行入口傳入的第一個入參tag是不同的,最終執行的反作用函數就區分開來了。
如今你們應該對useEffect和useLayoutEffect的執行有了一個大體的瞭解,那麼還有一個關於scheduler異步調度的小問題,本文最開始模擬的一個例子裏是經過setTimeout來完成的,React中則是經過MessageChannel來實現的,若是不熟悉能夠查查使用方式,這裏來看下異步執行的過程:
瀏覽器的渲染是一個十分複雜的過程,若是不是很瞭解,能夠瀏覽谷歌提供的介紹文章,連接以下:https://developers.google.cn/web/fundamentals/performance/rendering
瞭解了瀏覽器的基本渲染以後,能夠更加深刻窺探瀏覽器的運行,首先上一張圖:
上面這幅圖是來源於https://aerotwist.com/blog/the-anatomy-of-a-frame
這裏還給你們推薦一篇講解瀏覽器渲染的文章:https://juejin.im/entry/6844903476506394638
在學習Hooks的時候,不免會和class組件中的生命週期作比較,這裏咱們只關注useEffect,useEffect在某些程度上至關於componentDidMount
、 componentDidUpdate
、 componentWillUnmount
三個鉤子函數的集合,由於這些函數都會阻塞瀏覽器的渲染,其中componentDidMount
、 componentDidUpdate
的執行是在哪裏呢,看一下上面提到的commitLifeCycles函數就清楚了(componentWillUnmount你們有興趣本身找找吧);
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } case ClassComponent: { var instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { // 初次渲染 ...... instance.componentDidMount(); stopPhaseTimer(); } else { // 更新渲染 ...... instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate); stopPhaseTimer(); } }