Vue源码之虚拟DOM和diff算法
手写一个diff算法和h函数
1. 环境搭建
1.1 为实现模块化开发,我们使用webpack构建工具
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"
1.2 webpack.config.js
const path = require("path")
module.exports = {
mode: "development",
entry: './src/index.js',
output: {
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname, "www"),
compress: false,
port: 8080,
publicPath: "/xuni/"
}
}
2. diff算法
2.1 什么是diff算法
diff算法就是最小量更新算法,那什么是最小量,在Vue中我们使用v-for指令时,我们要加key属性,如果节点名称和key不变得话,我们就不破坏原来的,哪里变了我更新哪里
2.2 diff算法原理图
diff算法比较的是虚拟dom,在Vue编译阶段的时候,在形成AST抽象语法树的时候虚拟dom就已经形成了,而我们diff算法比较的是虚拟dom
diff算法的比较策略就是同级比较,不同级不比较,就如上图
diff算法采用4指针,4命中,1循环的方式,具体如何看下面代码,手动实现diff算法
3. 入口文件和html页面
3.1 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button>点我切换</button>
<div id="diff"></div>
<script src="/xuni/bundle.js"></script>
</body>
</html>
准备一个按钮,进行diff算法比对
3.2 index.js
import h from './MySnabbdom/h'
import patch from "./MySnabbdom/patch";
let myVNode1 = h('h1', {}, [
h('h2', {key: "a"}, "a"),
h('h2', {key: "b"}, "b"),
h("h2", {key: "c"}, "c"),
h("h2", {key: "d"}, "d"),
h("h2", {key: "e"}, "e"),
h("h2", {key: "f"}, "f"),
])
let myVNode2 = h('h1', {}, [
h("h2", {key: "f"}, "f"),
h('h2', {key: "a"}, "a"),
h("h2", {key: "e"}, "e"),
h("h2", {key: "d"}, "d"),
h("h2", {key: "g"}, "g"),
h("h2", {key: "c"}, "c"),
])
const div = document.querySelector("div")
patch(div, myVNode1)
document.querySelector("button").addEventListener("click", () => {
patch(myVNode1, myVNode2)
})
准备好两个Node,再给按钮绑定事件监听
4. vNode函数
export default function vNode(sel, data, children, text, elm) {
//The function passes an argument to an object and returns it
const key = data.key
return {
sel, data, children, text, elm, key
}
}
vNode函数的功能就是将你传入的模板变成一个对象的形式,方便进行diff算法的比对
5. h函数
import vNode from "./vNode";
export default function h(sel, data, c) {
if (arguments.length !== 3) throw new Error("The h function must take three arguments")
if (typeof c === "string" || typeof c === "number") {
return vNode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
let children = []
for (let i = 0; i < c.length; i++) {
if (typeof c[i] !== "object" && !c[i].hasOwnProperty('sel'))
throw new Error("One or more of the items passed into your array are not H-functions")
children.push(c[i])
}
return vNode(sel, data, children, undefined, undefined)
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
let children = [c]
return vNode(sel, data, children, undefined, undefined)
} else {
throw new Error("The third type passed into the function is incorrect")
}
}
h函数的主要功能就是判断你传入的模板的合法性,如果不合法,主动抛出异常
6. patch函数
首先我们要搞懂diff算法的更新判定机理
import vNode from "./vNode";
import createElements from "./createElements";
import patchVNode from "./patchVNode";
export default function patch(oldVNode, newVNode) {
//Determine whether the old node is a virtual DOM or a DOM
if (oldVNode.sel === '' || oldVNode.sel === undefined) {
//Wrapped as a virtual DOM
oldVNode = vNode(oldVNode.tagName.toLowerCase(), {}, [], undefined, oldVNode)
}
//Check whether oldVNode and newVNode are the same node
if (oldVNode.key === newVNode.key && oldVNode.sel === newVNode.sel) {
//If so, refine the update
patchVNode(oldVNode, newVNode)
} else {
//If not, take it apart violently and renew it
let newVNodeDom = createElements(newVNode)
if (oldVNode.elm && newVNodeDom) {
oldVNode.elm.parentNode.insertBefore(newVNodeDom, oldVNode.elm)
}
//Deleting an Old Node
oldVNode.elm.parentNode.removeChild(oldVNode.elm)
}
}
7. createElements函数
export default function createElements(vNode) {
//This function creates the actual node
let domNode = document.createElement(vNode.sel)
//Determine whether there are children or text
if (vNode.text !== "" && vNode.children === undefined || vNode.children.length === 0) {
domNode.innerHTML = vNode.text
} else if (Array.isArray(vNode.children) && vNode.children.length > 0) {
//Internal child nodes to recurse, create child nodes
vNode.children.forEach(object => domNode.appendChild(createElements(object)))
}
vNode.elm = domNode
return vNode.elm
}
该函数的主要功能是将传入的虚拟DOM转化为真实DOM
8. patchVNode函数
import createElements from "./createElements";
import updateChildren from "./updateChildren";
export default function patchVNode(oldVNode, newVNode) {
if (oldVNode === newVNode) return
if (newVNode.hasOwnProperty("text") && newVNode.children === undefined || newVNode.children.length === 0) {
//The new node has no text property
if (newVNode.text !== oldVNode.text) oldVNode.elm.innerText = newVNode.text
} else {
if (oldVNode.children !== undefined && oldVNode.children.length > 0) {
updateChildren(oldVNode.elm, oldVNode.children, newVNode.children)
} else {
oldVNode.elm.innerHTML = ""
newVNode.children.forEach(object => oldVNode.elm.appendChild(createElements(object)))
}
}
}
该函数的作用是除最后一个diff细致化的更新,在diff更新机理上的所有内容完成
9. updateChildren函数
该函数是diff算法最强和最核心的功能实现,是在children属性上的变化的侦测
在次Vue的设计是设置4个指针,分别指向着旧DOM的头,旧DOM的尾,新DOM的头和新DOM的尾,来进行比较
那么这4个指针设计到4个命中,还有设计这4个命中的顺序
- 新前与旧前 ---------- 如果新前与旧前相同,也就是命中,则新前和旧前指针下移
- 新后与旧后 ---------- 如果新前与旧前没有比对成功,就进行新后与旧后的比较,如果命中成功,新后与旧后指针上移
- 新后与旧前 ---------- 如果前两种没有命中成功,就比较新后与旧前,如果命中,则新前指向的节点,移动到旧后之后,并且,新后指针上移,旧前指针下移
- 新前与旧后 ---------- 如果前三种都没命中,就匹配这种,如果命中,则新前指向的节点,移动到旧前之前
- 如果前4种都没命中,就循环,映射来查找,最后找到映射到的结果移动到oldStartLdx之前
import patchVNode from "./patchVNode";
import createElements from "./createElements";
//Determine whether it is a node
function checkSameVNode(a, b) {
return a.sel === b.sel && a.key === b.key
}
export default function updateChildren(parentElm, oldCh, newCh) {
//Old before
let oldStartIdx = 0
//Before the new
let newStartIdx = 0
//After the old
let oldEndIdx = oldCh.length - 1
//After the new
let newEndIdx = newCh.length - 1
//Before the old node
let oldStartVNode = oldCh[0]
//After the old node
let oldEndVNode = oldCh[oldEndIdx]
//Before the new node
let newStartVNode = newCh[0]
//After the new node
let newEndVNode = newCh[newEndIdx]
let keyMap = null
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
switch (undefined) {
case oldStartVNode || oldCh[oldStartIdx]:
oldStartVNode = oldCh[++oldStartIdx]
break
case oldEndVNode || oldCh[oldEndIdx]:
oldEndVNode = oldCh[--oldEndIdx]
break
case newStartVNode || newCh[newStartIdx]:
newStartVNode = newCh[++newStartIdx]
break
case newEndVNode || newCh[newEndIdx]:
newEndVNode = newCh[--newEndIdx]
break
default:
//Before the new and before the old hit
if (checkSameVNode(oldStartVNode, newStartVNode)) {
patchVNode(oldStartVNode, newStartVNode)
oldStartVNode = oldCh[++oldStartIdx]
newStartVNode = newCh[++newStartIdx]
}
//The new back hits the old back
else if (checkSameVNode(oldEndVNode, newEndVNode)) {
patchVNode(oldEndVNode, newEndVNode)
oldEndVNode = oldCh[--oldEndIdx]
newEndVNode = newCh[--newEndIdx]
}
//The new post hits the old front
else if (checkSameVNode(oldStartVNode, newEndVNode)) {
patchVNode(oldStartVNode, newEndVNode)
parentElm.insertBefore(oldStartVNode.elm, oldEndVNode.elm.nextSibling)
oldStartVNode = oldCh[++oldStartIdx]
newEndVNode = newCh[--newEndIdx]
}
//New before and old after hit
else if (checkSameVNode(oldEndVNode, newStartIdx)) {
patchVNode(oldEndVNode, newStartVNode)
parentElm.insertBefore(oldEndVNode.elm, oldStartVNode.elm)
oldEndVNode = oldCh[--oldEndIdx]
newStartIdx = newCh[++newStartIdx]
}
//None of them hit
else {
if (!keyMap) {
keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key
if (key !== undefined) {
keyMap[key] = i
}
}
}
const idxInOld = keyMap[newStartVNode.key]
if (idxInOld === undefined) {
parentElm.insertBefore(createElements(newStartVNode), oldStartVNode.elm)
} else {
const elmToMove = oldCh[idxInOld]
patchVNode(elmToMove, newStartVNode)
oldCh[idxInOld] = undefined
parentElm.insertBefore(elmToMove.elm, oldStartVNode.elm)
}
newStartVNode = newCh[++newStartIdx]
}
}
}
//After the loop ends, start is still smaller than old
if (newStartIdx <= newEndIdx) {
const before = newCh[newEndIdx + 1] === undefined ? null : newCh[newEndIdx + 1].elm
for (let i = newStartIdx; i <= newEndIdx; i++) {
parentElm.insertBefore(createElements(newCh[i]), before)
}
} else if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm)
}
}
}
}
同时如果命中成功了,也要进行递归调用patchVNode函数,这样就能成功的解决children的嵌套
10. 最后
代码已上传至github
https://github.com/Bald-heads/DomAndDiff.git