Vue.js设计与实现-双端Diff算法
发表于:2024-12-10
字数统计:3059 字
预计阅读11分钟
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
function createRenderer(options) {
const {
createElement,
setElementText,
insert,
patchProps,
createText,
createComment,
setText,
} = options;
function render(vnode, container) {
if (vnode) {
// 新node存在,将其与旧node一起传递给patch函数,进行打补丁
patch(container._vnode, vnode, container);
} else {
if (container._vnode) {
// 旧node存在,且新node不存在,说明是卸载(unmount)操作
// 只需要将container中的DOM清空即可
// container.innerHTML = "";
// 8.5使用innerHTML直接清空是不严谨的,应该对每个DOM元素按照卸载流程清空
// 调用unmount函数卸载vnode
unmount(container._vnode);
}
}
// 把vnode存储到container._vnode下面,就是此次渲染结束,新node应该叫做旧node了
container._vnode = vnode;
}
function hydrate() {
// do sth...
}
function patch(n1, n2, container, anchor = null) {
// doing sth...
// 如果n1存在,则对比n1和n2的类型
console.log('update')
if (n1 && n1.type !== n2.type) {
// 如果新旧vnode的类型不同,直接将旧vnode卸载
unmount(n1);
n1 = null;
}
// 代码运行到这里,证明n1和n2所描述的内容相同
const { type } = n2;
// 如果n2.type的值是字符串类型,则它描述的是普通标签元素
if (typeof type === "string") {
if (!n1) {
// 挂载
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 {
// 旧node存在,替换
const el = (n2.el = n1.el);
if (n2.children !== n1.children) {
setText(el, n2.children);
}
}
} else if (type === Comment) {
// 文本节点
if (!n1) {
// 如果没有就节点,那么挂载
const el = (n2.el = createComment(n2.children));
// 插入
insert(el, container);
} else {
// 旧node存在,替换
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, container)
}
}
else if (typeof type === "object") {
// 如果n2.type的值的类型是对象,则它描述的是组件
} else if (type === "xxx") {
// 处理其他类型的vnode
}
}
function mountElement(vnode, container, anchor = null) {
// 创建DOM元素
// 让vnode.el引用真实DOM元素,便于后续卸载操作
const el = (vnode.el = createElement(vnode.type));
if (typeof vnode.children === "string") {
// 如果children是字符串,就直接放进去
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
// 如果children是数组,则遍历每一个子节点,并调用patch函数挂载他们
vnode.children.forEach((child) => {
patch(null, child, el);
});
}
// 如果vnode.props存在才处理它
if (vnode.props) {
// 遍历
for (const key in vnode.props) {
const value = vnode.props[key];
// 使用shouldSetAsProps函数判断是否应该作为DOM Properties代理
// 直接设置,是el[key]就行了,浏览器代理到DOM Properties上
// 抽离了依赖浏览器API的代码,封装到配置项中
patchProps(el, key, null, value);
}
}
insert(el, container, anchor);
}
function unmount(vnode) {
// 如果要卸载的是fragment,那么卸载它的children
if (vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
}
// 根据_vnode获取要卸载的真实DOM元素
const el = vnode.el;
// 获取el的父元素
const parent = el.parentNode;
parent && parent.removeChild(el);
}
function patchChildren(n1, n2, container) {
// 判断新的子节点的类型是否是文本节点
// 文本子节点
if (typeof n2.children === "string") {
// 旧子节点的类型有三种可能,没有子节点。文本子节点和一组子节点
// 为一组子节点时,逐个卸载
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => unmount(child));
}
// 其他情况什么也不用干,直接替换就行
setElementText(container, n2.children);
} else if (Array.isArray(n2.children)) {
// 说明新子节点时一组子节点
// 封装patchKeyedChildren函数处理两组子节点
patchKeyedChildren(n1, n2, container)
} else {
// 代码运行到这里,说明新子节点不存在
// 旧子节点是一组子节点,只需逐个卸载
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c));
} else if (typeof n1.children === "string") {
// 旧子节点是文本子节点,清空内容
setElementText(container, "");
}
// 如果没有旧子节点,啥也不干
}
}
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children
const newChildren = n2.children
// 四个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 四个索引指向的vnode节点
let oldStartVnode = oldChildren[oldStartIdx]
let oldEndVnode = oldChildren[oldEndIdx]
let newStartVnode = newChildren[newStartIdx]
let newEndVnode = newChildren[newEndIdx]
// 双端Diff算法逻辑
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 增加两个判断分支,如果头尾部节点为undefined说明该节点已经被处理,直接跳到下一个位置
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)
insert(oldStartVnode.el, container, oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIdx]
newEndVnode = newChildren[--newEndIdx]
} else if (oldEndVnode.key === newStartVnode.key) {
// 第四步,旧子节点的末个和新子节点的首个
// 仍需要调用patch函数打补丁(更新)
patch(oldEndVnode, newStartVnode, container)
// 移动DOM操作
// oldEndVnode.el移动到oldStartVnode.el前面
insert(oldEndVnode.el, container, oldStartVnode.el)
// 移动DOM完成后,更新索引值,并指向下一个位置
// 双端Diff核心特征:内缩
oldEndVnode = oldChildren[--oldEndIdx]
newStartVnode = newChildren[++newStartIdx]
} else {
// 非理想情况下,四次均不会命中
// 试图寻找newStartVnode相同的key值的节点
const idxInOld = oldChildren.findIndex(vnode => vnode.key === newStartVnode.key)
// 如果有idxInOld,说明对于newStartVnode节点,有可以复用的旧节点
if (idxInOld > 0) {
// idxInOld的位置对应的就是vnode需要移动的节点
const vnodeToMove = oldChildren[idxInOld]
// 打补丁
patch(vnodeToMove, newStartVnode, container)
// 将vnodeToMove.el移动到头部节点oldStart.el之前,此使用后者作为描点
insert(vnodeToMove.el, container, oldStartVnode.el)
// 由于位置idxInOld处的节点所对应的真实DOM已经移动到了别处,设置为undefined
oldChildren[idxInOld] = undefined
// 最后更新newStartIdx到下一个位置
} else {
// 如果没有idxInOld,就是说newStartVnode在old中不存在,那么是新节点,需要挂载
// 挂载到头部(就是当前),使用当前头部节点oldStartVnode.el作为描点
patch(null, newStartVnode, container, oldStartVnode.el)
}
newStartVnode = newChildren[++newStartIdx]
}
}
// 循环结束后,可能会遗留未挂载的新节点
// oldEndIdx < oldStartIdx是循环退出了的条件
// newStartIdx <= 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])
}
}
}
function patchElement(n1, n2) {
// 在上面,已经判断了n1和n2是类型相同的节点,只是要更新而已
// n1.type === n2.type,并且由于他们是相同的节点,所以n1.el === n2.el (比如说div)
const el = (n2.el = n1.el);
const oldProps = n1.props;
const newProps = n2.props;
// 更新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);
}
return {
render,
hydrate,
};
}
function normalizeClass(originClass) {
if (typeof originClass === "string") {
return originClass;
} else if (
typeof originClass === "object" &&
originClass !== null &&
!Array.isArray(originClass)
) {
return extractKeys(originClass);
} else if (Array.isArray(originClass)) {
return originClass.map((item) => [normalizeClass(item)]).join(" ");
} else {
return "";
}
function extractKeys(originClass) {
return Object.keys(originClass)
.filter((key) => originClass[key])
.join(" ");
}
}
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);
},
createComment(comment) {
return document.createComment(comment);
},
setText(el, text) {
el.nodeValue = text;
},
patchProps(el, key, prevValue, nextValue) {
// 匹配以on开头的属性,认为是事件
if (/^on/.test(key)) {
// 获取为该元素伪造的事件处理函数invoker
// 定义el._vei是一个对象
let invokers = el._vei || (el._vei = {});
// 根据事件名称获取invoker
let invoker = invokers[key];
const name = key.slice(2).toLowerCase();
if (nextValue) {
if (!invoker) {
// 如果没有,就创建一个
// vei是vue_event_invoker缩写
invoker = el._vei[key] = (e) => {
// 这个e是pointEvent
// e.timeStamp是事件发生的时间
// 屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行
if (e.timeStamp < invoker.attached) {
return;
}
// 当伪造的事件处理函数执行时,会执行真正的事件
// 如果invoke.value是数组,则遍历它并诸葛调用事件处理函数
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e));
} else {
invoker.value(e);
}
};
// 将真正的事件处理函数赋值给invoker.value
invoker.value = nextValue;
// 添加attached属性,存储事件处理函数被绑定的事件
invoker.attached = performance.now();
// 绑定invoker作为事件处理函数
el.addEventListener(name, invoker);
} else {
// 如果invoker存在,意味着更新,并且只需要更新invoker的值
invoker.value = nextValue;
}
} else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的invoker存在,则移除绑定
el.removeEventListener(name, invoker);
}
}
// class是比较多的,刚好发现使用className设置性能优化较大
// 对class进行特殊处理
else if (key === "class") {
el.className = nextValue || "";
} else if (shouldSetAsProps(el, key, nextValue)) {
// 获取该DOM Properties的类型
const type = typeof el[key];
// 如果是布尔类型,并且value是空字符串,则将值矫正为true
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
// 如果要设置的属性没有对应的DOM Properties,则使用setAttribute设置属性
el.setAttribute(key, nextValue);
}
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === "form" && el.tagName === "INPUT") {
return false;
}
return key in el;
}
},
});
// 执行区
const Fragment = Symbol()
const Text = Symbol()
const Comment = Symbol()
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"));
}, 1500);
</script>
</html>