你應該理解的react知識點(更新於2021-5-10)

前言

最近在準備面試。複習了一些react的知識點,特此總結。vue

開始

React 生命週期

react 16之前的生命週期是這樣的react

組件在首次渲染時會被實例化,而後調用實例上面的componentWillMount,render和componentDidMount函數。組件在更新渲染時能夠調用componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render和componentDidUpdate函數。組件在卸載時能夠調用componentWillUnmount函數。面試

借圖:算法

image.png

從 React v16.3 開始,React 建議使用getDerivedStateFromPropsgetSnapshotBeforeUpdate兩個生命週期函數替代 componentWillMountcomponentWillReceivePropscomponentWillUpdate三個生命週期函數。這裏須要注意的是 新增的兩個生命週期 函數和原有的三個生命週期函數必須分開使用,不能混合使用api

目前的生命週期(借圖):數組

image.png

componentWillMount存在的問題瀏覽器

有人認爲在componentWillMount中能夠提早進行異步請求,避免白屏。可是react在調用render渲染頁面的時候,render並不會等待異步請求結束,再獲取數據渲染。這麼寫是有潛在隱患的。緩存

而在react fiber以後 可能在一次渲染中屢次調用。緣由是:react fiber技術使用增量渲染來解決掉幀的問題,經過requestIdleCallback調度執行每一個任務單元,能夠中斷和恢復,生命週期一旦中斷,恢復以後會從新跑一次以前的生命週期markdown

新的生命週期併發

static getDerivedStateFromProps

  • 觸發時間(v16.4修正):組件每次被rerender的時候,包括在組件構建以後(render以前最後執行),每次獲取新的props或state以後。在v16.3版本時,組件state的更新不會觸發該生命週期
  • 每次接收新的props以後都會返回一個對象做爲新的state,返回null則說明不須要更新state
  • 配合componentDidUpdate,能夠覆蓋componentWillReceiveProps的全部用法

getSnapshotBeforeUpdate

  • 觸發時間: update發生的時候,在render以後,在組件dom渲染以前。
  • 返回一個值,做爲componentDidUpdate的第三個參數。
  • 配合componentDidUpdate, 能夠覆蓋componentWillUpdate的全部用法。

React Fiber

因爲React渲染/更新過程一旦開始沒法中斷,持續佔用主線程,主線程忙於執行JS,無暇他顧(佈局、動畫),形成掉幀、延遲響應(甚至無響應)等不佳體驗。fiber應運而生。

Fiber 是對react reconciler(調和) 核心算法的重構。關鍵特性以下:

  • 增量渲染(把渲染任務拆分紅塊,勻到多幀)
  • 更新時可以暫停,終止,複用渲染任務
  • 給不一樣類型的更新賦予優先級
  • 併發方面新的基礎能力

增量渲染用來解決掉幀的問題,渲染任務拆分以後,每次只作一小段,作完一段就把時間控制權交還給主線程,而不像以前長時間佔用。

Fiber tree

  • Fiber以前的reconciler(被稱爲Stack reconciler)自頂向下的遞歸mount/update,沒法中斷(持續佔用主線程),這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,影響體驗。

  • Fiber解決這個問題的思路是把渲染/更新過程(遞歸diff)拆分紅一系列小任務,每次檢查樹上的一小部分,作完看是否還有時間繼續下一個任務,有的話繼續,沒有的話把本身掛起,主線程不忙的時候再繼續。

fiber樹實際上是一個單鏈表結構,child指向第一個子節點,return指向父節點,sibling指向下個兄弟節點。結構以下:

// fiber tree節點結構
{
    stateNode,
    child,
    return,
    sibling,
    ...
}
複製代碼

Fiber reconciler

reconcile過程分爲2個階段:

1.(可中斷)render/reconciliation 經過構造workInProgress tree得出change

2.(不可中斷)commit 應用這些DOM change(更新DOM樹、調用組件生命週期函數以及更新ref等內部狀態)

構建workInProgress tree的過程就是diff的過程,經過requestIdleCallback來調度執行一組任務,每完成一個任務後回來看看有沒有插隊的(更緊急的),每完成一組任務,把時間控制權交還給主線程,直到下一次requestIdleCallback回調再繼續構建workInProgress tree

生命週期也被分紅了兩個階段:

// 第1階段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate

// 第2階段 commit
componentDidMount
componentDidUpdate
componentWillUnmount
複製代碼

第1階段的生命週期函數可能會被屢次調用,默認以low優先級執行,被高優先級任務打斷的話,稍後從新執行。

fiber tree與workInProgress tree

雙緩衝技術:指的是workInProgress tree構造完畢,獲得的就是新的fiber tree,而後把current指針指向workInProgress tree,因爲fiber與workInProgress互相持有引用,舊fiber就做爲新fiber更新的預留空間,達到複用fiber實例的目的。

每一個fiber上都有個alternate屬性,也指向一個fiber,建立workInProgress節點時優先取alternate,沒有的話就建立一個

let workInProgress = current.alternate;
if (workInProgress === null) {
  //...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
} else {
  // We already have an alternate.
  // Reset the effect tag.
  workInProgress.effectTag = NoEffect;

  // The effect list is no longer valid.
  workInProgress.nextEffect = null;
  workInProgress.firstEffect = null;
  workInProgress.lastEffect = null;
}
複製代碼

這麼作的好處:

  • 可以複用內部對象(fiber)
  • 節省內存分配、GC的時間開銷

fiber 中斷 恢復

中斷:檢查當前正在處理的工做單元,保存當前成果(firstEffect, lastEffect),修改tag標記一下,迅速收尾並再開一個requestIdleCallback,下次有機會再作

斷點恢復:下次再處理到該工做單元時,看tag是被打斷的任務,接着作未完成的部分或者重作

P.S.不管是時間用盡「天然」中斷,仍是被高優任務粗暴打斷,對中斷機制來講都同樣。

React setState

在代碼中調用setState函數以後,React 會將傳入的參數對象與組件當前的狀態合併,而後觸發所謂的調和過程(Reconciliation)。通過調和過程,React 會以相對高效的方式根據新的狀態構建 React 元素樹而且着手從新渲染整個UI界面。在 React 獲得元素樹以後,React 會自動計算出新的樹與老樹的節點差別,而後根據差別對界面進行最小化重渲染。在差別計算算法中,React 可以相對精確地知道哪些位置發生了改變以及應該如何改變,這就保證了按需更新,而不是所有從新渲染。

setState調用時有時是同步的(settimeout,自定義dom事件),有時是異步的(普通調用)

React 事件機制

React事件是經過事件代理,在最外層的 document上對事件進行統一分發,並無綁定在真實的 Dom節點上。 並且react內部對原生的Event對象進行了包裹處理。具備與瀏覽器原生事件相同的接口,包括 stopPropagation()preventDefault()

image.png

React 更新隊列

若是有多個同步setState(...)操做,React 會將它們的更新(update)前後依次加入到更新隊列(updateQueue),在應用程序的 render 階段處理更新隊列時會將隊列中的全部更新合併成一個,合併原則是相同屬性的更新取最後一次的值。若是有異步setState(...)操做,則先進行同步更新,異步更新則遵循 EventLoop 原理後續處理。

React 更新

// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdate(expirationTime, suspenseConfig) {
  var update = {
    // 過時時間與任務優先級相關聯
    expirationTime: expirationTime,
    suspenseConfig: suspenseConfig,
		// tag用於標識更新的類型如UpdateState,ReplaceState,ForceUpdate等
    tag: UpdateState,
    // 更新內容
    payload: null,
    // 更新完成後的回調
    callback: null,
		// 下一個更新(任務)
    next: null,
    // 下一個反作用
    nextEffect: null
  };
  {
    // 優先級會根據任務體系中當前任務隊列的執行狀況而定
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}
複製代碼

每個更新對象都有本身的過時時間(expirationTime)、更新內容(payload),優先級(priority)以及指向下一個更新的引用(next)。其中當前更新的優先級由任務體系統一指定。

React 更新隊列

// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdateQueue(baseState) {
  var queue = {
    // 當前的state
    baseState: baseState,
    // 隊列中第一個更新
    firstUpdate: null,
    // 隊列中的最後一個更新
    lastUpdate: null,
    // 隊列中第一個捕獲類型的update
    firstCapturedUpdate: null,
    // 隊列中第一個捕獲類型的update
    lastCapturedUpdate: null,
    // 第一個反作用
    firstEffect: null,
    // 最後一個反作用
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}
複製代碼

這是一個單向鏈表結構。

當咱們使用setState()時, React 會建立一個更新(update)對象,而後經過調用enqueueUpdate函數將其加入到更新隊列(updateQueue)

// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
// 每次setState都會建立update併入updateQueue
function enqueueUpdate(fiber, update) {
  // 每一個Fiber結點都有本身的updateQueue,其初始值爲null,通常只有ClassComponent類型的結點updateQueue纔會被賦值
  // fiber.alternate指向的是該結點在workInProgress樹上面對應的結點
  var alternate = fiber.alternate;
  var queue1 = void 0;
  var queue2 = void 0;
  if (alternate === null) {
    // 若是fiber.alternate不存在
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // 若是fiber.alternate存在,也就是說存在current樹上的結點和workInProgress樹上的結點都存在
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // 若是兩個結點上面均沒有updateQueue,則爲它們分別建立queue
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
      } else {
        // 若是隻有其中一個存在updateQueue,則將另外一個結點的updateQueue克隆到該結點
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // 若是隻有其中一個存在updateQueue,則將另外一個結點的updateQueue克隆到該結點
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // 若是兩個結點均有updateQueue,則不須要處理
      }
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // 通過上面的處理後,只有一個queue1或者queue1 == queue2的話,就將更新對象update加入到queue1
    appendUpdateToQueue(queue1, update);
  } else {
    // 通過上面的處理後,若是兩個queue均存在
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // 只要有一個queue不爲null,就須要將將update加入到queue中
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // 若是兩個都不是空隊列,因爲兩個結構共享,因此只在queue1加入update
      appendUpdateToQueue(queue1, update);
      // 仍然須要在queue2中,將lastUpdate指向update
      queue2.lastUpdate = update;
    }
  }
  ...
}
  
function appendUpdateToQueue(queue, update) {
  if (queue.lastUpdate === null) {
    // 若是隊列爲空,則第一個更新和最後一個更新都賦值當前更新
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    // 若是隊列不爲空,將update加入到隊列的末尾
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}
複製代碼

enqueueUpdate函數中,React 將更新加入到更新隊列時會同時維護兩個隊列對象 queue1 和 queue2,其中 queue1 是應用程序運行過程當中 current 樹上當前 Fiber 結點最新隊列,queue2 是應用程序上一次更新時(workInProgress 樹)Fiber 結點的更新隊列,它們之間的相互邏輯是下面這樣的。

  • queue1 取的是fiber.updateQueue,queue2 取的是fiber.alternate.updateQueue
  • 若是二者均爲null,則調用createUpdateQueue(...)獲取初始隊列;
  • 若是二者之一爲null,則調用cloneUpdateQueue(...)從對方中獲取隊列;
  • 若是二者均不爲null,則將update做爲lastUpdate加入 queue1 中。

React 處理更新隊列

React 應用程序運行到 render 階段時會處理更新隊列,處理更新隊列的函數是processUpdateQueue

// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {
  ...
  // 從隊列中取出第一個更新
  var update = queue.firstUpdate;
  var resultState = newBaseState;
  // 遍歷更新隊列,處理更新
  while (update !== null) {
    ...
    // 若是第一個更新不爲空,緊接着要遍歷更新隊列
    // getStateFromUpdate函數用於合併更新,合併方式見下面函數實現
    resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);
    ...
    update = update.next;
  }
  ...
  // 設置當前fiber結點的memoizedState
  workInProgress.memoizedState = resultState;
  ...
}

// 獲取下一個更新對象並與現有state對象合併
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
  switch (update.tag) {
      case UpdateState:
      	{
        	var _payload2 = update.payload;
        	var partialState = void 0;
        	if (typeof _payload2 === 'function') {
          	// setState傳入的參數_payload2類型是function
          	...
         	 partialState = _payload2.call(instance, prevState, nextProps);
          	...
        	} else {
          	// setState傳入的參數_payload2類型是object
          	partialState = _payload2;
        	}
        	// 合併當前state和上一個state.
       	 return _assign({}, prevState, partialState);
      }
  }
}
複製代碼

processUpdateQueue函數用於處理更新隊列,在該函數內部使用循環的方式來遍歷隊列,經過update.next依次取出更新(對象)進行合併,合併更新對象的方式是:

  • 若是setState傳入的參數類型是function,則經過payload2.call(instance, prevState, nextProps)獲取更新對象;
  • 若是setState傳入的參數類型是object,則可直接獲取更新對象;
  • 最後經過使用Object.assign()合併兩個更新對象並返回,若是屬性相同的狀況下則取最後一次值。

React 頁面渲染

React 應用程序首次渲染時在 prerender 階段會初始化 current 樹。最開始的 current 樹只有一個根結點— HostRoot類型的 Fiber 結點。在後面的 render 階段會根據此時的 current 樹建立 workInProgress 樹。在 workInProgress 樹上面進行一系列運算(計算更新等),最後將反作用列表(Effect List)傳入到 commit 階段。當 commit 階段運行完成後將當前的 current 樹替換爲 workInProgress 樹,至此一個更新流程就完成了。簡述:

  • 在 render 階段 React 依賴 current 樹經過工做循環(workLoop)構建 workInProgress 樹;
  • 在 workInProgress 樹進行一些更新計算,獲得反作用列表(Effect List);
  • 在 commit 階段將反作用列表渲染到頁面後,將 current 樹替換爲 workInProgress 樹(執行current = workInProgress)。

current 樹是未更新前應用程序對應的 Fiber 樹,workInProgress 樹是須要更新屏幕的 Fiber 樹。

FiberRootNode構造函數只有一個實例就是 fiberRoot 對象。而每一個 Fiber 節點都是 FiberNode 構造函數的實例,它們經過return,child和sibling三個屬性鏈接起來,造成了一個巨大鏈表。React 對每一個節點的更新計算都是在這個鏈表上完成的。React 在對 Fiber 節點標記更新標識的時候的作法就是爲節點的effectTag屬性賦不一樣的值。

React hooks 實現原理

借圖:

image.png

react hooks 原理

React useCallback useMemo 區別

這兩個api,其實概念上仍是很好理解的,一個是「緩存函數」, 一個是緩存「函數的返回值」。

  • 在組件內部,那些會成爲其餘useEffect依賴項的方法,建議用 useCallback 包裹,或者直接編寫在引用它的useEffect中。
  • 己所不欲勿施於人,若是你的function會做爲props傳遞給子組件,請必定要使用 useCallback 包裹,對於子組件來講,若是每次render都會致使你傳遞的函數發生變化,可能會對它形成很是大的困擾。同時也不利於react作渲染優化。
  • 對於使用高階函數的場景,建議一概使用 useMemo

React 捕獲錯誤的生命週期

  • componentDidCatch
  • static getDerivedStateFromError()

爲何必須在函數組件頂部做用域調用Hooks API 爲何不能在循環和判斷裏用?

  • Hook API調用會產生一個對應的Hook實例(並追加到Hooks鏈),可是返回給組件的是state和對應的setter,re-render時框架並不知道這個setter對應哪一個Hooks實例(除非用HashMap來存儲Hooks,但這就要求調用的時候把相應的key傳給React,會增長Hooks使用的複雜度)。
  • re-render時會從第一行代碼開始從新執行整個組件,即會按順序執行整個Hooks鏈,由於首次render以後,只能經過useState返回的dispatch修改對應Hook的memoizedState,所以必需要保證Hooks的順序不變,因此不能在分支調用Hooks,只有在頂層調用才能保證各個Hooks的執行順序!

一旦在條件語句中聲明hooks,在下一次函數組件更新,hooks鏈表結構,將會被破壞,current樹的memoizedState緩存hooks信息,和當前workInProgress不一致,若是涉及到讀取state等操做,就會發生異常。

總結:能夠認爲維護了一個state數組 和一個cursor指針 。第一次渲染時把當前的值push進states數組裏,把綁定了指針的setter推動 setters 數組中。每次的後續渲染都會重置指針cursor的位置,並會從每一個數組中讀取對應的值。每一個 setter 都會有一個對應的指針位置的引用,所以當觸發任何 setter 調用的時候都會觸發去改變狀態數組中的對應的值。若是放在條件語句中。那麼setstate的順序就會發生錯亂。

let first = true;
      const [num1, setNum1] = usestate(1);
      if (first) {
        const [num2, setNum2] = usestate(2);
        first = false
      }
      const [num3, setNum3] = usestate(3);

複製代碼

第一次渲染時,維護的states數組時[1,2,3] ,setters數組指針指向[state[0],state[1],state[2]]。可是當咱們的組件更新時,這時候的states數組是[1,2,3] setters數組是[state[0],state[1]]。顯然咱們的setNum3被設置成了2。

React 高階組件

  • 高階組件不是組件,它是一個將某個組件轉換成另外一個組件的純函數。
  • 高階組件的主要做用是實現代碼複用和邏輯抽象、對 state 和 props 進行抽象和操做、對組件進行細化(如添加生命週期)、實現渲染劫持等。在實際的業務場景中合理的使用高階組件,能夠提升開發效率和提高代碼的可維護性。
  • 高階組件的實用性 使其頻繁地被大量 React.js 相關的第三方庫,如 React-Redux的 connect 方法、React-Loadable等所使用,瞭解高階組件對咱們理解各類 React.js 第三方庫的原理頗有幫助 👍。
  • 高階組件有兩種實現方式,分別是屬性代理和反向繼承。它能夠看做是裝飾器模式在 React 中的實現:在不修改原組件的狀況下實現組件功能的加強。

React 和 vue對比

vue react 共同點:

  • 使用 Virtual DOM,有本身的diff渲染算法
  • 提供了響應式 (Reactive) 和組件化 (Composable) 的視圖組件。
  • 將注意力集中保持在覈心庫,而將其餘功能如路由和全局狀態管理交給相關的庫。

1. 監聽數據變化的實現原理不一樣

  • React 默認是經過比較引用的方式進行的,若是不優化(PureComponent/shouldComponentUpdate)可能致使大量沒必要要的VDOM的從新渲染
  • Vue 經過 getter/setter 以及一些函數的劫持,能精確知道數據變化,不須要特別的優化就能達到很好的性能

2. 數據流的不一樣

  • vue是雙向綁定
  • react是單向數據流

3. 組件通訊的區別

4. 模板渲染方式的不一樣

  • React是在組件JS代碼中,經過原生JS實現模板中的常見語法,好比插值,條件,循環等,都是經過JS語法實現的
  • Vue是在和組件JS代碼分離的單獨的模板中,經過指令來實現的,好比條件語句就須要 v-if 來實現
相關文章
相關標籤/搜索