這是我參與更文挑戰的第7天,活動詳情查看: 更文挑戰javascript
爲何是鉑金呢,由於和王者還有很遠的距離。本文僅實現簡單版本的 React,參考 React 16.8 的基本功能,包括虛擬 DOM、Fiber、Diff 算法、函數式組件、hooks 等。html
本文基於 pomb.us/build-your-… 實現簡單版 React。java
本文學習思路來自 卡頌-b站-React源碼,你在第幾層。node
模擬的版本爲 React 16.8。react
將實現如下功能:git
下面上正餐,請繼續閱讀。github
先來看看一個簡單的 React Demo,代碼以下:web
const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);
複製代碼
本例完整源碼見:reactDemo面試
在瀏覽器中打開 reactDemo.html,展現以下:算法
咱們須要實現本身的 React,那麼就須要知道上面的代碼到底作了什麼。
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 對象,該對象擁有如下屬性:
ReactDOM.render()
將 element 添加到 id 爲 container 的 DOM 節點中,下面咱們將簡單手寫一個方法代替 ReactDOM.render()
。
const node = document.createElement(element.type)
複製代碼
node["title"] = element.props.title
複製代碼
const text = document.createTextNode("")
複製代碼
text["nodeValue"] = element.props.children
複製代碼
node.appendChild(text)
複製代碼
container.appendChild(node)
複製代碼
本例完整源碼見:reactDemo2
運行源碼,結果以下,和引入 React 的結果一致:
上文經過模擬 React,簡單代替了 React.createElement、ReactDOM.render 方法,接下來將真正開始實現 React 的各個功能。
上面有了解到 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
render 函數幫助咱們將 element 添加至真實節點中。
將分爲如下步驟實現:
/** * 將虛擬 DOM 添加至真實 DOM * @param {element} 虛擬 DOM * @param {container} 真實 DOM */
function render (element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
複製代碼
element.props.children.forEach(child =>
render(child, dom)
)
複製代碼
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(element.type)
複製代碼
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
結果如圖,成功輸出:
再來看看上面寫的 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 是用來執行單元事件,並返回下一個單元事件的,具體實現將在下文介紹。
上文介紹了經過 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 的渲染過程,渲染過程詳細描述以下:
下面將渲染過程用代碼實現。
/** * 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
}
複製代碼
fiber 根節點僅包含 children 屬性,值爲參數 fiber。
// 下一個工做單元
let nextUnitOfWork = null
/** * 將 fiber 添加至真實 DOM * @param {element} fiber * @param {container} 真實 DOM */
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
}
}
複製代碼
/** * 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)
複製代碼
/** * 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,如圖:
因爲渲染過程被咱們作了可中斷的,那麼中斷的時候,咱們確定不但願瀏覽器給用戶展現的是渲染了一半的 UI。
對渲染提交階段優化的處理以下:
function performUnitOfWork(fiber) {
// 把這段刪了
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
複製代碼
// 根節點
let wipRoot = null
function render (element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
}
// 下一個工做單元是根節點
nextUnitOfWork = wipRoot
}
複製代碼
function workLoop (deadline) {
// 省略
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
// 省略
}
複製代碼
// 所有工做單元完成後,將 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
源碼運行結果如圖:
當 element 有更新時,須要將更新前的 fiber tree 和更新後的 fiber tree 進行比較,獲得比較結果後,僅對有變化的 fiber 對應的 dom 節點進行更新。
經過協調,減小對真實 DOM 的操做次數。
新增 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
}
複製代碼
將 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++
}
}
複製代碼
在 reconcileChildren 中對比新舊 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",
}
}
// ~~省略~~
}
// ~~省略~~
}
複製代碼
建立一個新的 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",
}
}
// ~~省略~~
}
複製代碼
刪除舊 fiber,設置 effectTag 爲 DELETION;
function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ~~省略~~
}
複製代碼
新建 deletions 數組存儲需刪除的 fiber 節點,渲染 DOM 時,遍歷 deletions 刪除舊 fiber;
let deletions = null
function render (element, container) {
// 省略
// render 時,初始化 deletions 數組
deletions = []
}
// 渲染 DOM 時,遍歷 deletions 刪除舊 fiber
function commitRoot () {
deletions.forEach(commitWork)
}
複製代碼
在 commitWork 中對 fiber 的 effectTag 進行判斷,並分別處理。
當 fiber 的 effectTag 爲 PLACEMENT 時,表示是新增 fiber,將該節點新增至父節點中。
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
複製代碼
當 fiber 的 effectTag 爲 PLACEMENT 時,表示是刪除 fiber,將父節點的該節點刪除。
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
複製代碼
當 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
輸出結果如圖:
先來看一個簡單的函數式組件示例:
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 標籤組件相比,有如下兩點不一樣:
經過下列步驟實現函數組件:
function performUnitOfWork(fiber) {
// 是不是函數類型組件
const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
// 若是是函數組件,執行 updateFunctionComponent 函數
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
// 若是不是函數組件,執行 updateHostComponent 函數
updateHostComponent(fiber)
}
// 省略
}
複製代碼
非函數式組件可直接將 fiber.props.children 做爲參數傳遞。
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
複製代碼
函數組件須要運行來得到 fiber.children。
function updateFunctionComponent(fiber) {
// fiber.type 就是函數組件自己,fiber.props 就是函數組件的參數
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
複製代碼
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
運行結果如圖:
下面繼續爲 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 函數。
經過如下步驟實現:
// 當前工做單元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 當前工做單元 fiber 的 hook
wipFiber.hook = []
// 省略
}
複製代碼
// initial 表示初始參數,在本例中,initail=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源碼 。
建議跟着一步步敲,進行實操練習。
但願能對你有所幫助,感謝閱讀~
別忘了點個贊鼓勵一下我哦,筆芯❤️