React16 瞭解源碼系列(一)

前置知識

使用react也有將近一年了,在使用的過程當中,我相信你也會像我同樣,存在不少疑惑的點; 舉些例子,好比爲何react有些生命鉤子會執行屢次,而有些只會安全的執行一次?react 16 大版本更新的fiber究竟是個什麼東西?諸如此類的問題,我也百思不得其解;因此我踏上了探究源碼之路;html

總所周知,react源碼不是通常的多,直接閱讀react源碼,真的是勸退... 在蒐集react源碼資料的時候,發現比較全和新的資料也不多,偶然一次機會看到奇舞團大佬按照react源碼思路本身debug造了一個;在學習他的源碼時候,我也私下和他交流了不少,真的是聽君一席話,勝讀十年書呀;2333....(仍是本身太菜了,很是感謝大佬解答個人問題)vue

言歸正傳,這裏強烈推薦他電子書,源碼系列文章是基於最新的16.13.1解析的; 雖然沒有更完,可是寫得至關精彩,反正我是看了還想看那種。(有點崔更了,哈哈)react

本文是根據最新的16.13.1進行解析,目的是把總體的源碼流程看懂個大概,並不會深刻到很細節的東西; 也就是說把react的總體更新流程弄明白,能夠幫助你更好的去探究最終的源碼細節,若是有不正確的地方,還望大佬們指正; 雖然說如今更新到了16.13.1版本了,可是總體的架構依然沒有變,這裏我推薦幾個必讀的資料,很精彩;git

  1. Lin Clark - A Cartoon Intro to Fiber - React Conf 2017
  2. 這多是最通俗的 React Fiber(時間分片) 打開方式
  3. Deep In React 之淺談 React Fiber 架構(一)

React16架構

在瞭解react架構以前,咱們還須要瞭解一下瀏覽器渲染原理,主流的瀏覽器刷新頻率爲60Hz,即每(1000ms / 60Hz)16.6ms瀏覽器刷新一次。咱們知道,JS是能夠操做DOM的,因此JS腳本執行和瀏覽器佈局、繪製是處於同一線程(渲染線程)。 也就是瀏覽器在一幀的時間內要完成如下工做github

  1. JS腳本執行
  2. 樣式佈局
  3. 樣式繪製 當JS執行時間過長,超出了16.6ms,此次刷新就沒有時間執行樣式佈局和樣式繪製了。這就是形成卡頓的緣由

在16大版本以前,也就是React15架構只分爲兩層,Reconciler(協調器,可不中斷)+ Renderer(渲染器,不可中斷);也就是說協調階段,同步(遞歸更新完)更新的;這很容易形成JS執行時間過長,超出了16.6ms,也就是說一旦開始更新,就不可中斷,一口氣作完。會形成卡頓,這樣的用戶體驗很是差;算法

react團隊發現,讓用戶操做感受不到卡頓,操做之外的有延遲,卡頓一下,用戶是徹底能夠接受的;JS執行時間過長,因此react更改了架構;React16架構能夠分爲三層:數組

  1. Scheduler(調度器,可中斷)—— 調度任務的優先級,高優任務優先進入Reconciler
  2. Reconciler(協調器,可中斷)—— 負責找出變化的組件
  3. Renderer(渲染器,不可中斷)—— 負責將變化的組件渲染到頁面上
  • 這樣的三層架構,我的以爲有如下幾個優勢
  1. 像計算機網路協議同樣,每一層專一干一件事情(單一職責),這樣架構的應用,生命週期都相對的長;TCP/IP協議不是活了幾十年了嘛。QAQ
  2. 可擴展性和靈活性很強;給開發者保留了不少底層抽象的可能;(antd 就是一個例子)
  3. 熟悉react框架之後,轉其餘框架相對輕鬆,由於react是最先出現的主流框架。(該懂的應該都懂)
  • 固然也會有一些很是明顯的缺點
  1. 學習成本的提升,像新出的hook,和將來即將穩定的Concurrent 模式,都存在必定的學習成本;
  2. react並無作不少優化工做,好比在編譯階段,像vue這樣的框架就作了相應的優化;不過這也是框架和庫的區別;由於react的定位始終是庫,react核心開發人員dan本身也說過,將來的發展不會把react變成框架;

初始化階段

要理解react的更新流程,我以爲最好的方式是畫流程圖,結合一點源碼註釋;否則在學習源碼的過程會很是的混亂;先看react的初始化階段。瀏覽器

  1. reactDOM.render,還記得應用掛載的時候麼?
  • 應用掛載時候的入口
// ReactDOM.render(<App name="Hello"/>, document.querySelector('#app'));
  const ReactDOM = {
  render(element, container) {
    // 建立 FiberRoot
    const root = container._reactRootContainer = new ReactRoot(container);
    // 首次渲染不須要批量更新
    DOMRenderer.unbatchedUpdates(() => {
      //調用 FiberRoot 的render方法開始渲染
      root.render(element);
    })
  }
複製代碼
  1. FiberRoot 數據結構一探究竟
  • 這裏我就不想貼一整大段代碼,只把關鍵的屬性列出來
  • FiberNode 裏面的數據結構先無論,咱們只須要知道它是記錄組件(class/fc/element)的狀態和信息
  • 最關鍵的是 current 屬性,便是 RootFiber,也就是說 FiberNode.current = RootFiber;
export default class ReactRoot {
  constructor(container) {
    // RootFiber tag === 3
    this.current = new FiberNode(3, null, null);
    // 初始化rootFiber的updateQueue
    initializeUpdateQueue(this.current);
    // RootFiber指向FiberRoot
    this.current.stateNode = this;
    // 應用掛載的根DOM節點
    this.containerInfo = container;
    // root下已經render完畢的fiber
    this.finishedWork = null;
  }
}
複製代碼
  1. unbatchedUpdates,這裏涉及到一個react的批量更新問題;
  • 在 react 中,若是我在一個 classComponent 組件內的點擊事件屢次調用 this.setState
  • 主動batchedUpdates, 會輸出1,2,3
  • 事件處理函數自帶batchedUpdates,至關於使用定時器的效果,會輸出0,0,0
  • 這是由於,react認爲,在很短的時間內觸發的更新,實際上是沒有必要的,會自動的加上事件合成 batchedUpdates
  • 固然,首次更新是非批量更新的,因此纔會調用 unbatchedUpdates 方法;
handleClick = () => {
    // 主動`unbatchedUpdates`
    // setTimeout(() => {
    // this.countNumber()
    // }, 0)

    // setTimeout中沒有`batchedUpdates`
    setTimeout(() => {
      batchedUpdates(() => this.countNumber())
    }, 0)

    // 事件處理函數自帶`batchedUpdates`,至關於上面的狀況
    // this.countNumber()
  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number)
  }
複製代碼
  1. 緊接着調用 FiberRoot.render
  • expirationTime 過時時間,表明着本次更新(update)的優先級;
  • 這裏得注意,React16.13.1 的 expirationTime 和 16.7 的過時時間是相反的,在16.7中,值越小,優先級越大;
  • 在建立好更新之後,就進入了react調度階段;
export default class ReactRoot {
  constructor(container) {
    // TODO...
  }   
  render(element) {
    // RootFiber 
    const current = this.current;
    // 申請當前的建立更新時間
    const currentTime = DOMRenderer.requestCurrentTimeForUpdate();
    // expirationTime 過時時間,能夠表明着本次更新任務的優先級;
    // 不一樣事件觸發的update會產生不一樣priority
    // 不一樣priority使fiber得到不一樣的expirationTime
    const expirationTime = DOMRenderer.computeExpirationForFiber(currentTime, current);
    // 建立更新
    const update = createUpdate(expirationTime);
    // fiber.tag爲HostRoot類型,payload爲對應要渲染的ReactComponents(APP 組件)
    update.payload = {element};
    enqueueUpdate(current, update);
    // 首次渲染會走這裏,再次更新就直接建立更新對象而後開始調度
    return DOMRenderer.scheduleUpdateOnFiber(current, expirationTime);
  }
}
複製代碼

首次渲染更新流程

老樣子,咱們仍是直接先上流程圖,根據流程再來看代碼和註釋;在閱讀react源碼的時候,是至關枯燥的,咱們須要一點耐心慢慢解刨; 安全

  1. scheduleUpdateOnFiber
  • 咱們只處理異步任務,因此不須要經過expirationTime檢查是不是異步
// 從當前fiber遞歸上去到root,再從root開始work更新
export function scheduleUpdateOnFiber(fiber, expirationTime) {
  // 注意是值越大,權限越大,和16.7相反了;
  // 向上冒泡更新,同時更新的過時時間(expirationTime)和子節點的過時時間 (childExpirationTime)
  // 這樣作的緣由是讓整個fiber樹上更新的最高優先級冒泡到root節點,進行更新 
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  // root == FiberRoot
  if (!root) {
    return;
  }
  // 開始安排調度安排調度
  ensureRootIsScheduled(root);
}
複製代碼
  1. ensureRootIsScheduled 開始安排調度
  • 這個階段相對來講是很是複雜的,可是總的來講它作了如下幾件事:
  • 將root加入schedule,root上每次只能存在一個scheduled的任務
  • 每次建立update後都會調用這個函數,須要考慮以下狀況:
  • 1.root上有過時任務,須要以ImmediatePriority(同步不中斷)馬上調度該任務
  • 2.root上已有schedule但還未到時間執行的任務,比較新舊任務expirationTime和優先級處理
  • 3.root上尚未已有schedule的任務,則開始該任務的render階段
function ensureRootIsScheduled(root) {
  // 這個變量記錄過時未執行的fiber的expirationTime
  const lastExpiredTime = root.lastExpiredTime;
  if (lastExpiredTime !== NoWork) {
   // ....TODO 
  }
  // 尋找root(FiberRoot)本次更新的過時時間
  const expirationTime = getNextRootExpirationTimeToWorkOn(root);
  const existingCallbackNode = root.callbackNode;
  // 本次更新的過時時間實際上是沒有任務 
  if (expirationTime === NoWork) {
    // 又存在當前正在進行的異步任務,同步執行掉
    if (existingCallbackNode) {
      root.callbackNode = null;
      root.callbackExpirationTime = NoWork;
      root.callbackPriority = Scheduler.NoPriority;
    }
    return;
  }

  // 從當前時間和expirationTime推斷任務優先級
  const currentTime = requestCurrentTimeForUpdate();
  const priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime);

  if (existingCallbackNode) {
    // 該root上已存在schedule的root
    const existingCallbackNodePriority = root.callbackPriority;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (existingCallbackExpirationTime === expirationTime && existingCallbackNodePriority >= priorityLevel) {
      // 該root已經存在的任務expirationTime和新udpate產生的expirationTime一致
      // 這表明他們多是同一個事件觸發產生的update
      // 且已經存在的任務優先級更高,則能夠取消此次update的render
      return;
    }
    // 不然表明新udpate產生的優先級更高,取消以前的schedule,從新開始一次新的
    Scheduler.cancelCallback(existingCallbackNode);
  }

  root.callbackExpirationTime = expirationTime;
  root.callbackPriority = priorityLevel;
  // 保存Scheduler保存的當前正在進行的異步任務
  let callbackNode;
  // 過時任何和同步任務同樣,不中斷,一口氣更新完;
  if (expirationTime === Sync) {
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    // 正常的異步任務和Concurrent首次渲染走走這裏 
    callbackNode = Scheduler.scheduleCallback(
      priorityLevel, 
      performConcurrentWorkOnRoot.bind(null, root),
      // 根據expirationTime,爲任務計算一個timeout
      // timeout會影響任務執行優先級
      {timeout: expirationTimeToMs(expirationTime) - Scheduler.now()}
    )
  }
  root.callbackNode = callbackNode;
}
複製代碼
  1. performSyncWorkOnRoot
  • 這是不經過scheduler的同步任務render階段的入口
  • 注意render階段其實就是Reconcile協調階段, diff算法就是在這個階段作的;
function performSyncWorkOnRoot(root) {
  const lastExpiredTime = root.lastExpiredTime;
  const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
  //先暫時忽略這個函數 
  flushPassiveEffects();
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
    // 建立WIP樹進行建立更新,若是WIP樹還存在,說明須要打斷這個任務
    prepareFreshStack(root, expirationTime);
  }
  //根據WIP樹進行更新 
  if (workInProgress) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext;
    do {
      // 進入同步的workLoop渲染大循環
      workLoopSync();
      break;
    } while (true)
    // render階段結束,進入commit階段,commit階段不可中斷
    commitRoot(root);
    // 從新安排調度, 以避免又執行不到過時了的任務;
    ensureRootIsScheduled(root);
  }
  return null;
}
複製代碼
  1. workLoopSync
  • 同步模式,不須要考慮任務是否須要中斷, 這也是爲何渲染階段能夠同步的緣由;
function workLoopSync() {
  while (workInProgress) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
複製代碼
  1. performUnitOfWork
  • 開始執行每一個單元的渲染工做,執行到WIP樹爲空,也就是說沒有更新了;
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  // beginWork會返回fiber.child,不存在next意味着深度優先遍歷已經遍歷到某個子樹的最深層葉子節點
  // beginWork 爲render階段的主要工做之一,主要作了以下事:
  // 根據update更新 state
  // 根據update更新 props
  // 根據update更新 effectTag
  let next = beginWork(current, unitOfWork, renderExpirationTime);
  // beginWork完成 fiber的diff,能夠更新momoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (!next) {
    // completeUnitOfWork 主要作了以下事:
    // 1.爲 beginWork階段生成的fiber生成對應DOM,併產生DOM樹
    // let next = completeWork(current, workInProgress);
    // 2. 將child fiber的expirationTime冒泡到父級
    // 這樣在父級就能直到子孫中優先級最高到expirationTime
    // resetChildExpirationTime(workInProgress);
    // 3. 組裝聖誕樹鏈條 effect list
    next = completeUnitOfWork(unitOfWork);
  }
  return next;
}
複製代碼
  • 對着代碼咱們再來看個圖,你就明白了;work階段結束了,也就表明着渲染階段已結束
  1. commitRoot 提交階段
  • 提交階段相對簡單,由於是同步執行的,不可中斷
function commitRoot(root) {
  const renderPriorityLevel = Scheduler.getCurrentPriorityLevel();
  // 包裹一層commitRoot,commit使用Scheduler調度
  Scheduler.runWithPriority(Scheduler.ImmediatePriority, commitRootImp.bind(null, root, renderPriorityLevel));
}

// commit階段的入口,包括以下子階段:
// before mutation階段:遍歷effect list,執行 DOM操做前觸發的鉤子
// mutation階段:遍歷effect list,執行effect
function commitRootImp(root) {
  do {
    // syncCallback會保存在一個內部數組中,在 flushPassiveEffects 中 同步執行完
    // 因爲syncCallback的callback是 performSyncWorkOnRoot,可能產生新的 passive effect
    // 因此須要遍歷直到rootWithPendingPassiveEffects爲空
    flushPassiveEffects();
  } while (ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects !== null)

  if (!finishedWork) {
    return null;
  }

  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;

  // 重置Scheduler相關
  root.callbackNode = null;
  root.callbackExpirationTime = NoWork;
  root.callbackPriority = Scheduler.NoPriority;

  // 已經在commit階段,finishedWork對應的expirationTime對應的任務的處理已經接近尾聲
  // 讓咱們找找下一個須要處理的任務
  // 在 completeUnitOfWork中有childExpirationTime的冒泡邏輯
  // fiber樹中高優先級的expirationTime會冒泡到頂上
  // 因此 childExpirationTime 表明整棵fiber樹中下一個最高優先級的任務對應的expirationTime
  const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(finishedWork);
  // 更新root的firstPendingTime,這表明下一個要進行的任務的expirationTime
  markRootFinishedAtTime(root, expirationTime, remainingExpirationTimeBeforeCommit);

  if (root === workInProgressRoot) {
    // 重置 workInProgress
    workInProgressRoot = null;
    workInProgress = null;
    renderExpirationTime = NoWork;
  }

  let firstEffect;
  if (root.effectTag) {
    // 因爲根節點的effect list不含有自身的effect,因此當根節點自己存在effect時須要將其append 入 effect list
    if (finishedWork.lastEffect) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // 根節點自己沒有effect
    firstEffect = finishedWork.firstEffect;
  }
  let nextEffect;
  if (firstEffect) {
    // before mutation階段
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    nextEffect = firstEffect;    
    do {
      try {
        nextEffect = commitBeforeMutationEffects(nextEffect);
      } catch(e) {
        console.warn('commit before error', e);
        nextEffect = nextEffect.nextEffect;
      }
    } while(nextEffect)

    // mutation階段
    nextEffect = firstEffect;
    do {
      try {
        nextEffect = commitMutationEffects(root, nextEffect);
      } catch(e) {
        console.warn('commit mutaion error', e);
        nextEffect = nextEffect.nextEffect;
      }
    } while(nextEffect)

    // workInProgress tree 如今完成反作用的渲染變成current tree
    // 之因此在 mutation階段後設置是爲了componentWillUnmount觸發時 current 仍然指向以前那棵樹
    root.current = finishedWork;
    
    if (ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects) {
      // 本次commit含有passiveEffect
      ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects = false;
      ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects = root;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsExpirationTime = expirationTime;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else {
      // effectList已處理完,GC
      nextEffect = firstEffect;
      while (nextEffect) {
        const nextNextEffect = nextEffect.next;
        nextEffect.next = null;
        nextEffect = nextNextEffect;
      }
    }
    executionContext = prevExecutionContext;
  } else {
    // 無effect
    root.current = finishedWork;
  }
}
複製代碼

非首次渲染更新流程

內容未完待續 markdown

總結

待更新

相關文章
相關標籤/搜索