一、前言
本文基於 https://pomb.us/build-your-own-react/ 實現簡單版 React。
本文學習思路來自 卡頌-b站-React原始碼,你在第幾層。
模擬的版本為 React 16.8。
將實現以下功能:
- createElement(虛擬 DOM);
- render;
- 可中斷渲染;
- Fibers;
- Render and Commit Phases ;
- 協調(Diff 演算法);
- 函式元件;
- hooks;
下面上正餐,請繼續閱讀。
二、準備
1. React Demo
先來看看一個簡單的 React Demo,程式碼如下:
const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);
本例完整原始碼見:reactDemo
在瀏覽器中開啟 reactDemo.html,展示如下:
我們需要實現自己的 React,那麼就需要知道上面的程式碼到底做了什麼。
1.1 element
const element = <div>123</div>
實際上是 JSX 語法。
React 官網 對 JSX 的解釋如下:
JSX 是一個 JavaScript 語法擴充套件。它類似於模板語言,但它具有 JavaScript 的全部能力。JSX 最終會被 babel 編譯為 React.createElement() 函式呼叫。
通過 babel 線上編譯 const element = <div>123</div>
。
可知 const element = <div>123</div>
經過編譯後的實際程式碼如下:
const element = React.createElement("div", {
title: "foo"
}, "hello");
再來看看上文的 React.createElement 實際生成了一個怎麼樣的物件。
在 demo 中列印試試:
const element = <div title="foo">hello</div>
console.log(element)
const container = document.getElementById('container')
ReactDOM.render(element, container);
可以看到輸出的 element 如下:
簡化一下 element:
const element = {
type: 'div',
props: {
title: 'foo',
children: 'hello'
}
}
簡單總結一下,React.createElement
實際上是生成了一個 element 物件,該物件擁有以下屬性:
- type: 標籤名
- props
- title: 標籤屬性
- children: 子節點
1.2 render
ReactDOM.render()
將 element 新增到 id 為 container 的 DOM 節點中,下面我們將簡單手寫一個方法代替 ReactDOM.render()
。
- 建立標籤名為 element.type 的節點;
const node = document.createElement(element.type)
- 設定 node 節點的 title 為 element.props.title;
node["title"] = element.props.title
- 建立一個空的文字節點 text;
const text = document.createTextNode("")
- 設定文字節點的 nodeValue 為 element.props.children;
text["nodeValue"] = element.props.children
- 將文字節點 text 新增進 node 節點;
node.appendChild(text)
- 將 node 節點新增進 container 節點
container.appendChild(node)
本例完整原始碼見:reactDemo2
執行原始碼,結果如下,和引入 React 的結果一致:
三、開始
上文通過模擬 React,簡單代替了 React.createElement、ReactDOM.render 方法,接下來將真正開始實現 React 的各個功能。
1. createElement(虛擬 DOM)
上面有了解到 createElement 的作用是建立一個 element 物件,結構如下:
// 虛擬 DOM 結構
const element = {
type: 'div', // 標籤名
props: { // 節點屬性,包含 children
title: 'foo', // title 屬性
children: 'hello' // 子節點,注:實際上這裡應該是陣列結構,幫助我們儲存更多子節點
}
}
根據 element 的結構,設計了 createElement 函式,程式碼如下:
/**
* 建立虛擬 DOM 結構
* @param {type} 標籤名
* @param {props} 屬性物件
* @param {children} 子節點
* @return {element} 虛擬 DOM
*/
function createElement (type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
這裡有考慮到,當 children 是非物件時,應該建立一個 textElement 元素, 程式碼如下:
/**
* 建立文字節點
* @param {text} 文字值
* @return {element} 虛擬 DOM
*/
function createTextElement (text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}
接下來試一下,程式碼如下:
const myReact = {
createElement
}
const element = myReact.createElement(
"div",
{ id: "foo" },
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)
console.log(element)
本例完整原始碼見:reactDemo3
得到的 element 物件如下:
const element = {
"type": "div",
"props": {
"id": "foo",
"children": [
{
"type": "a",
"props": {
"children": [
{
"type": "TEXT_ELEMENT",
"props": {
"nodeValue": "bar",
"children": [ ]
}
}
]
}
},
{
"type": "b",
"props": {
"children": [ ]
}
}
]
}
}
JSX
實際上我們在使用 react 開發的過程中,並不會這樣建立元件:
const element = myReact.createElement(
"div",
{ id: "foo" },
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)
而是通過 JSX 語法,程式碼如下:
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
在 myReact 中,可以通過新增註釋的形式,告訴 babel 轉譯我們指定的函式,來使用 JSX 語法,程式碼如下:
/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
本例完整原始碼見:reactDemo4
2. render
render 函式幫助我們將 element 新增至真實節點中。
將分為以下步驟實現:
- 建立 element.type 型別的 dom 節點,並新增至容器中;
/**
* 將虛擬 DOM 新增至真實 DOM
* @param {element} 虛擬 DOM
* @param {container} 真實 DOM
*/
function render (element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
- 將 element.children 都新增至 dom 節點中;
element.props.children.forEach(child =>
render(child, dom)
)
- 對文字節點進行特殊處理;
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(element.type)
- 將 element 的 props 屬性新增至 dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
以上我們實現了將 JSX 渲染到真實 DOM 的功能,接下來試一下,程式碼如下:
const myReact = {
createElement,
render
}
/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)
myReact.render(element, document.getElementById('container'))
本例完整原始碼見:reactDemo5
結果如圖,成功輸出:
3. 可中斷渲染(requestIdleCallback)
再來看看上面寫的 render 方法中關於子節點的處理,程式碼如下:
/**
* 將虛擬 DOM 新增至真實 DOM
* @param {element} 虛擬 DOM
* @param {container} 真實 DOM
*/
function render (element, container) {
// 省略
// 遍歷所有子節點,並進行渲染
element.props.children.forEach(child =>
render(child, dom)
)
// 省略
}
這個遞迴呼叫是有問題的,一旦開始渲染,就會將所有節點及其子節點全部渲染完成這個程式才會結束。
當 dom tree 很大的情況下,在渲染過程中,頁面上是卡住的狀態,無法進行使用者輸入等互動操作。
可分為以下步驟解決上述問題:
- 允許中斷渲染工作,如果有優先順序更高的工作插入,則暫時中斷瀏覽器渲染,待完成該工作後,恢復瀏覽器渲染;
- 將渲染工作進行分解,分解成一個個小單元;
使用 requestIdleCallback 來解決允許中斷渲染工作的問題。
window.requestIdleCallback 將在瀏覽器的空閒時段內呼叫的函式排隊。這使開發者能夠在主事件迴圈上執行後臺和低優先順序工作,而不會影響延遲關鍵事件,如動畫和輸入響應。
window.requestIdleCallback 詳細介紹可檢視文件:文件
程式碼如下:
// 下一個工作單元
let nextUnitOfWork = null
/**
* workLoop 工作迴圈函式
* @param {deadline} 截止時間
*/
function workLoop(deadline) {
// 是否應該停止工作迴圈函式
let shouldYield = false
// 如果存在下一個工作單元,且沒有優先順序更高的其他工作時,迴圈執行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 如果截止時間快到了,停止工作迴圈函式
shouldYield = deadline.timeRemaining() < 1
}
// 通知瀏覽器,空閒時間應該執行 workLoop
requestIdleCallback(workLoop)
}
// 通知瀏覽器,空閒時間應該執行 workLoop
requestIdleCallback(workLoop)
// 執行單元事件,並返回下一個單元事件
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
performUnitOfWork 是用來執行單元事件,並返回下一個單元事件的,具體實現將在下文介紹。
4. Fiber
上文介紹了通過 requestIdleCallback 讓瀏覽器在空閒時間渲染工作單元,避免渲染過久導致頁面卡頓的問題。
注:實際上 requestIdleCallback 功能並不穩定,不建議用於生產環境,本例僅用於模擬 React 的思路,React 本身並不是通過 requestIdleCallback 來實現讓瀏覽器在空閒時間渲染工作單元的。
另一方面,為了讓渲染工作可以分離成一個個小單元,React 設計了 fiber。
每一個 element 都是一個 fiber 結構,每一個 fiber 都是一個渲染工作單元。
所以 fiber 既是一種資料結構,也是一個工作單元。
下文將通過簡單的示例對 fiber 進行介紹。
假設需要渲染這樣一個 element 樹:
myReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
生成的 fiber tree 如圖:
橙色代表子節點,黃色代表父節點,藍色代表兄弟節點。
每個 fiber 都有一個連結指向它的第一個子節點、下一個兄弟節點和它的父節點。這種資料結構可以讓我們更方便的查詢下一個工作單元。
上圖的箭頭也表明了 fiber 的渲染過程,渲染過程詳細描述如下:
- 從 root 開始,找到第一個子節點 div;
- 找到 div 的第一個子節點 h1;
- 找到 h1 的第一個子節點 p;
- 找 p 的第一個子節點,如無子節點,則找下一個兄弟節點,找到 p 的兄弟節點 a;
- 找 a 的第一個子節點,如無子節點,也無兄弟節點,則找它的父節點的下一個兄弟節點,找到 a 的 父節點的兄弟節點 h2;
- 找 h2 的第一個子節點,找不到,找兄弟節點,找不到,找父節點 div 的兄弟節點,也找不到,繼續找 div 的父節點的兄弟節點,找到 root;
- 第 6 步已經找到了 root 節點,渲染已全部完成。
下面將渲染過程用程式碼實現。
- 將 render 中建立 DOM 節點的部分抽離為 creactDOM 函式;
/**
* createDom 建立 DOM 節點
* @param {fiber} fiber 節點
* @return {dom} dom 節點
*/
function createDom (fiber) {
// 如果是文字型別,建立空的文字節點,如果不是文字型別,按 type 型別建立節點
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// isProperty 表示不是 children 的屬性
const isProperty = key => key !== "children"
// 遍歷 props,為 dom 新增屬性
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
// 返回 dom
return dom
}
- 在 render 中設定第一個工作單元為 fiber 根節點;
fiber 根節點僅包含 children 屬性,值為引數 fiber。
// 下一個工作單元
let nextUnitOfWork = null
/**
* 將 fiber 新增至真實 DOM
* @param {element} fiber
* @param {container} 真實 DOM
*/
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
}
}
- 通過 requestIdleCallback 在瀏覽器空閒時,渲染 fiber;
/**
* workLoop 工作迴圈函式
* @param {deadline} 截止時間
*/
function workLoop(deadline) {
// 是否應該停止工作迴圈函式
let shouldYield = false
// 如果存在下一個工作單元,且沒有優先順序更高的其他工作時,迴圈執行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
// 如果截止時間快到了,停止工作迴圈函式
shouldYield = deadline.timeRemaining() < 1
}
// 通知瀏覽器,空閒時間應該執行 workLoop
requestIdleCallback(workLoop)
}
// 通知瀏覽器,空閒時間應該執行 workLoop
requestIdleCallback(workLoop)
- 渲染 fiber 的函式 performUnitOfWork;
/**
* performUnitOfWork 處理工作單元
* @param {fiber} fiber
* @return {nextUnitOfWork} 下一個工作單元
*/
function performUnitOfWork(fiber) {
// TODO 新增 dom 節點
// TODO 新建 filber
// TODO 返回下一個工作單元(fiber)
}
4.1 新增 dom 節點
function performUnitOfWork(fiber) {
// 如果 fiber 沒有 dom 節點,為它建立一個 dom 節點
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 如果 fiber 有父節點,將 fiber.dom 新增至父節點
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
4.2 新建 filber
function performUnitOfWork(fiber) {
// ~~省略~~
// 子節點
const elements = fiber.props.children
// 索引
let index = 0
// 上一個兄弟節點
let prevSibling = null
// 遍歷子節點
while (index < elements.length) {
const element = elements[index]
// 建立 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 將第一個子節點設定為 fiber 的子節點
if (index === 0) {
fiber.child = newFiber
} else if (element) {
// 第一個之外的子節點設定為該節點的兄弟節點
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
4.3 返回下一個工作單元(fiber)
function performUnitOfWork(fiber) {
// ~~省略~~
// 如果有子節點,返回子節點
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟節點,返回兄弟節點
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 否則繼續走 while 迴圈,直到找到 root。
nextFiber = nextFiber.parent
}
}
以上我們實現了將 fiber 渲染到頁面的功能,且渲染過程是可中斷的。
現在試一下,程式碼如下:
const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)
myReact.render(element, document.getElementById('container'))
本例完整原始碼見:reactDemo7
如預期輸出 dom,如圖:
5. 渲染提交階段
由於渲染過程被我們做了可中斷的,那麼中斷的時候,我們肯定不希望瀏覽器給使用者展示的是渲染了一半的 UI。
對渲染提交階段優化的處理如下:
- 把 performUnitOfWork 中關於把子節點新增至父節點的邏輯刪除;
function performUnitOfWork(fiber) {
// 把這段刪了
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
- 新增一個根節點變數,儲存 fiber 根節點;
// 根節點
let wipRoot = null
function render (element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
}
// 下一個工作單元是根節點
nextUnitOfWork = wipRoot
}
- 當所有 fiber 都工作完成時,nextUnitOfWork 為 undefined,這時再渲染真實 DOM;
function workLoop (deadline) {
// 省略
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
// 省略
}
- 新增 commitRoot 函式,執行渲染真實 DOM 操作,遞迴將 fiber tree 渲染為真實 DOM;
// 全部工作單元完成後,將 fiber tree 渲染為真實 DOM;
function commitRoot () {
commitWork(wipRoot.child)
// 需要設定為 null,否則 workLoop 在瀏覽器空閒時不斷的執行。
wipRoot = null
}
/**
* performUnitOfWork 處理工作單元
* @param {fiber} fiber
*/
function commitWork (fiber) {
if (!fiber) return
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 渲染子節點
commitWork(fiber.child)
// 渲染兄弟節點
commitWork(fiber.sibling)
}
本例完整原始碼見:reactDemo8
原始碼執行結果如圖:
6. 協調(diff 演算法)
當 element 有更新時,需要將更新前的 fiber tree 和更新後的 fiber tree 進行比較,得到比較結果後,僅對有變化的 fiber 對應的 dom 節點進行更新。
通過協調,減少對真實 DOM 的操作次數。
1. currentRoot
新增 currentRoot 變數,儲存根節點更新前的 fiber tree,為 fiber 新增 alternate 屬性,儲存 fiber 更新前的 fiber tree;
let currentRoot = null
function render (element, container) {
wipRoot = {
// 省略
alternate: currentRoot
}
}
function commitRoot () {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
2. performUnitOfWork
將 performUnitOfWork 中關於新建 fiber 的邏輯,抽離到 reconcileChildren 函式;
/**
* 協調子節點
* @param {fiber} fiber
* @param {elements} fiber 的 子節點
*/
function reconcileChildren (fiber, elements) {
// 用於統計子節點的索引值
let index = 0
// 上一個兄弟節點
let prevSibling = null
// 遍歷子節點
while (index < elements.length) {
const element = elements[index]
// 新建 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// fiber的第一個子節點是它的子節點
if (index === 0) {
fiber.child = newFiber
} else if (element) {
// fiber 的其他子節點,是它第一個子節點的兄弟節點
prevSibling.sibling = newFiber
}
// 把新建的 newFiber 賦值給 prevSibling,這樣就方便為 newFiber 新增兄弟節點了
prevSibling = newFiber
// 索引值 + 1
index++
}
}
3. reconcileChildren
在 reconcileChildren 中對比新舊 fiber;
3.1 當新舊 fiber 型別相同時
保留 dom,僅更新 props,設定 effectTag 為 UPDATE;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
// oldFiber 可以在 wipFiber.alternate 中找到
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
while (index < elements.length || oldFiber != null) {
const element = elements[index]
let newFiber = null
// fiber 型別是否相同
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
// 如果型別相同,僅更新 props
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
// ~~省略~~
}
// ~~省略~~
}
3.2 當新舊 fiber 型別不同,且有新元素時
建立一個新的 dom 節點,設定 effectTag 為 PLACEMENT;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
// ~~省略~~
}
3.3 當新舊 fiber 型別不同,且有舊 fiber 時
刪除舊 fiber,設定 effectTag 為 DELETION;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ~~省略~~
}
4. deletions
新建 deletions 陣列儲存需刪除的 fiber 節點,渲染 DOM 時,遍歷 deletions 刪除舊 fiber;
let deletions = null
function render (element, container) {
// 省略
// render 時,初始化 deletions 陣列
deletions = []
}
// 渲染 DOM 時,遍歷 deletions 刪除舊 fiber
function commitRoot () {
deletions.forEach(commitWork)
}
5. commitWork
在 commitWork 中對 fiber 的 effectTag 進行判斷,並分別處理。
5.1 PLACEMENT
當 fiber 的 effectTag 為 PLACEMENT 時,表示是新增 fiber,將該節點新增至父節點中。
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
5.2 DELETION
當 fiber 的 effectTag 為 DELETION 時,表示是刪除 fiber,將父節點的該節點刪除。
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
5.3 UPDATE
當 fiber 的 effectTag 為 UPDATE 時,表示是更新 fiber,更新 props 屬性。
else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
updateDom 函式根據不同的更新型別,對 props 屬性進行更新。
const isProperty = key => key !== "children"
// 是否是新屬性
const isNew = (prev, next) => key => prev[key] !== next[key]
// 是否是舊屬性
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
// 刪除舊屬性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 更新新屬性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
另外,為 updateDom 新增事件屬性的更新、刪除,便於追蹤 fiber 事件的更新。
function updateDom(dom, prevProps, nextProps) {
// ~~省略~~
const isEvent = key => key.startsWith("on")
//刪除舊的或者有變化的事件
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// 註冊新事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// ~~省略~~
}
替換 creactDOM 中設定 props 的邏輯。
function createDom (fiber) {
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// 看這裡鴨
updateDom(dom, {}, fiber.props)
return dom
}
新建一個包含輸入表單項的例子,嘗試更新 element,程式碼如下:
/** @jsx myReact.createElement */
const container = document.getElementById("container")
const updateValue = e => {
rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
myReact.render(element, container)
}
rerender("World")
本例完整原始碼見:reactDemo9
輸出結果如圖:
7. 函式式元件
先來看一個簡單的函式式元件示例:
myReact 還不支援函式式元件,下面程式碼執行會報錯,這裡僅用於比照函式式元件的常規使用方式。
/** @jsx myReact.createElement */
const container = document.getElementById("container")
function App (props) {
return (
<h1>hi~ {props.name}</h1>
)
}
const element = (
<App name='foo' />
)
myReact.render(element, container)
函式式元件和 html 標籤元件相比,有以下兩點不同:
- 函式元件的 fiber 沒有 dom 節點;
- 函式元件的 children 需要執行函式後得到;
通過下列步驟實現函式元件:
- 修改 performUnitOfWork,根據 fiber 型別,執行 fiber 工作單元;
function performUnitOfWork(fiber) {
// 是否是函式型別元件
const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
// 如果是函式元件,執行 updateFunctionComponent 函式
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
// 如果不是函式元件,執行 updateHostComponent 函式
updateHostComponent(fiber)
}
// 省略
}
- 定義 updateHostComponent 函式,執行非函式元件;
非函式式元件可直接將 fiber.props.children 作為引數傳遞。
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
- 定義 updateFunctionComponent 函式,執行函式元件;
函式元件需要執行來獲得 fiber.children。
function updateFunctionComponent(fiber) {
// fiber.type 就是函式元件本身,fiber.props 就是函式元件的引數
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
- 修改 commitWork 函式,相容沒有 dom 節點的 fiber;
4.1 修改 domParent 的獲取邏輯,通過 while 迴圈不斷向上尋找,直到找到有 dom 節點的父 fiber;
function commitWork (fiber) {
// 省略
let domParentFiber = fiber.parent
// 如果 fiber.parent 沒有 dom 節點,則繼續找 fiber.parent.parent.dom,直到有 dom 節點。
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// 省略
}
4.2 修改刪除節點的邏輯,當刪除節點時,需要不斷向下尋找,直到找到有 dom 節點的子 fiber;
function commitWork (fiber) {
// 省略
// 如果 fiber 的更新型別是刪除,執行 commitDeletion
else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber.dom, domParent)
}
// 省略
}
// 刪除節點
function commitDeletion (fiber, domParent) {
// 如果該 fiber 有 dom 節點,直接刪除
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
// 如果該 fiber 沒有 dom 節點,則繼續找它的子節點進行刪除
commitDeletion(fiber.child, domParent)
}
}
下面試一下上面的例子,程式碼如下:
/** @jsx myReact.createElement */
const container = document.getElementById("container")
function App (props) {
return (
<h1>hi~ {props.name}</h1>
)
}
const element = (
<App name='foo' />
)
myReact.render(element, container)
本例完整原始碼見:reactDemo10
執行結果如圖:
8. hooks
下面繼續為 myReact 新增管理狀態的功能,期望是函式元件擁有自己的狀態,且可以獲取、更新狀態。
一個擁有計數功能的函式元件如下:
function Counter() {
const [state, setState] = myReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
已知需要一個 useState 方法用來獲取、更新狀態。
這裡再重申一下,渲染函式元件的前提是,執行該函式元件,因此,上述 Counter 想要更新計數,就會在每次更新都執行一次 Counter 函式。
通過以下步驟實現:
- 新增全域性變數 wipFiber;
// 當前工作單元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 當前工作單元 fiber 的 hook
wipFiber.hook = []
// 省略
}
- 新增 useState 函式;
// initial 表示初始引數,在本例中,initial=1
function useState (initial) {
// 是否有舊鉤子,舊鉤子儲存了上一次更新的 hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hook
// 初始化鉤子,鉤子的狀態是舊鉤子的狀態或者初始狀態
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
// 從舊的鉤子佇列中獲取所有動作,然後將它們一一應用到新的鉤子狀態
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
// 設定鉤子狀態
const setState = action => {
// 將動作新增至鉤子佇列
hook.queue.push(action)
// 更新渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
// 把鉤子新增至工作單元
wipFiber.hook = hook
// 返回鉤子的狀態和設定鉤子的函式
return [hook.state, setState]
}
下面執行一下計陣列件,程式碼如下:
function Counter() {
const [state, setState] = myReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
本例完整原始碼見:reactDemo11
執行結果如圖:
本章節簡單實現了 myReact 的 hooks 功能。
撒花完結,react 還有很多實現值得我們去學習和研究,希望有下期,和大家一起手寫 react 的更多功能。
總結
本文參考 pomb.us 進行學習,實現了包括虛擬 DOM、Fiber、Diff 演算法、函式式元件、hooks 等功能的自定義 React。
在實現過程中小編對 React 的基本術語及實現思路有了大概的掌握,pomb.us 是非常適合初學者的學習資料,可以直接通過 pomb.us 進行學習,也推薦跟著本文一步步實現 React 的常見功能。
本文原始碼: github原始碼 。
建議跟著一步步敲,進行實操練習。
希望能對你有所幫助,感謝閱讀~
別忘了點個贊鼓勵一下我哦,筆芯❤️
參考資料
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: