本教程基于本教程,但使用了 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中文网其他相关文章!