首頁  >  文章  >  web前端  >  建構一個小型 React ChHooks

建構一個小型 React ChHooks

Linda Hamilton
Linda Hamilton原創
2024-10-20 18:31:02822瀏覽

Build a Tiny React ChHooks

本教學是基於本教學,但使用了 JSX、Typescript 和更簡單的實作方法。您可以在我的 GitHub 儲存庫上查看註釋和程式碼。

好吧,在深入討論之前,我們需要對最後一章進行一些總結 - 還有一些東西需要修復,但是最後一章太多了,所以,好吧,就在這裡。

修復最後一章

這裡有一些小問題 - 不完全是錯誤,但最好修復它們。

vDOM 比較

在 javascript 中,兩個函數只有相同才相等,即使過程相同也不相等,即

const a = () => 1;
const b = () => 1;
a === b; // false

所以,當談到vDOM比較時,我們應該跳過函數比較。這是修復方法,

for (let i = 0; i < aKeys.length; i++) {
    const key = aKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
for (let i = 0; i < bKeys.length; i++) {
    const key = bKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}

處理 CSS

樣式應被視為一種特殊屬性,歸因於具有 .style 屬性的元素。這是修復方法,

export type VDomAttributes = { 
    key?: string | number
    style?: object
    [_: string]: unknown | undefined
}

export function createDom(vDom: VDomNode): HTMLElement | Text {
    if (isElement(vDom)) {
        const element = document.createElement(vDom.tag)
        Object.entries(vDom.props ?? {}).forEach(([name, value]) => {
            if (value === undefined) return
            if (name === 'key') return
            if (name === 'style') {
                Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => {
                    element.style[styleName as any] = styleValue as any
                })
                return
            }
            if (name.startsWith('on') && value instanceof Function) {
                element.addEventListener(name.slice(2).toLowerCase(), value as EventListener)
            } else {
                element.setAttribute(name, value?.toString() ?? "")
            }
        })
        return element
    } else {
        return document.createTextNode(vDom)
    }
}

現在這些輔助修復已經完成,讓我們繼續本章的主題 - Hooks。

封裝 vDOM 創建

我們之前明確地呼叫了 render(vDom, app!),這需要使用者建立 vDOM,這裡有一個更好的方法。

import { mount, useState, type FuncComponent } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    const [cnt, setCnt] = useState(0)
    return <div>
        <button onClick={() => setCnt(cnt() + 1)}>Click me</button>
        <p>Count: {cnt()}</p>
    </div>
}
const app = document.getElementById('app')
mount(App, {}, [], app!)
let reRender: () => void = () => {}
export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) {
    reRender = () => {
        const vDom = app(props, children) as unknown as VDomNode
        render(vDom, parent)
    }
    reRender()
}

或多或少看起來更好了。現在讓我們進入本章的主題—Hooks。

使用狀態

好吧,讓我們開始吧。我們要實作的第一個鉤子是 useState。它是一個允許我們管理元件狀態的鉤子。我們可以為 useState 提供以下簽名,

請注意,我們的實作與原始 React 略有不同。我們將傳回一個 getter 和一個 setter 函數,而不是直接傳回狀態。

function useState<T>(initialValue: T): [() => T, (newValue: T) => void] {
    // implementation
}

那我們將把這個值掛在哪裡呢?如果我們只是將它隱藏在閉包本身中,那麼當元件重新渲染時,該值將會遺失。如果你堅持這樣做,你需要存取外部函數的空間,這在 javascript 中是不可能的。

所以我們的方法是將其儲存在纖維中,你猜對了。那麼,讓我們為光纖添加一個欄位。

interface Fiber {
    parent: Fiber | null
    sibling: Fiber | null
    child: Fiber | null
    vDom: VDomNode
    dom: HTMLElement | Text | null
    alternate: Fiber | null
    committed: boolean
    hooks?: {
        state: unknown[]
    },
    hookIndex?: {
        state: number
    }
}

我們只將鉤子掛載到根 Fiber,因此我們可以將以下行加入掛載函數。

export function render(vDom: VDomNode, parent: HTMLElement) {
    wip = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: null,
        committed: false,
        alternate: oldFiber,
        hooks: oldFiber?.hooks ?? {
            state: []
        },
        hookIndex: {
            state: 0
        }
    }
    wipParent = parent
    nextUnitOfWork = wip
}

Hook索引稍後會使用。現在,每次重新渲染元件時,鉤子索引都會重置,但舊的鉤子會被保留。

請注意,我們渲染元件 vDOM,只有舊的 Fiber 是可存取的,因此我們只能操作該變數。不過一開始它就是空的,所以我們來設定一個虛擬的。

const a = () => 1;
const b = () => 1;
a === b; // false

現在我們將有大量的大腦時間- 因為每個鉤子調用的順序是固定的(你不能在循環或條件中使用鉤子,基本的React規則,你知道為什麼現在是這樣),所以我們可以安全地使用使用hookIndex 來存取鉤子。

for (let i = 0; i < aKeys.length; i++) {
    const key = aKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
for (let i = 0; i < bKeys.length; i++) {
    const key = bKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}

好吧,讓我們試試看吧,

export type VDomAttributes = { 
    key?: string | number
    style?: object
    [_: string]: unknown | undefined
}

export function createDom(vDom: VDomNode): HTMLElement | Text {
    if (isElement(vDom)) {
        const element = document.createElement(vDom.tag)
        Object.entries(vDom.props ?? {}).forEach(([name, value]) => {
            if (value === undefined) return
            if (name === 'key') return
            if (name === 'style') {
                Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => {
                    element.style[styleName as any] = styleValue as any
                })
                return
            }
            if (name.startsWith('on') && value instanceof Function) {
                element.addEventListener(name.slice(2).toLowerCase(), value as EventListener)
            } else {
                element.setAttribute(name, value?.toString() ?? "")
            }
        })
        return element
    } else {
        return document.createTextNode(vDom)
    }
}

這確實有效 - 計數從零增加到一,但不會進一步增加。

嗯...很奇怪吧?讓我們看看發生了什麼,調試時間。

import { mount, useState, type FuncComponent } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    const [cnt, setCnt] = useState(0)
    return <div>
        <button onClick={() => setCnt(cnt() + 1)}>Click me</button>
        <p>Count: {cnt()}</p>
    </div>
}
const app = document.getElementById('app')
mount(App, {}, [], app!)

你會看到,它總是記錄1。但是網頁告訴我們它是1,所以應該是2。這是怎麼回事?

對於原生類型,javascript 是按值傳遞,因此值是複製的,而不是引用的。在 React 類別元件中,它需要你有一個狀態物件來解決問題。在函數式元件中,React 使用閉包來解決。但如果我們要使用後者,則需要對程式碼進行很大的更改。所以一個簡單的取得pass的方法是,使用函數來取得狀態,這樣函數總是會傳回最新的狀態。

let reRender: () => void = () => {}
export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) {
    reRender = () => {
        const vDom = app(props, children) as unknown as VDomNode
        render(vDom, parent)
    }
    reRender()
}

現在,我們明白了!有用!我們為我們的小型 React 創建了 useState 鉤子。

概括

好吧,你可能認為這一章太短了——鉤子對於反應來說很重要,那麼為什麼我們只實現了 useState 呢?

首先,許多鉤子只是 useState 的變體。這種鉤子與其呼叫的元件無關,例如 useMemo。這些都是小事,我們沒有時間可以浪費。

但是,第二個也是最重要的原因是,對於像 useEffect 這樣的鉤子,在我們目前基於根更新的框架下,它們幾乎是不可能做到的。當 Fiber 卸載時,你不能發出訊號,因為我們只取得全域 vDOM 並更新整個 vDOM,而在真正的 React 中,情況並非如此。

在真實的 React 中,功能元件是由父元件更新的,因此父元件可以向子元件發出卸載訊號。但在我們的例子中,我們只更新根元件,因此我們無法通知子元件卸載。

不過目前的小專案已經基本展示了react的工作原理,希望對大家更好地理解react框架有幫助。

以上是建構一個小型 React ChHooks的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn