站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!

サムネイル|Reactの作り方.png

介紹

大家好,我是渡邊仁(@Sicut_study)。

我一直在向初學者教授 React。

我教過的學生使用 React 開發了令人印象深刻的服務,但即使他們具備技能,他們也經常浪費時間處理錯誤,因為他們缺乏對虛擬 DOM 和重新渲染的紮實理解。

(無需深入了解即可使用,這也是它的吸引力所在。)

在開發 React 時,對「虛擬 DOM」和「重新渲染」有紮實的理解對於處理 bug提高效能非常重要。

這次,我建立了一個教程,透過僅使用 JavaScript 從頭開始重新實作 React ,讓您在實作層級了解虛擬 DOM。

在建立本教程時,我自己能夠理解虛擬 DOM。

我現在能夠編寫更像 React 的程式碼,我真的很高興我做到了。

我希望剛開始使用 React 的人能夠理解本教程,因此我確保提供詳細的解釋。

“重新發明輪子”

如果您是工程師,您可能以前聽說過這個術語。

重新發明輪子是一個習慣用語,指的是重新發明一種既定的技術或解決方案,無論是有意或無意地忽略它。

人們常說,在 IT 產業中最好避免「重新發明輪子」。

但是,我認為重新實現它將是一項非常划算的努力,因為它將幫助您更深入地了解 React 並讓您編寫更好的程式碼

透過本教程,您將更深入地了解 React 本身的工作原理。

您一定能夠編寫更多類似 React 的程式碼!

也提供影片教學

本課程還附帶一套影片,提供更詳細的講解。

如果您對教科書中的任何細節不理解,請使用影片。

目標受眾

  • 有 React 使用經驗的人

  • 不了解虛擬 DOM 的人

  • 不懂重新渲染的人

  • 想深入了解 React 的人

  • 想要掌握基本技能的人

任何具有 React 經驗的人都可以在大約 2 小時內完成本教學。

本次實作課程是對以下文章的現代化解釋:

什麼是 DOM?

在討論虛擬 DOM 之前,讓我們先來了解一些基礎知識。

DOM 是一種模型,它以程式(JavaScript)可以理解和操作的形式來表示用 HTML 編寫的網頁。

影像.png

引用

DOM 具有由節點(元素)組成的樹狀結構,稱為 DOM 樹。

節點也有幾種類型。

影像.png

在 DOM 樹中,節點之間的關係呼叫如下,需要記住的是,「父節點」、「子節點」和「兄弟節點」也將出現在此實作中。

影像.png

讓我們操作 DOM

DOM 是一個可以用 JavaScript 操作的模型,因此您可以透過在瀏覽器的控制台中執行 JavaScript 程式碼來操作它。

前往https://google.com

右鍵單擊並按一下“檢查”或“檢查元素”以開啟開發人員工具。

影像.png

開啟開發者工具後,按一下頂部的「控制台」標籤。

影像.png

現在讓我們實際編寫 JavaScript 來了解目前的 DOM 結構。

// document オブジェクトを確認
console.log(document);

// HTML要素(ルート要素)を取得
console.log(document.documentElement);

// body要素を取得
console.log(document.body);

// 特定の要素を取得する方法(inputタグ要素を取得)
console.log(document.querySelector('input')); // 検索フォームのinput取得

影像.png

您可以看到它具有樹狀結構。

現在我們已經獲得了 DOM,讓我們使用 DOM 操作為頁面新增一個新的h1 標籤

// 新しいh1要素を作成(まだページには表示されない)
const newHeading = document.createElement('h1');

// 要素の内容を設定
newHeading.textContent = 'Hello World!';

// ページの最上部に追加(ここで初めて画面に表示される)
document.body.insertBefore(newHeading, document.body.firstChild);

影像.png

我們在螢幕上新增了「Hello World!」!

我將解釋我執行的程式碼。

const newHeading = document.createElement('h1');

此行在記憶體中建立一個新的 h1 元素(標題)。

此時,它尚未加入到 DOM 樹,因此在螢幕上不可見。

newHeading.textContent = 'Hello World!';

在建立的 h1 元素中設定文字「Hello World!」。

document.body.insertBefore(newHeading, document.body.firstChild);

此行實際上將建立的元素插入到 DOM 中。

insertBefore 將其第一個參數(newHeading)插入到其第二個參數(document.body.firstChild,即 body 的第一個子元素)之前,將 h1 元素定位為頁面 body 的第一個子元素,並使其在螢幕上可見。

DOM 的問題

在現代複雜應用程式中操作 DOM 面臨多項挑戰。

影像.png

  • 程式碼往往很複雜

  • 難以管理狀態

  • 容易出現效能問題

React 是一個解決這些問題的革命性函式庫。

最大的創新在於一種稱為虛擬 DOM 的技術。

React 首先建立一個虛擬 DOM(實際瀏覽器 DOM 的輕量級記憶體副本)。

然後,只有與先前的虛擬 DOM 有差異的虛擬節點才會更新為實際 DOM 上的節點。

與直接操作 DOM 相比,這透過顯著降低渲染成本(繪製需要各種流程)來提高效能。

從這裡開始,我們將透過在 JavaScript 中實際重新實作 React 來更深入地理解虛擬 DOM 和渲染。

渲染流程

在用 JavaScript 重新實作 React 之前,如果您了解 React 渲染的整體情況,那麼理解實作會更容易。

如果您在實施某些事情時感到困惑,請參考本章。

影像.png

React 渲染過程遵循以下順序:

  1. 元件定義(JSX)

使用 JSX 定義一個元件並開始渲染它。

// JSXを使ったコンポーネント定義
function MyComponent(props) {
  return <div className="container">Hello, {props.name}!</div>;
}

// レンダリングの開始点
ReactDOM.render(<MyComponent name="World" />, document.getElementById('root'));
  1. 將 JSX 轉換為 React 元素

JSX 實際上會轉換為這樣的函數呼叫:

// Viteが変換する
function MyComponent(props) {
  return React.createElement(
    'div',
    { className: 'container' },
    'Hello, ',
    props.name,
    '!'
  );
}

// レンダリング開始
ReactDOM.render(
  React.createElement(MyComponent, { name: 'World' }),
  document.getElementById('root')
);

我們寫的程式碼如下所示:

function MyComponent(props) {
  return <div className="container">Hello, {props.name}!</div>;
}

Vite 內部會進行轉換,轉換成這種形式再執行。

function MyComponent(props) {
  return React.createElement(
    'div',
    { className: 'container' },
    'Hello, ',
    props.name,
    '!'
  );
}
  1. 建立 React 元素

呼叫createElement將建立一個如下所示的物件(React 元素)。

{
  type: 'div', // または関数コンポーネント
  props: {
    className: 'container',
    children: ['Hello, ', 'World', '!']
  },
  key: null,
  ref: null
}

4.渲染階段

將3中建立的React元素分割成更小的單元(節點單元),並為每個單元建立一個保存元件資訊的物件。

這些被稱為Fiber ,由 Fiber 建立的樹結構稱為 Fiber 樹。目前,我們可以將 Fiber 樹想像成類似虛擬 DOM 樹的東西。

Fibers 包含有關節點的訊息,例如 ReactElement 類型(div、h1 等)和 parent(父元素)。

const counterFiber = {
  // 基本情報
  type: Counter, // 関数自体への参照
  props: {
    initial: 0,
    children: []
  },

  // 関数コンポーネントはDOMノードを持たない
  dom: null,

  // ツリー構造
  parent: appFiber,    // Appコンポーネントなど
  child: pFiber,       // p要素のファイバー
  sibling: null,
  alternate: null,

  // 副作用タグ
  effectTag: 'UPDATE'
};

對於每條光纖,將其與前一條光纖樹進行比較。

如果有任何變化,請記錄光纖中的變化。

const counterFiber = {
  type: Counter,
  props: {
    initial: 0,
    children: []
  },
  dom: null,
  parent: appFiber,
  child: pFiber,
  sibling: null,
  alternate: null,
  effectTag: 'UPDATE' // 更新されたことを表す
};

透過對纖維樹進行精細纖維處理,

如果使用者在螢幕上執行某些操作(例如點擊選單),則任務將中斷,並且使用者的操作將優先處理。

這可以防止螢幕在渲染過程中無法操作,從而改善使用者體驗。

(稍後我們將詳細解釋纖維。)

  1. 提交階段

最後,它對新增、修改或刪除的節點執行實際的 DOM 操作。

如果你查看4中建立的fiber,你可以看到元素是否會被更新/刪除/更新。

它檢查每個纖程,如果有更新,它會對實際的 DOM 進行操作。

這意味著它實際上會反映在螢幕上。

這是該過程的粗略概述,此實施將遵循大致相同的流程。

如果你沒有完全理解這個解釋也沒關係。我們先大致了解整個事情,然後再實際實現 React,加深理解。

  1. 環境構築

首先,讓我們建立一個可以使用 Vite 來執行 JavaScript 的環境。

通常,使用 React 時,需要將其作為庫安裝,但這次我們將只使用 JavaScript 建立 React,因此我們需要做的就是準備一個可以執行 JavaScript 的環境。

$ node -v
v22.4.0

$ npm create vite@latest
> npx
> create-vite

✔ Project name: … make-react
✔ Select a framework: › Vanilla
✔ Select a variant: › JavaScript

$ cd make-react
$ npm i
$ npm run dev

開啟http://localhost:5173 ,如果出現以下畫面,就完成了。

影像.png

  1. 用 JavaScript 取代 React

這裡我們將簡單地用 JavaScript 取代 React 程式碼。

當然,此程式碼依賴於不允許差異更新的範例問題,但讓我們繼續進行。

開啟剛剛在 VSCode 中建立的make-react目錄。

首先,刪除main.js中的所有程式碼。

讓我們編寫一些常規的 React 程式碼(當然,由於未安裝 React,它將無法運作)。

function MyComponent() {
  return <h1 title="foo">Hello</h1>;
}

const container = document.getElementById("root");
ReactDOM.render(<MyComponent />, container);

使用ViteBabel等編譯工具,JSX 會被轉換成建立名為createElement的物件的程式碼。讓我們手動替換這段程式碼。

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
);
const container = document.getElementById("root");
ReactDOM.render(element, container);

createElement是一個接受三個參數的函數:

第一個參數:類型(指定要建立的元素的類型)

第二個參數:Props(傳遞給元素的屬性(attribute)物件)

第三個參數:children(定義要放置在 HTML 中的內容(文字、其他 React 元素或子元素陣列))

當你createElement時,它將被轉換為以下物件並返回。

{
  type: 'h1',
  props: {
    title: 'foo',
    children: ['Hello']
  }
}

如果用 JavaScript 取代 React.createElement,可以這樣寫,達到同樣的意思。

const element = {
  type: 'h1',
  props: {
    title: 'foo',
    children: ['Hello']
  }
};

接下來我們要用 JavaScript 取代ReactDom.render

在根元素中新增 h1 屬性,就像之前在控制台中所做的那樣。

const element = {
  type: 'h1',
  props: {
    title: 'foo',
    children: ['Hello']
  }
};

const container = document.getElementById("root");
const node = document.createElement(element.type);
node.title = element.props.title;

const text = document.createTextNode(element.props.children);
node.appendChild(text);

container.appendChild(node);

appendChild是一種基本的 DOM 方法,它將子元素新增至父元素。

此行將我們建立的文字節點(「Hello」)放置在 h1 元素內。

在 HTML 中表達如下:

<h1 title="foo">Hello</h1>

在 React 中,透過新增和修改root元素的 DOM 來改變(渲染)畫面。

所以我們的螢幕也需要有root元素。

請如下修改index.html。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <!-- appからrootにidを変更 -->
    <div id="root"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

我將 id 從 app 更改為 root。

<div id="root"></div>

現在它已開始工作,讓我們啟動伺服器並嘗試存取它。

$ npm run dev

開啟http://localhost:5173

影像.png

  1. 實作 createElement

就目前而言,除非我們自己將其轉換為以下物件,否則它將無法工作,因此我們將實現createElement來自動化此操作。

{
  type: 'h1',
  props: {
    title: 'foo',
    children: ['Hello']
  }
}

現在讓我們將程式碼改回使用 createElement 時的方式。

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
);

然後將 createElement 準備為一個函數並呼叫你自己的函數而不是 React.createElement。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

const element = createElement("h1", { title: "foo" }, "Hello");

const container = document.getElementById("root");
ReactDOM.render(element, container);

在 createElement 中,我們建立一個具有所需形狀的物件:

{
  type: 'h1',
  props: {
    title: 'foo',
    children: ['Hello']
  }
}

道具部分比較難,所以我會多解釋一下。

props: {
  ...props,
  children: children.map((child) =>
    typeof child === "object" ? child : createTextElement(child)
  )
}

props 是第二個參數{ title: "foo" } ,...children 是第三個參數"Hello"

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

...props道具

{
  type: 'h1',
  props: {
    title: 'foo',
    children: ...
  }
}

例如,如果 props 是{ title: "foo", name: "hoge" } ,它將看起來像這樣:

{
  type: 'h1',
  props: {
    title: 'foo',
    name: 'hoge',
    children: ...
  }
}

順便說一下,JSX 應該看起來像這樣:

function MyComponent() {
  return <h1 title="foo" name="hoge">Hello</h1>;
}

生孩子是更複雜的過程。

children: children.map((child) =>
  typeof child === "object" ? child : createTextElement(child)
)

children 接受可變數量的參數。

function createElement(type, props, ...children) {

例如,如果有這樣的各種子元素,children 將包含多個元素。

// JSX
function MyList() {
  return (
    <ul id="my-list">
      <li>專案1</li>
      <li>專案2</li>
      テキストノード
      {123}
    </ul>
  );
}

// コンパイルしたもの
createElement(
  "ul",
  { id: "my-list" },
  createElement("li", null, "專案1"),
  createElement("li", null, "專案2"),
  "テキストノード",
  123
);

這樣,檢查每個子元素以確定它是否是一個物件。

如果它是一個物件,則按原樣返回,否則將其轉換為文字元素並返回。

children: children.map((child) =>
  typeof child === "object" ? child : createTextElement(child)
)

這裡我們需要一個名為createTextElement的函數,所以讓我們來準備它。

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  };
}

由於字串和數字在 JavaScript 中不是物件,因此不能將它們直接視為虛擬 DOM 元素,因此createTextElement將它們轉換為類似形狀的物件。

React 內部使用名為 ReactText 的類型來表示文字節點,但由於我們這次重新實作 React,因此我們設定了自己的類型,名為TEXT_ELEMENT

如果您能走到這一步,您應該能夠重新實作 createElement,所以讓我們檢查一下。

目前Vite無法啟動。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  };
}

const element = createElement("h1", { title: "foo" }, "Hello");
console.log(element);

手動建立伺服器啟動

const container = document.getElementById("root");
const node = document.createElement("h1");
const text = document.createTextNode("Hello World!");
node.appendChild(text);
container.appendChild(node);

現在它已開始工作,讓我們啟動伺服器並嘗試存取它。

$ npm run dev

開啟http://localhost:5173

影像.png

  1. 重新實作渲染

接下來,我們將準備自己的渲染,但我們將準備自己的物件並呼叫它,就像我們通常使用ReactDom.render一樣。

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element]
    }
  };
}

讓 nextUnitOfWork = null;

讓 wipRoot = null;

讓 currentRoot = null;

函數工作循環(截止日期){

讓 shouldYield = false;

while (nextUnitOfWork && !shouldYield) {

nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;

}

請求空閒回呼(工作循環);

}

請求空閒回呼(工作循環);

函數 performUnitOfWork(fiber){

如果(!fiber.dom){

fiber.dom = createDom(fiber);

}

const 元素 = fiber.props.children;

協調兒童(纖維,元素);

如果(fiber.child){

return fiber.child;

}

讓 nextFiber = fiber;

while (nextFiber) {

if (nextFiber.sibling) {
  return nextFiber.sibling;
}
nextFiber = nextFiber.parent;

}

}

函數 reconcileChildren(wipFiber,元素){

讓索引=0;

讓 oldFiber = wipFiber.alternate && wipFiber.alternate.child;

讓 prevSibling = null;

while (index < elements.length || oldFiber) {

const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && oldFiber.type === element.type;
if (sameType) {
  newFiber = {
    type: oldFiber.type,
    props: element.props,
    dom: oldFiber.dom,
    parent: wipFiber,
    alternate: oldFiber,
    effectTag: "UPDATE"
  };
}
if (element && !sameType) {
  newFiber = {
    type: element.type,
    props: element.props,
    dom: null,
    parent: wipFiber,
    alternate: null,
    effectTag: "PLACEMENT"
  };
}
if (oldFiber && !sameType) {
  oldFiber.effectTag = "DELETION";
  deletions.push(oldFiber);
}
if (oldFiber) {
  oldFiber = oldFiber.sibling;
}
if (index === 0) {
  wipFiber.child = newFiber;
} else if (element) {
  prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;

}

}

const MyReact = {

建立元素,

使成為

};

導出預設的 MyReact;

結論

您覺得怎麼樣?

這次我嘗試只使用 JavaScript 重新實作 React。

我認為透過實現我對虛擬 DOM 和重新渲染有了更深的理解。

我已經發布了一個詳細解釋的影片,如果您有興趣,請看一下!

我們正在招募JISOU成員!

程式輔導JISOU正在招募新成員。

為什麼不在日本第一產出社群中發展你的事業呢?

如果您有興趣,請隨時透過我們的網站申請諮詢!

▼▼▼

我發布了很多圖解動手影片!


原文出處:https://qiita.com/Sicut_study/items/710ea707d4426011710f


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!