このチュートリアルはこのチュートリアルに基づいていますが、JSX、typescript、および実装がより簡単なアプローチを使用しています。私の GitHub リポジトリでメモとコードをチェックアウトできます。
さて、本題に入る前に、最後の章について少しまとめておく必要があります。まだ修正すべき点はありますが、最後の章はやりすぎたので、ここまでです。
ここでは、いくつかの小さな点を示します。完全なバグではありませんが、修正したほうがよいでしょう。
JavaScript では、2 つの関数は同じである場合にのみ同等であり、同じプロシージャを持っている場合でも等しくありません。つまり、
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) } }
これらの側面修正が完了したので、この章のメイントピックであるフックに進みましょう。
以前は明示的に 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() }
見た目は多かれ少なかれ良くなりました。さて、この章のメイントピックであるフックに移りましょう。
さて、サビにいきましょう。実装する最初のフックは useState です。これはコンポーネントの状態を管理できるようにするフックです。 useState には次の署名を含めることができます。
私たちの実装は元の React とは少し異なることに注意してください。状態を直接返すのではなく、ゲッター関数とセッター関数を返します。
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 } }
そして、フックをルートファイバーにマウントするだけなので、次の行をマウント関数に追加できます。
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 }
フックインデックスは後で使用します。現在、フックインデックスはコンポーネントが再レンダリングされるたびにリセットされますが、古いフックは引き継がれます。
コンポーネント vDOM をレンダリングするときは、古いファイバーのみにアクセスできるため、その変数のみを操作できることに注意してください。ただし、最初はnullなのでダミーを設定しましょう。
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) } }
これはある程度機能します。カウントは 0 から 1 に増加しましたが、それ以上増加しません。
そうですね...奇妙ですよね?何が起こっているのか見てみましょう。デバッグ時間です。
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 が記録されます。しかし、Web ページでは 1 であるため、2 になるはずです。何が起こっているのでしょうか?
ネイティブ型の場合、JavaScript は値渡しなので、値は参照されずにコピーされます。 React クラス コンポーネントでは、問題に対処するために状態オブジェクトが必要です。関数コンポーネントでは、React はクロージャで対応します。ただし、後者を使用する場合は、コードに大きな変更が必要になります。したがって、パスを取得する簡単な方法は、関数を使用して状態を取得し、関数が常に最新の状態を返すようにすることです。
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 など、呼び出されるコンポーネントとは無関係です。そんなことは些細な作業であり、無駄にする時間はありません。
しかし、2 番目に最も重要な理由は、現在のルート更新ベースのフレームでは useEffect のようなフックを実行することがほぼ不可能であるということです。ファイバーがアンマウントされると、グローバル vDOM をフェッチして vDOM 全体を更新するだけなので、シグナルを送ることはできませんが、実際の React ではそうではありません。
実際の React では、機能コンポーネントは親コンポーネントによって更新されるため、親コンポーネントは子コンポーネントにアンマウントするよう通知できます。ただし、この場合はルート コンポーネントのみを更新するため、子コンポーネントにアンマウントするよう通知できません。
ただし、現在の小規模プロジェクトは基本的に React がどのように機能するかを示しており、React フレームワークをより深く理解するのに役立つことを願っています。
以上が小さな React ChHook を構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。