<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue2的双端 Diff算法</title>
<style>
.bgColor {
background-color: deeppink;
}
.fontColor {
color: #fff;
font-size: 24px;
}
p {
width: 400px;
padding: 5px 0;
font-size: 24px;
font-weight: bold;
color: blue;
text-align: center;
border: 1px solid #eee;
margin-top: 10px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
/**
* 定义文本节点和注释节点
*/
const Text = Symbol();
const Comment = Symbol();
const Fragment = Symbol();
const newVnode1 = {
type: Text,
children: "我是文本内容 ",
};
const newVnode2 = {
type: Comment,
children: "我是注释内容",
};
const fragmentNode = {
type: "ul",
children: [
{
type: Fragment,
children: [
{ type: "li", children: "item 1" },
{ type: "li", children: "item 2" },
{ type: "li", children: "item 3" },
],
},
],
};
/**
* 自定义渲染器
*/
function createRenderer(options) {
const {
createElement,
insert,
setElementText,
patchProps,
createText,
setText,
} = options;
/**
* 卸载
*/
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) {
if (vnode) {
patch(container._vnode, vnode, container);
} else {
// 删除旧节点
if (container._vnode) {
unmount(container._vnode);
}
}
container._vnode = vnode;
}
/**
* 打补丁
*/
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
// console.log(type);
if (typeof type === "string") {
if (!n1) {
mountElement(n2, container, anchor);
} else {
patchElement(n1, n2);
}
} else if (type === Text) {
// console.log("文本节点");
// 文本节点
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) {
// 将Fragment的 children逐个挂载
n2.children.forEach((c) => patch(null, c, container));
} else {
patchChildren(n1, n2, container);
}
}
}
/**
* 更新子节点
*/
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);
}
}
patchChildren(n1, n2, el);
}
/**
* 抽离封装核心 Diff算法
*/
function diff(n1, n2, container) {
console.log("此处是核心的 Diff算法");
console.log(n1);
console.log(n2);
const oldChildren = n1.children;
const newChildren = n2.children;
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
let oldStartVNode = oldChildren[oldStartIdx];
let oldEndVNode = oldChildren[oldEndIdx];
let newStartVNode = newChildren[newStartIdx];
let newEndVNode = newChildren[newEndIdx];
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVNode) {
oldStartVNode = oldChildren[++oldStartIdx];
} else if (!oldEndVNode) {
oldEndVNode = oldChildren[--oldEndIdx];
} else if (oldStartVNode.key === newStartVNode.key) {
// 打补丁
patch(oldStartVNode, newStartVNode, container);
oldStartVNode = oldChildren[++oldStartIdx];
newStartVNode = newChildren[++newStartIdx];
} else if (oldEndVNode.key === newEndVNode.key) {
// 打补丁
patch(oldEndVNode, newEndVNode, container);
// 更新索引值
oldEndVNode = oldChildren[--oldEndIdx];
newEndVNode = newChildren[--newEndIdx];
} else if (oldStartVNode.key === newEndVNode.key) {
// 打补丁
patch(oldStartVNode, newEndVNode, container);
// 移动DOM节点
insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling);
// 更新索引值
oldStartVNode = oldChildren[++oldStartIdx];
newEndVNode = newChildren[--newEndIdx];
} else if (oldEndVNode.key === newStartVNode.key) {
// 打补丁
patch(oldEndVNode, newStartVNode, container);
// 移动 DOM 操作
insert(oldEndVNode.el, container, oldStartVNode.el);
// 更新索引值
oldEndVNode = oldChildren[--oldEndIdx];
newStartVNode = newChildren[++newStartIdx];
} else {
// 非理想状况的处理方式
const idxInOld = oldChildren.findIndex(
(node) => node.key === newStartVNode.key
);
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld];
// console.log("vnodeToMove", vnodeToMove);
patch(vnodeToMove, newStartVNode, container);
insert(vnodeToMove.el, container, oldStartVNode.el);
oldChildren[idxInOld] = undefined;
} else {
// 添加新元素
patch(null, newStartVNode, container, oldStartVNode.el)
}
newStartVNode = newChildren[++newStartIdx];
}
}
// 循环结束后检查索引值的情况
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
// 添加新元素
for(let i = newStartIdx; i <= newEndIdx; i++) {
patch(null, newChildren[i], container, oldStartVNode.el)
}
} else if(newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
// 移除不存在的节点
for(let i = oldStartIdx; i <= oldEndIdx; i++) {
unmount(oldChildren[i])
}
}
}
/**
* 更新children
*/
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)) {
// 此处是核心的 Diff算法
diff(n1, n2, container);
} 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, "");
}
}
}
// 主要改动这里
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) {
const value = vnode.props[key];
patchProps(el, key, null, value);
}
}
insert(el, container, anchor);
}
function shouldSetAsProps(el, key, value) {
if (key === "form" && el.tagName === "INPUT") {
return false;
}
return key in el;
}
return { render };
}
/**
* 公共函数
*/
function shouldSetAsProps(el, key, value) {
if (key === "form" && el.tagName === "INPUT") {
console.log("el", el);
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)) {
// const name = key.slice(2).toLowerCase();
// prevValue && el.removeEventListener(name, prevValue)
// el.addEventListener(name, nextValue);
const invokers = el._vei || (el._vei = {});
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if (nextValue) {
if (!invoker) {
// 绑定一个伪造的事件处理函数invoker
invoker = el._vei[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.value = nextValue;
// 绑定事件的时间
invoker.attached = performance.now();
el.addEventListener(name, invoker);
} else {
invoker.value = nextValue;
}
} else {
el.removeEventListener(name, invoker);
}
} else if (key === "class") {
el.className = nextValue || "";
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
},
});
// 虚拟节点对象
const oldVnode = {
type: "div",
children: [
{ type: "p", children: "1", key: 1 },
{ type: "p", children: "2", key: 2 },
{ type: "p", children: "3", key: 3 },
],
};
const newVnode = {
type: "div",
children: [
{ type: "p", children: "1", key: 1 },
{ type: "p", children: "3", key: 3 },
],
};
renderer.render(oldVnode, document.querySelector("#app"));
setTimeout(() => {
renderer.render(newVnode, document.querySelector("#app"));
}, 2000);
</script>
</body>
</html>