이 튜토리얼은 이 튜토리얼을 기반으로 하지만 JSX, TypeScript 및 구현하기 더 쉬운 접근 방식을 사용합니다. 내 GitHub 저장소에서 메모와 코드를 확인할 수 있습니다.
자, 후크를 살펴보기 전에 마지막 장에 대해 간략히 정리해야 합니다. 아직 수정할 부분이 있지만 마지막 장의 내용이 너무 많아서 여기까지입니다.
다음은 몇 가지 사소한 사항입니다. 완전한 버그는 아니지만 수정하는 것이 좋습니다.
자바스크립트에서 두 함수는 동일한 경우에만 동일하며, 동일한 절차를 사용해도 동일하지 않습니다. 즉,
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) } }
이제 측면 수정이 완료되었습니다. 이 장의 주요 주제인 후크로 넘어가겠습니다.
이전에는 사용자가 vDOM을 생성해야 하는 render(vDom, app!)를 명시적으로 호출했는데, 여기에 더 나은 방법이 있습니다.
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 }
그럼 가치를 어디에 연결할까요? 클로저 자체 내에 이를 숨기면 구성 요소가 다시 렌더링될 때 값이 손실됩니다. 굳이 그렇게 하려고 한다면 자바스크립트에서는 불가능한 외부 함수의 공간에 접근해야 합니다.
그래서 우리의 방법은 짐작하셨겠지만 섬유질에 저장하는 것입니다. 그럼 Fiber에 Field를 추가해 보겠습니다.
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 구성 요소를 렌더링하면 기존 Fiber에만 액세스할 수 있으므로 해당 변수만 조작할 수 있습니다. 다만, 처음에는 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을 기록합니다. 하지만 웹페이지에서는 1이라고 나와 있으므로 2여야 합니다. 무슨 일이 일어나고 있나요?
네이티브 유형의 경우 자바스크립트는 값을 전달하므로 값은 참조되지 않고 복사됩니다. 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)와 관련이 없습니다. 그런 일은 하찮은 일이고 우리에게는 낭비할 시간이 없습니다.
그러나 두 번째로 가장 중요한 이유는 현재 루트 업데이트 기반 프레임에서는 useEffect와 같은 후크의 경우 수행이 거의 불가능하다는 것입니다. Fiber가 마운트 해제되면 신호를 보낼 수 없습니다. 왜냐하면 우리는 전역 vDOM만 가져오고 전체 vDOM을 업데이트하기 때문입니다. 반면 실제 React에서는 그렇지 않습니다.
실제 React에서는 기능적 구성 요소가 상위 구성 요소에 의해 업데이트되므로 상위 구성 요소가 하위 구성 요소에 마운트 해제 신호를 보낼 수 있습니다. 하지만 우리의 경우 루트 구성요소만 업데이트하므로 하위 구성요소에 마운트 해제 신호를 보낼 수 없습니다.
하지만 현재 소규모 프로젝트에서는 기본적으로 React가 어떻게 작동하는지 보여주었고, React 프레임워크를 더 잘 이해하는 데 도움이 되기를 바랍니다.
위 내용은 작은 React ChHook 만들기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!