《Vue.js 设计与实现》读书笔记 - 第9章、简单 Diff 算法
第9章、简单 Diff 算法
9.1 减少 DOM 操作的性能开销
在之前的章节,如果新旧子节点的类型都是数组,我们会先卸载所有旧节点,再挂载所有新的子节点。但是如果存在相同类型的节点,我们完全可以复用节点,只修改类型即可。
所以这一节采取就朴素的复用思路,按顺序依次 patch
节点,如果旧子节点多就卸载,新子节点多就挂载。
for (let i = 0; i < minLen; i++) {
patch(oldChildren[i], newChildren[i], container)
}
9.2 DOM 复用与 key 的作用
因为直接按顺序复用并不总是合理,比如我把第一个节点移到最后,则所有的节点顺序都乱了。在 Vue 中数组节点都需要设置 key,所以我们可以通过 key 来找到复用的节点。
通过两层循环我们可以找到一个新节点对应的旧节点,然后把两个节点进行 patch。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
break
}
}
}
}
但是 patch 只是原地修改节点的属性,之后还需要移动节点。
9.3 找到需要移动的元素
判断移动的方式也很简单,遍历新子元素数组,找到对应的旧节点的下标并记录。如果后续的节点对应旧节点下标大于当前下标,则不需要移动,同时保留最大下标,否则表示需要移动。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
} else {
// 不需要移动 更新最大下标
lastIndex = j
}
break
}
}
}
}
9.4 如何移动元素
直接判断需要移动就移动到上一个(新)子元素后就可以了。
function patchChildren() {
// ... 上面都是之前的逻辑
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
}
}
9.5 添加新元素
感觉这种情况也非常好理解,无非是旧节点中没有对应的,那就在新节点对应的前一个节点后面加入就好了。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let find = false
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
if (!find) {
// 没有对应的旧节点
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
}
新节点挂载需要使用 patch
来处理。不过之前的 patch 不能指定锚点,需要调整逻辑。
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
// ...
if (typeof type === 'string') {
if (!n1) {
// 挂载 传入anchor
mountElement(n2, container, anchor)
} else {
// 打补丁
patchElement(n1, n2)
}
}
// ...
}
function mountElement(vnode, container, anchor) {
// ...
insert(el, container, anchor)
}
9.6 移除不存在的元素
移除就是在旧节点找和新节点没有对应 key 的节点,然后卸载。
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
if (!has) {
unmount(oldVNode)
}
}
最后附本章完整代码:
点击查看代码
const { effect, ref } = VueReactivity
const Text = Symbol()
const Comment = Symbol()
const Fragment = Symbol()
function createRenderer(options) {
const {
createElement,
insert,
setElementText,
patchProps,
createText,
setText,
} = options
function mountElement(vnode, container, anchor) {
const el = (vnode.el = createElement(vnode.type))
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container, anchor)
}
// n1 旧node n2 新node
function patchElement(n1, n2) {
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
// 更新children
patchChildren(n1, n2, el)
}
function patchChildren(n1, n2, container) {
// 如果新节点是字符串类型
if (typeof n2.children === 'string') {
// 新节点只有在为一组节点的时候需要卸载处理 其他情况不需要任何操作
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
// 设置新内容
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 如果新子元素是一组节点
if (Array.isArray(n1.children)) {
// 如果旧子节点也是一组节点
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let find = false
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
if (!find) {
// 没有对应的就节点
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
if (!has) {
unmount(oldVNode)
}
}
} else {
// 否则旧节点不存在或者是字符串 只需要清空容器然后添加新节点就可以
setElementText(container, '')
n2.children.forEach((c) => patch(null, c, container))
}
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
}
}
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
// 挂载 传入anchor
mountElement(n2, container, anchor)
} else {
// 打补丁
patchElement(n1, n2)
}
} else if (type === Text) {
if (!n1) {
const el = (n2.el = createText(n2.children))
insert(el, container)
} else {
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
// 更新文本节点内容
setText(el, n2.children)
}
}
} else if (type === Fragment) {
if (!n1) {
// 如果之前不存在 需要把节点一次挂载
n2.children.forEach((c) => patch(null, c, container))
} else {
// 之前存在只需要更新子节点即可
patchChildren(n1, n2, children)
}
} else if (typeof type === 'object') {
// 组件
} else if (type == 'xxx') {
// 处理其他类型的vnode
}
}
// 传入一个 vnode 卸载与其相关联的 DOM 节点
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
function render(vnode, container) {
console.log(vnode, container)
if (vnode) {
// 如果有新 vnode 就和旧 vnode 一起用 patch 处理
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 没有新 vnode 但是有旧 vnode 卸载
unmount(container._vnode)
}
}
// 把旧 vnode 缓存到 container
container._vnode = vnode
}
function hydrate(vnode, container) {
// 服务端渲染
}
return {
render,
hydrate,
}
}
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === 'form' && el.tagName === 'INPUT') return false
return key in el
}
const renderer = createRenderer({
createElement(tag) {
return document.createElement(tag)
},
setElementText(el, text) {
el.textContent = text
},
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
},
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
},
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// evl: vue event invoker
const invokers = el._vel || (el._vel = {})
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
invoker = el._vel[key] = (e) => {
// e.timeStamp 是事件发生的时间
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
// 存储事件被绑定的时间
invoker.attached = performance.now()
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
}
if (key === 'class') {
el.className = nextValue || ''
} else if (shouldSetAsProps(el, key, nextValue)) {
// 判断 key 是否存在对应的 DOM Properties
const type = typeof el[key] // 获取该属性的类型
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 没有对应的 DOM Properties
el.setAttribute(key, nextValue)
}
},
})
const vnode1 = {
type: 'div',
children: [
{
type: 'p',
children: '1',
key: 1,
},
{
type: 'div',
children: '2',
key: 2,
},
{
type: 'h1',
children: '3',
key: 3,
},
],
}
const vnode2 = {
type: 'div',
children: [
{
type: 'h1',
children: '3',
// key: 3,
},
{
type: 'p',
children: '1',
// key: 1,
},
{
type: 'div',
children: '4',
},
],
}
renderer.render(vnode1, document.querySelector('#app'))
renderer.render(vnode2, document.querySelector('#app'))