虚拟DOM和key
Vue为什么需要虚拟DOM?
虚拟 DOM 在 Vue 中起到了优化性能、提供跨平台兼容性 以及简化开发流程的作⽤。
- 虚拟 DOM 可以减少直接操作实际 DOM 的次数。
- 虚拟 DOM 是⼀个抽象层,将实际 DOM 抽象为⼀个跨平台 的表示形式。使得vue 可以在不同的平台上运⾏。 Vue 会通过⽐较新旧虚拟
- DOM 树的差异(Diff算法),找出需要更新的部分进⾏更新。
Vue中key的作⽤?
对节点进⾏标识(相同),⽤于优化节点更新。key在同⼀层级的兄弟节点中必须是唯⼀的
- 在节点复⽤时,判断是否是相同节点。主要看标签和key是否相同,如果相同则可以进⾏复⽤
Vue2中Diff算法解析
Vue 2 的 diff 算法通过递归、双指针和优化策略来实现的,是同层级比较,不会涉及到跨级比对
- 同层级节点的⽐较 (⽐较节点的标签、Key 和属性)
- ⽐较⼦节点 (采⽤双指针⽅式进⾏⽐较),递归⽐较⼦节点
实现Vue2Diff算法
创建虚拟节点
虚拟节点就是一个对象来描述真实节点
h.js
// h.js
// 创建元素节点
export function createElement(tag, data = {
},...children) {
let key = data.key; // key属性
if (key) {
delete data.key
}
return vnode(tag,data,key,children)
}
// 创建文本节点
export function createTextNode(text) {
return vnode(undefined,undefined,undefined,undefined,text)
}
function vnode(tag,data,key,children,text) {
return {
// -> vnode.key // vnode.data.key 不存在
tag,data,key,children,text
}
}
生成真实节点
完整patch.js
export function patch(oldVnode, vnode) {
// 判断oldVnode是一个元素节点?
if (oldVnode.nodeType) {
// 元素
const el = createElm(vnode);
oldVnode.appendChild(el);
} else {
patchVnode(oldVnode, vnode); // 从根开始比较的
}
}
function isSameVnode(oldVnode, vnode) {
// 必须标签一样key 一样才是同一个元素
return (oldVnode.tag === vnode.tag) && (oldVnode.key === vnode.key)
}
function patchVnode(oldVnode, vnode) {
// 比较两个节点 (节点需要能复用)
if (!isSameVnode(oldVnode, vnode)) {
// 如果不是相同节点,将老dom元素直接替换成新元素即可
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}
// 走到这里说明之前和现在的节点是同一个节点, 要复用节点
const el = vnode.el = oldVnode.el
if (!oldVnode.tag) {
// 文本比较文本内容,有变化复用文本节点更新内容
if (oldVnode.text !== vnode.text) {
el.textContent = vnode.text
}
}
// 除了文本那就是元素了, 元素的话需要比较自己的属性和儿子
updateProperties(vnode, oldVnode.data); // 更新属性,需要和老的比对
// 比较双方儿子
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 双方都有儿子
if (oldChildren.length > 0 && newChildren.length > 0) {
// 比较双方的儿子
updateChildren(el, oldChildren, newChildren); // 交给此方法来更新
} else if (oldChildren.length > 0) {
el.innerHTML = '';
} else if (newChildren.length > 0) {
for (let i = 0; i < newChildren.length; i++) {
el.appendChild(createElm(newChildren[i]))
}
}
// 之前有儿子 现在没儿子 把以前的儿子删除掉
// 之前的没儿子 现在有儿子 直接将现在的儿子插入即可
}
// 给dom元素添加样式
function updateProperties(vnode, oldProps = {
}) {
const newProps = vnode.data || {
}
const el = vnode.el;
// 对于属性来说新的要直接生效 但是老的里面有的新的没有还要移除
let newStyle = newProps.style || {
}
let oldStyle = oldProps.style || {
};
for (let key in oldStyle) {
// 老的样式有,新的没有要删除dom元素的样式
if (!newStyle[key]) {
el.style[key] = ''
}
}
for (let key in oldProps) {
// 老的属性有新的没有 移除这个属性
if (!newProps[key]) {
el.removeAttribute(key)
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else {
el.setAttribute(key, newProps[key])
}
}
}
// 递归创建节点
function createElm(vnode) {
let {
tag, children, text } = vnode
// 如果标签名是字符串说明是一个元素节点
if (typeof tag === 'string') {
// createElement DOMapi
vnode.el = document.createElement(tag);
updateProperties(vnode)
children.forEach(child => vnode.el.appendChild(createElm(child)))
} else {
vnode.el = document.createTextNode(text)