本教學是基於本教學,但使用了 JSX、Typescript 和更簡單的實作方法。您可以在我的 GitHub 儲存庫上查看註釋和程式碼。
好吧,在深入討論之前,我們需要對最後一章進行一些總結 - 還有一些東西需要修復,但是最後一章太多了,所以,好吧,就在這裡。
這裡有一些小問題 - 不完全是錯誤,但最好修復它們。
在 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 }
樣式應被視為一種特殊屬性,歸因於具有 .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。
我們之前明確地呼叫了 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中文網其他相關文章!