首页  >  文章  >  web前端  >  构建一个小型 React ChHooks

构建一个小型 React ChHooks

Linda Hamilton
Linda Hamilton原创
2024-10-20 18:31:02756浏览

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