总结
阿里十分注重你对源码的理解,对你所学,所用东西的理解,对项目的理解。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
可以说理解了snabbdom 中的具体方法即理解了vue中虚拟dom的核心思想:
当状态更新时,将新的JavaScript对象和旧的JavaScript对象进行比较(即diff算法),运算出两者的差异,将差异应用到真正的DOM,以此减少了DOM的操作。
diff 策略
- 只比较同一级,不跨级比较
- 标签名不同,直接删除,不继续深度比较
- 标签名相同,key相同就认为是相同节点,不继续比较
patch
patch方法是虚拟dom核心中的核心。在VNode(虚拟节点)改变后和初始化前都会调用。
主要逻辑如下:
- 调用 module 中的 pre hook(生命周期相关)
- 如果传入的是 Element 转成空的 vnode(这里在首次创建时,会传入一个dom元素)
- 新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点
- 创建新节点 ,插入新节点 ,移除旧节点
- 调用元素上的 insert hook(生命周期相关)
- 调用元素上的 post hook(生命周期相关)
源码解析:
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 调用 module 中的 pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果传入的是 Element 转成空的 vnode
if (!isVnode(oldVnode)) {
//创建一个空的vnode 并关联DOM元素 emptyNodeAt
oldVnode = emptyNodeAt(oldVnode);
}
// sameVnode 时 (sel 和 key相同) 调用 patchVnode
// 判断两个参数是否是相同的vnode
// 比较标签名 key
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
// 创建新的 dom 节点 vnode.elm
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 插入 dom
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
// 移除旧 dom
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 调用元素上的 insert hook,注意 insert hook 在 module 上不支持
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
// 调用 module post hook
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
}
function emptyNodeAt(elm: Element) {
const id = elm.id ? '#' + elm.id : '';
const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
// key 和 selector 相同
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
patchVnode
主要逻辑如下:
- 调用 module 中的 prepatch hook(生命周期相关)
- 按照新旧Vnode 的text 和children 的差异做出差异化处理
- 值得注意的是:新旧节点均存在 children,且不一样时,对 children 进行 diff 调用 updateChildren
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
// 调用 prepatch hook 生命周期钩子
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode);
}
//设置新的vnode的dom 关联
const elm = (vnode.elm = oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 调用 module 上的 update hook 生命周期钩子
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
// 调用 vnode 上的 update hook 生命周期钩子
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧节点均存在 children,且不一样时,对 children 进行 diff
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
// 旧节点不存在 children 新节点有 children
// 旧节点存在 text 置空
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 加入新的 vnode
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 新节点不存在 children 旧节点存在 children 移除旧节点的 children
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
// 旧节点存在 text 置空
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 更新 text
api.setTextContent(elm, vnode.text as string);
}
// 调用 postpatch hook 生命周期钩子
if (isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode);
}
}
核心方法updateChildren
新的Vnode 和 老的Vnode 都有children时,进入方法。
- 对新旧node分别赋值 newStartIdx oldStartIdx newEndIdx oldEndIdx 进行指针循环遍历
- 对不符合same 的节点进行劲增 其他的进行patchVnode 的替换
- 循环结束处理剩余节点 进行批量删除或批量新增
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
### 最后
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
>![](https://img-blog.csdnimg.cn/img_convert/797c30400abf31ce6ce57729f9cbcdab.webp?x-oss-process=image/format,png)
ensure removed elements stay in correct relative positions
### 最后
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
>[外链图片转存中...(img-zYGSRNqc-1715202150135)]