一、Vue为什么需要采用虚拟DOM?
虚拟 DOM 在 Vue 中起到了优化性能、提供跨平台兼容性
以及简化开发流程的作⽤。
- 虚拟 DOM 可以减少直接操作实际 DOM 的次数。
- 虚拟 DOM 是⼀个抽象层,将实际 DOM 抽象为⼀个跨平台
的表示形式。使得vue 可以在不同的平台上运⾏。 - Vue 会通过⽐较新旧虚拟 DOM 树的差异(Diff算法),找
出需要更新的部分进⾏更新。
二、Vue中key的作⽤?
在 Vue 中,key 是用于识别 Vue 中的列表(例如使用 v-for 指令)中每个子节点的特殊属性。key 的作用主要有两个方面:
-
用于 Vue 的列表渲染时的性能优化:
当 Vue 用 v-for 指令渲染列表时,它会尽可能地复用已经存在的元素,而不是重新渲染所有元素。Vue 会尽量高效地更新 DOM,以确保与虚拟 DOM 中的数据一致。 -
当列表中的元素没有 key 时,Vue 会采用就地更新策略(in-place patch),也就是会尽量复用已有的 DOM 元素。但是当列表项的顺序发生变化时,或者有动态的增减操作时,Vue 可能无法正确识别哪个元素对应哪个数据项,导致错误的渲染结果。
-
而当列表中的元素有 key 时,Vue 会基于 key 的变化重新排序和更新元素,这样可以确保列表的变化能够正确地映射到数据的变化上,避免出现意外的渲染结果。
-
用于确保组件状态的完整性:
在某些情况下,如果同一组件在不同的渲染中,存在相同的 key,Vue 可能会复用该组件的状态。这在一些特定场景下可能会导致状态混乱。因此,给组件设置唯一的 key 可以确保组件状态的完整性,每个组件都是独立的,不会被复用之前的状态。
因此,key 在 Vue 中是一个非常重要的属性,它能够确保列表渲染的正确性和性能优化,以及确保组件状态的完整性。
三、Vue2中diff算法的实现原理
Vue.js 2.x 中的 Virtual DOM diff 算法的实现原理主要依赖于 Snabbdom 这个虚拟 DOM 库。Snabbdom 是一个非常轻量级且高效的虚拟 DOM 库,Vue.js 在其基础上进行了适当的改进和定制以满足自身的需求。
下面是 Vue.js 2.x 中 Virtual DOM diff 算法的简要实现原理:
-
虚拟 DOM 的生成:首先,Vue.js 会根据模板或者 render 函数生成当前状态下的虚拟 DOM 树。
-
新旧虚拟 DOM 树的对比:然后,当状态发生变化时,Vue.js 会生成一个新的虚拟 DOM 树。接着,Vue.js 使用 diff 算法比较新旧虚拟 DOM 树的差异。
-
差异的标记:在比较过程中,如果发现节点类型相同但是内容不同,那么就会更新该节点的内容;如果节点类型不同,直接将旧节点替换为新节点;如果节点位置发生变化,那么就会将节点移动到新的位置,而不是销毁并重新创建。
-
差异的应用:最后,Vue.js 根据这些差异使用最小的操作数来更新真实 DOM。这样可以最大程度地减少真实 DOM 操作,提高渲染效率。
总体来说,Vue.js 2.x 中的 Virtual DOM diff 算法主要通过创建新旧虚拟 DOM 树的比较,并根据差异进行最小化的更新来实现高效的页面更新。这种方式能够尽量减少对真实 DOM 的操作,从而提升页面渲染的性能。
四、项目的搭建
第一步:配置package.json
//新建一个文件夹为vue2-diff,在对应的文件夹中执行下面的命令
npm init -y
第二步:新建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createElement, createTextNode } from './h.js'
import { patch } from './patch.js'
const vnode1 = createElement(
'div',
{ style: { color: 'red',background:'purple' }, key: 'a' },
createElement('li',{key:'a'},createTextNode('a')),
createElement('li',{key:'b'},createTextNode('b')),
createElement('li',{key:'c'},createTextNode('c')),
createElement('li',{key:'d'},createTextNode('d'))
);
// 虚拟节点就是一个对象来描述我们真实的节点
patch(app, vnode1); // 初次渲染
const vnode2 = createElement(
'div',
{ style: { color: 'blue' }, key: 'a' },
createElement('li',{key:'b'},createTextNode('b')),
createElement('li',{key:'m'},createTextNode('m')),
createElement('li',{key:'a'},createTextNode('a')),
createElement('li',{key:'c'},createTextNode('c')),
createElement('li',{key:'q'},createTextNode('q')),
);
setTimeout(()=>{
patch(vnode1,vnode2); // 用vnode2 和 vnode1 做diff 更新vnode1上的元素
},1000)
</script>
</body>
</html>
第三部:新建一个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)
}
return vnode.el
}
function updateChildren(el, oldChildren, newChildren) {
// 对dom操作的常见优化
// 给你一个列表 增加一个 删除一个 倒序 反序
// 双端比对
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex]
function makeIndexByKey(children) {
let map = {};
children.forEach((child, index) => {
map[child.key] = index; // 老的key 和索引的映射表
})
return map;
}
const map = makeIndexByKey(oldChildren)
// 一直比较直到一方指针重合就停止
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 如果头指针指向的结点是同一个节点,要复用这个节点
if (!oldStartVnode) { // 比对的时候跳过空节点
oldStartVnode = oldChildren[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex];
} else if (isSameVnode(oldStartVnode, newStartVnode)) { // 从头往后比
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex]
} else if (isSameVnode(oldEndVnode, newEndVnode)) { // 从尾往前比
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 尾部和头部比较
patchVnode(oldEndVnode, newStartVnode); // 递归比较
el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 把尾部移动到头部
oldEndVnode = oldChildren[--oldEndIndex]; // 老的往前移动
newStartVnode = newChildren[++newStartIndex]; // 新的往后移动
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 把尾部移动到头部
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
}
// 优化diff算法, 通过dom常见操作优化出来的
else {
// 用新的节点去老的里面找,如果找的到则移动复用
// 如果找不到则创建插入,
// 如果新的都判断完了,老的中多的就删除即可
let moveIndex = map[newStartVnode.key]; // 用新的节点去老的里面找索引
if (moveIndex == undefined) { // null == undefiend
el.insertBefore(createElm(newStartVnode), oldStartVnode.el); // 老的中没有
} else {
let moveVnode = oldChildren[moveIndex]; // 找到要移动的节点
el.insertBefore(moveVnode.el, oldStartVnode.el); // 将节点移动到头指针的前面
oldChildren[moveIndex] = null;
patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
}
newStartVnode = newChildren[++newStartIndex]
}
}
console.log(oldStartIndex,oldEndIndex)
if (oldStartIndex <= oldEndIndex) { // 老的对于的要删除掉
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i]
if (child) {
el.removeChild(child.el)
}
}
}
if (newStartIndex <= newEndIndex) { // 新的比老的多
for (let i = newStartIndex; i <= newEndIndex; i++) {
let ele = newChildren[i]
let anchor = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el
el.insertBefore(createElm(ele), anchor);
// el.insertBefore(createElm(ele),null) === el.appendChild(createElm(ele))
}
}
// newStartIndex >= newEndIndex
}
// 初次渲染
// 比对的核心是从patch开始的 patch(真实的容器,虚拟节点)
// - 根据虚拟节点创建成真实节点插入到容器中 (创建真实节点采用的是createElm)
// - 根据虚拟节点属性创建真实的属性updateProperties
// diff算法
// 从patch开始的 patch(老的虚拟节点,新的虚拟节点)
// patchVnode 比较两个节点的差异做更新的 文本、孩子、属性。。。
// - isSameVnode 看两个节点是不是同一个节点,如果不相同删除替换即可
// - 复用之前的dom元素
// - 如果是文本看文本内容是否有差异
// - 如果是元素更新属性
// - 如果是元素在更新儿子
// - 更新儿子的三种情况 (updateChildren 两方都有儿子如何更新)
第三步:新建一个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
}
}
将对应的文件引入,然后执行对应的命令启动,就能看到对应的效果了