VirtualDom: patch関数を実装してみる
マウントした段階でvnodeに実際のDOMへの参照を持たせておきたいので、vnodeのelというプロパティを持たせておく
code: ~/packages/runtime-core/vnode.ts
export interface VNode<HostNode = RendererNode> {
type: VNodeTypes
props: VNodeProps | null
children: VNodeNormalizedChildren
el: HostNode | undefined
}
createRenderer 関数の中にpatch関数を実装していく。実装済の renderVNode 関数は消してしまう
code: ~/packages/runtime-core/renderer.ts
export function createRenderer(options: RendererOptions) {
// .
// .
// .
const patch = (n1: VNode | null, n2: VNode, container: RendererElement) => {
const { type } = n2
if(type === Text) {
// processText(n1, n2, container);
} else {
// processElement(n1, n2, container);
}
}
}
processElement の mountElement から実装
code: ~/packages/runtime-core/renderer.ts
const processElement(
n1: VNode | null,
n2: VNode,
container: RendererElement
) => {
if(n1 === null) {
mountElement(n2, container) // 比較対象のn1がない場合はmountするだけ
} else {
// patchElement(n1, n2) // n1がある場合は差分比較!
}
}
const mountElement = (vnode: VNode, container: RendererElement) => {
let el: RendererElement
const { type, props } = vnode
// NOTE: vnode.elを更新する理由は??
el = vnode.el = hostCreateElement(type as string)
// 子要素作成
mountChildren(vnode.children, el) // 今から実装する
// 属性やイベント紐付け
if (props) {
for (const key in props) {
hostPatchProp(el, key, propskey) }
}
// 親(container)にelを差し込む
hostInsert(el, container)
}
子要素のマウント処理
code: ~/packages/runtime-core/renderer.ts
const mountChildren = (children: VNode[], container: RendererElement) => {
for (let i = 0; i < children.length; i++) {
const child = (childreni = normalizeVNode(childreni)) // normalize関数を噛ませてVNodeの型にしておく patch(null, child, container)
}
}
これで要素のマウントは実装完了!
次は Text のマウント処理。
code: ~/packages/runtime-core/renderer.ts
const processText = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
) => {
if (n1 == null) {
// NOTE: ここでもn2.elを更新している
hostInsert((n2.el = hostCreateText(n2.children as string)), container) // dom操作しているだけ
} else {
// TODO: patch
}
}
ここまでで初回のマウントはできるようになった。render関数でpatch関数を使用してplaygroundで試してみる!
今まで createAppAPI の mount に書いていた処理を一部 render 関数に移植して、2つの vnodeを保持できるようにする。これでも初回の描画はできるようになっているはず
code: ~/packages/runtime-core/apiCreateApp.ts
return function createApp(rootComponent) {
const app: App = {
mount(rootContainer: HostElement) {
// rootComponentを渡すだけに
render(rootComponent, rootContainer)
},
}
}
code: ~/packages/runtime-core/renderer.ts
const render: RootRenderFunction = (rootComponent, container) => {
const componentRender = rootComponent.setup!()
let n1: VNode | null = null
const updateComponent = () => {
const n2 = componentRender()
patch(n1, n2, container)
n1 = n2
}
const effect = new ReactiveEffect(updateComponent)
effect.run()
}
patch関数の処理を書いて、画面の更新もできるようにする
code: ~/packages/runtime-core/renderer.ts
const patchElement = (n1: VNode, n2: VNode) => {
const el = (n2.el = n1.el!)
const props = n2.props
patchChildren(n1, n2, el)
for (const key in props) {
if (propskey !== n1.props?.key ?? {}) { hostPatchProp(el, key, propskey) }
}
}
// ※ 本来はkey属性などを付与して動的な長さの子要素に対応する必要あり (これはminimumな実装)
const patchChildren = (n1: VNode, n2: VNode, container: RendererElement) => {
const c1 = n1.children as VNode[]
const c2 = n2.children as VNode[]
for (let i = 0; i < c2.length; i++) {
// NOTE: c2i に代入する必要ある? 再帰しているから? const child = (c2i = normalizeVNode(c2i)) patch(c1i, child, container) }
}
Textも同様
code: packages/runtime-core/renderer.ts
const processText = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
) => {
if (n1 == null) {
// 省略
} else {
// patchの処理を追加
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
これで画面の更新を差分レンダリングによって行えるようになった!
mountする
- 実DOMへの反映
- 仮想DOMへの反映