概述
本文旨在学习vue2 Diff算法,可能有的说法没有理解清楚,如果有疑问,可以提出,或者前往官网或者自行查找源码
html页面,引入index.js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
传入index.js
<script type="module" src="index.js"></script>
</head>
<body>
<div>vue2 Diff</div>
<div id="container">
<h1> 这是container</h1>
</div>
<button id="btn">按钮</button>
</body>
</html>
index.js页面,创建新旧虚拟节点
通过h函数创建vnode1、vnode2、vnode3节点 这里先vnode1转为vnode2,通过按钮将vnode2转为vnode3
import h from "./h.js";
import patch from './patch.js'
// 获取到真的dom节点
let container = document.getElementById('container')
// 虚拟dom节点
let vnode1 = h('div', {}, '你好呀')
let vnode2 = h('ul', {}, [
h('li', {key:'a'}, 'a'),
h('li', {key:'b'}, 'b'),
h('li', {key:'c'}, 'c'),
// h('li', {key:'d'}, 'd'),
// h('li', {key:'e'}, 'e'),
])
// console.log(vnode2);
patch(container, vnode2)
let vnode3 = h('ul', {}, [
h('li', {key:'c'}, 'c'),
h('li', {key:'b'}, 'b'),
h('li', {key:'e'}, 'e'),
h('li', {key:'a'}, 'a'),
// h('li', {key:'d'}, 'd'),
])
let btn = document.getElementById('btn')
btn.onclick = () => {
console.log('11');
patch(vnode2, vnode3)
}
h.js 根据节点是否有子节点,创建虚拟节点
import vnode from "./vnode.js";
export default function (sel, data, params) {
// string 没有子元素
if (typeof params == 'string') {
return vnode(sel, data, undefined, params, undefined)
} else if(Array.isArray(params)){
// 数组则遍历成children传递给第三个参数
// console.log(params)
let children = []
for (const item of params) {
children.push(item)
}
return vnode(sel, data, children, undefined, undefined)
}
}
vnode.js 虚拟dom函数
返回一个带有key值的虚拟dom节点
export default function (sel, data, children, text, elm) {
let key = data.key
return {
sel, data, children, text, elm, key
}
}
patch.js
- 先判断oldVnode是否是真实dom,如果是真实dom(没有sel),转为虚拟dom,diff算法其实就是将两个虚拟dom进行对比,对比有差异,再在真实dom上进行修改
- 判断两个虚拟dom的元素节点是否一样,如果不一样,则通过createElement根据新虚拟节点内容,创建新的节点,并插入到旧虚拟节点前面,最后移除旧的虚拟节点元素
- 如果一样,则通过patchVnode方法进行节点内容的对比
import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnode from './patchVnode.js';
// 旧虚拟节点 oldVnode
// 新虚拟节点 newVnode
export default function (oldVnode, newVnode) {
// console.log(oldVnode);
// console.log(oldVnode.sel);
// console.log(newVnode.sel);
// 如果oldVnode没有sel 就是非虚拟节点 就让它变成虚拟节点
if (oldVnode.sel == undefined) {
oldVnode = vnode(
oldVnode.tagName.toLowerCase(),
{}, //data,
[], //children,
undefined,//text,
oldVnode
)
}
// console.log(oldVnode);
// 元素节点是否相同
if (oldVnode.sel !== newVnode.sel) {
// console.log('不同元素');
// 暴力删除旧节点,插入新节点
let newVnodeElm = createElement(newVnode)
// console.log(newVnodeElm);
// 获取旧的虚拟节点的 .elm就是真的节点
let oldVnodeElm = oldVnode.elm
// 创建新的节点
if (newVnodeElm) {
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
}
// 删除旧节点
oldVnodeElm.parentNode.removeChild(oldVnodeElm)
} else {
// console.log('相同元素');
patchVnode(oldVnode, newVnode)
}
}
creatElement.js 创建新的节点
// vnode为新节点,就是要创建的节点
export default function createElement(vnode) {
// 创建dom节点
let domNode = document.createElement(vnode.sel)
// 判断有没有子节点
if (vnode.children == undefined) {
// console.log('// 没有子节点');/
domNode.innerText = vnode.text
} else if (Array.isArray(vnode.children)) {
// console.log('// 有子节点,需要递归创建节点');
for (const child of vnode.children) {
let childDom = createElement(child)
domNode.appendChild(childDom)
}
}
// 补充elm属性 elm为真实节点 , 虚拟节点的属性值
vnode.elm = domNode
return domNode
}
patchVnode.js 判断子节点内容
- 新虚拟节点没有子节点,新虚拟节点覆盖旧虚拟节点
- 新虚拟节点有子节点,旧虚拟节点没有子节点,旧虚拟节点清空,新虚拟节点循环添加到旧虚拟节点下
- 新虚拟节点和旧虚拟节点都有子节点,通过updateChildren进一步判断子节点内容
import createElement from "./createElement.js";
import updatChildren from './updatChildren.js'
export default function (oldVnode, newVnode) {
// console.log(oldVnode.children);
// console.log(newVnode.children);
if (newVnode.children == undefined) {
// console.log('新节点没有children,直接覆盖旧的');
if (newVnode.text !== oldVnode.text) {
// console.log('文本不同');
oldVnode.elm.innerText = newVnode.text
}
} else {
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// console.log('新有,旧有',oldVnode.elm, oldVnode.children, newVnode.children);
updatChildren(oldVnode.elm, oldVnode.children, newVnode.children)
} else {
// console.log('新有,旧无');
// 将旧节点清空 可能有文本节点
oldVnode.elm.innerHTML = ''
// 遍历新的子节点添加到旧节点
for (const child of newVnode.children) {
let childDom = createElement(child)
oldVnode.elm.appendChild(childDom)
}
}
}
}
updateChildren.js, 对比新旧虚拟节点的子节点更新虚拟dom
- 根据双端对比法
- 通过sameVnode判断两旧前和新前节点是否一致,如果一致,patchVnode更新旧虚拟节点内容,
- 通过sameVnode判断两旧后和新后节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,
- 通过sameVnode判断两旧前和新后节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,将旧前节点插入到旧后节点后面
- 通过sameVnode判断两旧后和新前节点是否一致,如果一致,然后patchVnode更新旧虚拟节点内容,将旧后节点插入到旧前节点前面
- 双端对比法查询之后,没有循环结束则通过哈希表存储旧节点的值,然后在哈希表里找新节点内容,如果一致,则将旧节点插入到旧节点的顶部,并设置为undefined,如果没找到,旧创建新的节点放在旧节点底部
- 循环结束之后,如果oldStartIdx > oldEndIdx,则表示新的虚拟节点子节点比旧的虚拟节点的子节点多,则旧的虚拟节点需要新增相应的节点到尾部,反之,表示新的虚拟节点比旧的虚拟节点少,则旧的虚拟节点删除相应的节点
import patchVnode from "./patchVnode.js"
import createElement from "./createElement.js"
// 判断两个虚拟节点是否为同一个节点
function sameVnode(vNode1, vNode2) {
return vNode1.key == vNode2.key
}
// 真实dom节点 , 旧的虚拟节点 新的虚拟节点
export default function name(parentElm, oldCh, newCh) {
// console.log(parentElm,oldCh,newCh);
let oldStartIdx = 0
let oldEndIdx = oldCh.length - 1
let newStartIdx = 0
let newEndIdx = newCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == undefined) {
oldEndVnode = oldCh[--oldStartIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
console.log(1);
// 旧前和新前
patchVnode(oldStartVnode, newStartVnode)
// 看看有什么问题
if (newStartVnode) newStartVnode.elm = oldStartVnode?.elm
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
console.log(2);
// 旧后和新后
patchVnode(oldEndVnode, newEndVnode)
if (newEndVnode) newEndVnode.elm = oldEndVnode?.elm
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
console.log(3);
// 旧前和新后
patchVnode(oldStartVnode, newEndVnode)
if (newEndVnode) newEndVnode.elm = oldStartVnode?.elm
// 把旧前指定的节点移动到旧后指向的节点后面
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
console.log(4);
// 旧后和新前
patchVnode(oldEndVnode, newStartVnode)
if (newStartVnode) newStartVnode.elm = oldEndVnode?.elm
// 把旧后指定的节点移动到旧前指向的节点后面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 其他情况 查找
console.log(5);
// 创建对象 存放虚拟节点 判断新旧有没有相同节点
const keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i]?.key
if (key) keyMap[key] = i
}
// 在旧节点中查找匹配新前节点
let idxInOld = keyMap[newStartVnode.key]
// 如果匹配到节点 该节点在新旧虚拟节点中都存在
if (idxInOld) {
const elmMove = oldCh[idxInOld]
patchVnode(elmMove, newStartVnode)
// 处理过的节点 在旧虚拟节点的数组中,设置为undefined
oldCh[idxInOld] = undefined
parentElm.insertBefore(elmMove.elm, oldStartVnode.elm)
} else {
// 创建节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
}
newStartVnode = newCh[++newStartIdx]
}
}
// 结束循环 新增和删除
//oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
if (oldStartIdx > oldEndIdx) {
// 新增
const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
for (let i = newStartIdx; i <= newEndIdx; i++) {
parentElm.insertBefore(createElement(newCh[i]), before)
}
} else {
// 删除
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldCh[i].elm)
}
}
}