React筆記-首次渲染

来源:https://www.cnblogs.com/raion/archive/2019/04/14/10704050.html
-Advertisement-
Play Games

渲染機制 渲染機制主要分為兩部分: 首次渲染和更新渲染。 首次渲染 首先通過一個小例子,來講解首次渲染過程。 程式運行到 時,其中的 babel React.createElement(ClickCounter, null) element`如下: 接下來執行 函數,生成 節點。首先瞭解下 的部分數 ...


渲染機制

渲染機制主要分為兩部分: 首次渲染和更新渲染。

首次渲染

首先通過一個小例子,來講解首次渲染過程。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';

class ClickCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  handleClick = () => {
    this.setState((state) => {
      return {count: state.count + 1};
    });
  }
  render() {
    return [
      <button key="1" onClick={this.handleClick}>Update counter</button>,
      <span key="2">{this.state.count}</span>,
    ]
  }
}
ReactDOM.hydrate(<ClickCounter />, document.getElementById('root'));

程式運行到ReactDOM.hydrate時,其中的<ClickCounter />已被babel轉換為React.createElement(ClickCounter, null),生成的element如下:

{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ClickCounter
}

接下來執行hydrate函數,生成root節點。首先瞭解下fiber的部分數據結構。

  • alternate(對應的workInProgressfiber
  • stateNode(關聯的fiber,組件實例或者DOM節點)
  • type(組件或HTML tag,如divspan等)
  • tag(類型,詳見workTags
  • effectTag(操作類型,詳見sideEffectTag
  • updateQueue(更新隊列)
  • memoizedState(state
  • memoizedProps(props
  • pendingProps(VDOM
  • return(父fiber
  • sibling(兄弟fiber
  • child(孩子fiber
  • firstEffect(第一個待處理的effect fiber
  • lastEffect(最後一個待處理的effect fiber

首次渲染會以同步渲染的方式進行渲染,首先創建一個update,將element裝載到其payload屬性中,然後合併到root.current.updateQueue,如果沒有updateQueue會創建一個。我們暫且將root.current看成HostRoot

接著根據HostRoot克隆一棵workInProgress更新樹。將HostRoot.alternate指向workInProgressworkInProgress.alternate指向HostRoot。然後進入workLoop進行更新樹操作部分。workLoop的任務也很簡單,就是將所有節點的更新掛載到更新樹上。下麵詳細看看reconciliation階段。

reconciliation階段

reconciliation的核心在於workLoopworkLoop會以workInProgress為起點,即克隆的HostRoot,不斷向下尋找。如果workInProgress.child不為空,會進行diff;如果為空會創建workInProgress.child`。

// 第一次迴圈nextUnitOfWork為workInProgress
function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until there's a higher priority event
    while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

因為只涉及首次渲染,所以這裡將performUnitOfWork簡單化。beginWork根據workInProgress.tag選擇不同的處理方式。先暫且看看如何處理HostRoot。進入updateHostRoot方法,先進行workInProgress.updateQueue的更新,計算新的state,將update.baseStateworkInProgress.memoizedState指向新的state。這裡新的state裝載的是element

接下來調用createFiberFromElement創建fiber,將workInProgress.child指向該fiberfiber.return指向workInProgress

function performUnitOfWork(workInProgress) {
  let next = beginWork(workInProgress); // 創建workInProgress.child並返回
  if (next === null) { // 沒有孩子,收集effect list,返回兄弟或者父fiber
      next = completeUnitOfWork(workInProgress);
  }
  return next;
}

function beginWork(workInProgress) {
  switch(workInProgress.tag) {
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case ClassComponent: 
      ...
  }
}

用一張圖體現更新樹創建完成後的樣子:

workInProgress沒有孩子時,即創建的孩子為空。說明已經到達底部,開始收集effect

function completeUnitOfWork(workInProgress) {
  while (true) {
    let returnFiber = workInProgress.return;
    let siblingFiber = workInProgress.sibling;
    nextUnitOfWork = completeWork(workInProgress);
   ...// 省略收集effect list過程
    if (siblingFiber !== null) {
      // If there is a sibling, return it
      // to perform work for this sibling
      return siblingFiber;
    } else if (returnFiber !== null) {
      // If there's no more work in this returnFiber,
      // continue the loop to complete the parent.
      workInProgress = returnFiber;
      continue;
    } else {
      // We've reached the root.
      return null;
    }
  }
}
function completeWork(workInProgress) {
  //根據workInProgress.tag創建、更新或刪除dom
  switch(workInProgress.tag) {
    case HostComponent: 
      ...
  }
  return null;
}

協調演算法過程結束後,workInProgress更新樹更新完畢,收集的effect list如下:

commit階段

effect list(鏈表)是reconciliation階段的結果,決定了哪些節點需要插入、更新和刪除,以及哪些組件需要調用生命周期函數。firstEffect記錄第一個更新操作,firstEffect.nextEffect(fiber)記錄下一個,然後繼續通過其nextEffect不斷往下尋找直至為null。下麵是commit階段的主要流程:

// finishedWork為更新樹
function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles();
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}

變數nextEffect每次執行完上面一個函數會被重置為finishedWork

  • commitBeforeMutationLifecycles

檢查effect list中每個fiber是否有Snapshot effect,如果有則執行getSnapshotBeforeUpdate

// 觸發getSnapshotBeforeUpdate
function commitBeforeMutationLifecycles() {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    if (effectTag & Snapshot) {
      const current = nextEffect.alternate;
      commitBeforeMutationLifeCycles(current, nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  • commitAllHostEffects

提交所有effect,實現dom的替換、更新和刪除。

function commitAllHostEffects() {
  while(nextEffect !== null) {
    var effectTag = nextEffect.effectTag;
    var primaryEffectTag = effectTag & (Placement | Update | Deletion);
    switch (primaryEffectTag) {
      case Placement: {
        commitPlacement(nextEffect);
        ...
      }
      case PlacementAndUpdate: {
        commitPlacement(nextEffect);
        var _current = nextEffect.alternate;
        commitWork(_current, nextEffect);
        ...
      }
      case Update: {
        var _current2 = nextEffect.alternate;
        commitWork(_current2, nextEffect); 
        ...
      }
      case Deletion: {// 觸發componentWillUnmout
        commitDeletion(nextEffect);
        ...
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  • commitAllLifeCycles

觸發componentDidMountcomponentDidUpdate

function commitAllLifeCycles(finishedRoot, committedExpirationTime) {
  while (nextEffect !== null) {
    var effectTag = nextEffect.effectTag;

    if (effectTag & (Update | Callback)) {
      var current$$1 = nextEffect.alternate;
      commitLifeCycles(finishedRoot, current$$1, nextEffect, committedExpirationTime);
    }
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }
    if (effectTag & Passive) {
      rootWithPendingPassiveEffects = finishedRoot;
    }

    nextEffect = nextEffect.nextEffect;
  }
}

總結

這裡並未逐一細說,不想讀起來直犯困,更多講述了大概流程。如果覺得有疑惑的地方,也知道該在什麼地方找到對應的源碼,解答疑惑。

更好的閱讀體驗在我的github,歡迎

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 現如今前後端分離開發越來越普遍,前端人員寫好頁面後可以自己模擬一些數據進行代碼測試,這樣就不必等後端介面,提高了我們開發效率。今天就來分析下前端常用的mock數據的方式是如何實現的。 主體 項目是基於vue cli的,首先必須搭建好vue項目環境。如果有同學不知道如何搭建vue項目,可以參考我 ...
  • 項目中經常用到的日期格式化以及金額千分位方法,封裝一下直接拿去用 // 時間格式化export function formatDate(date, fmt) { if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYea ...
  • javascript中toFixed使用的是銀行家舍入規則。 銀行家舍入:所謂銀行家舍入法,其實質是一種四舍六入五取偶(又稱四舍六入五留雙)法。 簡單來說就是:四舍六入五考慮,五後非零就進一,五後為零看奇偶,五前為偶應捨去,五前為奇要進一。但是不論引入toFixed解決浮點數計算精度缺失的問題也好, ...
  • 瞭解JavaScript的同學可能知道,JavaScript語言由於設計原因,導致語言本身存在很多先天性的不足,當然這並非設計者有意的,js語言最初是被設計來作為網頁交互的腳本語言,依照現有的js語法來看,其最初的需求已經完全能夠滿足。互聯網的發展極大的提高了對web端的要求,不僅僅要求簡單的頁面交 ...
  • 1.什麼是XSS http://netsecurity.51cto.com/art/201610/518517.htm https://www.bilibili.com/video/av32599703 ...
  • 谷歌向上滾動滾輪是正值,向下是負值,而火狐正好相反 完整測試代碼,分別用谷歌和火狐瀏覽器測試 ...
  • 閑聊 步入前端切圖仔行列的我曾多次糾結過「到底使用哪種編輯器寫前端好用?」這樣的問題,前前後後嘗試過 Dreamweaver 、HBuilder 、Sublime Text 、Atom 和現在主要使用的 VSCode 。現在回過頭來看,我發現這個問題有了答案,那就是「愛用啥用啥」。(笑…… 今天的主 ...
  • request、out對象使用 一、實驗目的 1、掌握代碼片段中的註釋的應用; 2、掌握JSP腳本標示 Java代碼片段的應用。 二、實驗內容 1、設計教師與學生不同登陸界面,如下圖; 2、驗證碼隨機生成; 3、提交後分別轉向教師頁面和學生頁面進行判斷用戶名和密碼正確性; 4、如果正確,3秒後,轉向 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...