跳到主要内容

仿写 mini-vue 虚拟dom & domdiff

· 阅读需 8 分钟
Yancy Qi
Front End Engineer

完成一个简版Vue3 Day 03 虚拟dom & domdiff

01 虚拟dom

在 core 目录下新建 h.js 文件:

// 创建一个虚拟节点 vdom vnode
export function h (tag, props, children) {
return {
tag,
props,
children,
}
}

修改 App.js 文件:

// reactivity 响应式实现

// const { effect, reactive } = require('@vue/reactivity')
// const { effectWatch, reactive } = require('./core/reactivity')
import { effectWatch, reactive } from './core/reactivity/index.js'
import { h } from './core/h.js'

// v1
// let a = 10
// let b = a + 10
// console.log(b)

// a = 20
// b = a + 10
// console.log(b)

// v2
// let a = 10
// let b
// update()
// function update() {
// b = a + 10
// console.log(b)
// }

// a = 20
// update()

// v3
// a 发生变更了,我想让 b 自动更新

// 声明一个响应式对象
let a = reactive({
value: 1,
})
let b
effectWatch(() => {
// 函数
// 1. 会执行以下
b = a.value + 10
console.log(b)
})

// a 响应式对象的值发生改变之后 再次执行
a.value = 30

// vue3

export default {
// template → render
render(context) {
// 构建 view = b
// view → 每次都需要重新创建
// 要计算出最小的更新点 → vdom
// js → diff
// reset
// tag
// props
// children
// const div = document.createElement("div")
// div.innerText = context.state.count
// return div

return h("div", null, context.state.count)
},
setup() {
// a = 响应式数据
const state = reactive({
count: 0
})
window.state = state
return { state }
},
}

// App.render(App.setup())

修改 core 目录下的 index.js 文件:

import { effectWatch } from './reactivity/index.js'

export function createApp (rootComponent) {
return {
mount(rootContainer) {
const context = rootComponent.setup()

effectWatch(() => {
rootContainer.innerHTML = ``
const subTree = rootComponent.render(context)
console.log(subTree)

// rootContainer.append(element)
})
},
}
}

重新执行 html 文件:

img

在 core 目录下新建 renderer 目录,在 renderer 目录下新建 index.js 文件:

// vdom → dom
export function mountElement (vnode, container) {
const { tag, props, children } = vnode

// tag
const element = document.createElement(tag)

// props
if (props) {
for (const key in props) {
const val = props[key]
element.setAttribute(key, val)
}
}

// children
// 1. 可以接受一个 字符串类型 string
if (typeof children === "string") {
const textNode = document.createTextNode(children)
element.append(textNode)
}
// 2. 可以接受一个数组

// 插入
container.append(element)
}

修改 App.js 文件:

return h(
"div",
{
id: "app - id",
class: "showTime",
},
context.state.count
)

修改 core 目录下的 index.js 文件:

import { effectWatch } from './reactivity/index.js'
import { mountElement } from './renderer/index.js'

export function createApp (rootComponent) {
return {
mount(rootContainer) {
const context = rootComponent.setup()

effectWatch(() => {
rootContainer.innerHTML = ``
const subTree = rootComponent.render(context)
console.log(subTree)
mountElement(subTree, rootContainer)
// rootContainer.append(element)
})
},
}
}

重新执行 html 文件:

img

修改 App.js 文件:

return h(
"div",
{
id: "app - id",
class: "showTime",
},
String(context.state.count)
)

img

增加对数组的判断:

// children
// 1. 可以接受一个 字符串类型 string
if (typeof children === "string") {
const textNode = document.createTextNode(children)
element.append(textNode)
} else if (Array.isArray(children)) {
// 2. 可以接受一个数组
children.forEach((vnode) => {
mountElement(vnode, element)
})
}

修改 App.js 文件:

return h(
"div",
{
id: "app - id",
class: "showTime",
},
// String(context.state.count)
[
h("p", null, "heihei"),
h("p", null, "hahaha"),
]
)

img

02 domdiff

修改 App.js 文件:

return h(
"div",
{
id: "app - id",
class: "showTime",
},
// String(context.state.count)
[
h("p", null, String(context.state.count)),
h("p", null, "hahaha"),
]
)

img

img

修改 core 目录下的 index.js 文件:

import { effectWatch } from './reactivity/index.js'
import { mountElement, diff } from './renderer/index.js'

export function createApp (rootComponent) {
return {
mount(rootContainer) {
const context = rootComponent.setup()
const isMounted = false
let prevSubTree

effectWatch(() => {
if (!isMounted) {
// init
rootContainer.innerHTML = ``
const subTree = rootComponent.render(context)
console.log(subTree)
mountElement(subTree, rootContainer)
prevSubTree = subTree
} else {
// update
const subTree = rootComponent.render(context)
diff(prevSubTree, subTree)
prevSubTree = subTree
}

// diff
// newVnode oldVnode

// rootContainer.append(element)
})
},
}
}

修改 renderer 目录下的 index.js 文件:

// n1 → oldVnode
// n2 → newVnode
export function diff (n1, n2) {
// 1. tag
if (n1.tag !== n2.tag) {
n1.el.replaceWith(document.createElement(n2.tag))
} else {
// 2. props
// new: {id:"foo", class: "bar", a}
// old: {id:"foo", class: "bar1", a, b}
const {props: newProps} = n2
const {props: oldProps} = n1

if (newProps && oldProps) {
Object.keys(newProps).forEach((key) => {
const newVal = newProps[key]
const oldVal = oldProps[key]
if (newVal !== oldVal) {
n1.el.setAttribute(key, newVal)
}
})
}

if (oldProps) {
Object.keys(oldProps).forEach((key) => {
if (!newProps[key]) {
n1.el.removeAttribute(key)
}
})
}
// 3. children → (暴力的解法)
}
}

// vdom → dom
export function mountElement (vnode, container) {
const { tag, props, children } = vnode

// tag
const element = (vnode.el = document.createElement(tag))

// props
if (props) {
for (const key in props) {
const val = props[key]
element.setAttribute(key, val)
}
}

// children
// 1. 可以接受一个 字符串类型 string
if (typeof children === "string") {
const textNode = document.createTextNode(children)
element.append(textNode)
} else if (Array.isArray(children)) {
// 2. 可以接受一个数组
children.forEach((vnode) => {
mountElement(vnode, element)
})
}

// 插入
container.append(element)
}

修改 App.js 文件:

return h(
"div",
{
id: "app - " + context.state.count,
class: "showTime",
},
// String(context.state.count)
[
h("p", null, String(context.state.count)),
h("p", null, "hahaha"),
]
)

img

img

修改 core 目录下的 index.js 文件:

import { effectWatch } from './reactivity/index.js'
import { mountElement, diff } from './renderer/index.js'

export function createApp (rootComponent) {
return {
mount(rootContainer) {
const context = rootComponent.setup()
let isMounted = false
let prevSubTree

effectWatch(() => {
if (!isMounted) {
// init
isMounted = true
rootContainer.innerHTML = ``
const subTree = rootComponent.render(context)
console.log(subTree)
mountElement(subTree, rootContainer)
prevSubTree = subTree
} else {
// update
const subTree = rootComponent.render(context)
diff(prevSubTree, subTree)
prevSubTree = subTree
}

// diff
// newVnode oldVnode

// rootContainer.append(element)
})
},
}
}

console 一下:

img

img

img

完整 diff 函数代码 大致如下:

// n1 → oldVnode
// n2 → newVnode
export function diff (n1, n2) {
console.log(n1, n2)
// 1. tag
if (n1.tag !== n2.tag) {
n1.el.replaceWith(document.createElement(n2.tag))
} else {
// 小细节
const el = (n2.el = n1.el)

// 2. props
// new: {id:"foo", class: "bar", a}
// old: {id:"foo", class: "bar1", a, b}
const {props: newProps} = n2
const {props: oldProps} = n1

if (newProps && oldProps) {
Object.keys(newProps).forEach((key) => {
const newVal = newProps[key]
const oldVal = oldProps[key]
if (newVal !== oldVal) {
el.setAttribute(key, newVal)
}
})
}

if (oldProps) {
Object.keys(oldProps).forEach((key) => {
if (!newProps[key]) {
el.removeAttribute(key)
}
})
}

// 3. children → (暴力的解法)
// ① newChildren → string (oldChildren → string oldChildren → array)
// ② newChildren → array (oldChildren → string oldChildren → array)
const {children: newChildren} = n2
const {children: oldChildren} = n1

if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else if (Array.isArray(oldChildren)) {
el.textContent = newChildren
}
} else if (Array.isArray(newChildren)) {
if (typeof oldChildren === "string") {
el.innerText = `` // 先清空
mountElement(n2, el)
} else if (Array.isArray(oldChildren)) {
// new {a,b,c,d,f}
// old {a,e,c,d}

const length = Math.min(newChildren.length, oldChildren.length)

// 处理公共的 vnode
for (let index = 0; index < length; index++) {
const newVnode = newChildren[index]
const oldVnode = oldChildren[index]
diff(newVnode, oldVnode)
}

if (newChildren.length > length) {
// 创建节点
for (let index = length; index < newChildren.length; index++) {
const newVnode = newChildren[index]
mountElement(newVnode)
}
}

if (oldChildren.length > length) {
// 删除节点
for (let index = length; index < oldChildren.length; index++) {
const oldVnode = oldChildren[index]
oldVnode.el.parent.removeChild(oldVnode.el)
}
}
}
}
}
}

项目仓库: 点此访问