本文最后更新于 322 天前,如有失效请评论区留言。
dom更新
接着上面的虚拟dom转换成真实dom后
假设我们的dom以及发生了变化
const vdom = h('div', { class: 'red' }, [
h('span', null, 'hello world')
])
由之前的vdom变成vdom2,我们需要的就是找出这次变化了什么,已达到最小改动的目的
const vdom2 = h('div', { class: 'green' }, [
h('span', null, 'changed')
])
需要把之前的mount方法中第一行添加上
function mount (vnode, container) {
// 把创建出来的真实标签存储一份到vnode的el属性下,方便patch时直接获取
const el = vnode.el = document.createElement(vnode.tag)
}
写一个patch函数用于更新dom,接受n1,n2参数(老的dom树和新的dom树)
function patch (n1, n2) {
// 首先要确保他们是同一个标签
if (n1.tag === n2.tag) {
const el = n2.el = n1.el
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)
// 对比新老中是否有key更改了
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue != oldValue) {
el.setAttribute(key, newValue)
}
}
// 当旧的中没有key要去除
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
}
}
接着再去对比children部分
// 接着处理children,可能是一个'string' 或者 []
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 新老的children都是字符串 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
// 新的children是数组,老的是字符串就遍历循环添加
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
// 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
// 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素
// 这里只是做简单的对比,不一样就直接替换
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 还需要考虑newChildren更长或者更短的情况下进行新增和删除
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child.el)
})
}
}
}
完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.red {
color: red;
}
.green {
color: green;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue"></script>
<script>
function h (tag, props, children) {
return {
tag,
props,
children
}
}
function mount (vnode, container) {
const el = vnode.el = document.createElement(vnode.tag)
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
el.setAttribute(key, value)
}
}
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child))
} else {
mount(child, el)
}
})
}
}
container.appendChild(el)
}
const vdom = h('div', { class: 'red' }, [
h('span', null, ['hello', h('div', null, 'ha')])
])
mount(vdom, document.getElementById('app'))
const vdom2 = h('div', { class: 'green' }, [
h('span', null, 'changed')
])
function patch (n1, n2) {
// 首先要确保他们是同一个标签
if (n1.tag === n2.tag) {
const el = n2.el = n1.el
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 如果有了template模板,下面的这些对比都有可能直接跳过(比如检测到静态标签直接跳过对比)
// 对比新老中是否有key更改了
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue != oldValue) {
el.setAttribute(key, newValue)
}
}
// 当旧的中没有key要去除
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 接着处理children,可能是一个'string' 或者 []
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
// 新老的children都是字符串 不同就直接替换
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
// 新的children是数组,老的是字符串就遍历循环添加
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 当新老的children都是数组,有两种情况,一种使用v-for提供了一个唯一的key,用key去对比
// 另一种就是双端指针 头头尾尾,vue2中是 头头,尾尾,旧头新尾,旧尾新头
// 以上四种情况都匹配不到的话,就以新头对旧的vnode进行查找,看是否找到,都找不到就新建元素
// 这里只是做简单的对比,不一样就直接替换
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 还需要考虑newChildren更长或者更短的情况下进行新增和删除
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => {
mount(child, el)
})
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(child => {
el.removeChild(child.el)
})
}
}
}
} else {
// 当不是同一个类型就直接情况再挂载
const el = n2.el = n1.el
el.innerHTML = ''
mount(n2, el)
}
}
patch(vdom, vdom2)
</script>
</body>
</html>