Virtual DOM
Virtual DOM是什么
Virtual DOM 本质上是一个js对象, 拥有tag props text children等属性.用这些对象属性来描述DOM节点的属性. 最后可以通过一些列的操作这个js对象转化成真实dom. 下面就是一个虚拟dom对象
var a = {
tag: 'div',
props: {},
children: [
{tag: 'div', props: {}, text: 'hello world'}
]
}
// <div>
// <div> hello world </div>
// </div>
复制代码
为什么需要Virtual DOM
- 提高渲染性能。 一个真实的dom元素是非常庞大的,当我们大量频繁的操作dom,会造成性能问题。而虚拟dom能够通过diff算法对视图进行合理的更新
- 跨平台性。 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node等。
如果你想更加了解虚拟dom. 你可以去看看snabbdom. vue中的虚拟DOM 就借鉴了 snabbdom
后文说的节点全部指的是虚拟节点
vue中怎么进行虚拟dom和diff算法
Diff算法的作用
当我们修改数据的时候,就去操作整个dom.这显然是非常消耗性能的。我们能不能够对比前后需要修改的变化。从而针对性的去修改dom。这样就能减少浏览器的重绘重排
diff的比较方式
在我们采用diff算法比较新旧两个节点,只会在同层级比较,不会跨级比较 具体可以看图
我们有如下第一个html转化成第二个html
<div>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<div>
模版编译,转化成如下
h('h1',{},[
h('div',{key: 'A'}, 'A'),
h('div',{key: 'B'}, 'B'),
h('div', {key: 'C'}, 'C')
])
<div>
<ul>
<li>C</li>
<li>B</li>
<li>A</li>
</ul>
</div>
模版编译,转化成如下
h('h1', {},[
h('div',{key: 'C'}, 'C'),
h('div',{key: 'B'}, 'B'),
h('div', {key: 'A'}, 'A'),
])
// diff算法只会div和div进行比较,ul和ul进行比较. 不可能div和ul进行比较.
复制代码
h函数
h函数的作用主要是把真正的节点转化成虚拟节点 这里我们实现一个简单的h函数。 h函数必须传递三个参数,为了代码简单, 只实现核心功能(vue中的h函数可以传递任意参数)
import vnode from './vnode.js'
// console.log(vnode(div))
export default function h(sel, data, c) {
if(arguments.length !== 3) {
throw new Error("请输入三个参数")
}
// 检查参数c的类型
if(typeof c === 'string' || typeof c === 'number') {
return vnode(sel, data, undefined, c, undefined)
}else if (Array.isArray(c)) {
let children = []
for(let i=0;i<c.length;i++) {
if( !typeof c[i] === 'object' && c.hasOwnProperty(sel)){
throw new Error("请输入正确的参数")
}
children.push(c[i])
}
return vnode(sel, data, children, undefined, undefined)
}else if(typeof c === 'object' && c.hasOwnProperty(sel)) {
return vnode(sel, data, [c], undefined, undefined)
}else {
throw new Error("请输入正确的参数")
}
}
复制代码
VNode函数
VNode 生成真正的虚拟dom
export default function(sel,data,children,text,elm) {
const key = data.key
return {
sel,data,children,text,elm,key
}
}
复制代码
经过上述操作,我们就把节点转化成虚拟dom
diff算法的大体思路
步骤1. 首先判断两个节点是否是同一个节点(tag,key相同)。如果不同,直接替换所有
步骤2. 判断两个节点完全相同, 如果相同 就不做任何操作
步骤3. 判断新节点是否有子元素。如果没有,直接替换text
步骤4. 判断旧节点是否含有子元素。如果没有,用新节点的子元素直接替换旧节点的子元素
步骤5. 如果旧节点也有子元素。那么就开始精细化比较(也是diff算法的核心部分)
patch方法
点击按钮,将两个虚拟节点进行比较,patch方法只是做一层简单的拦截,最开始的一个元素都不一样,直接暴力替换,如果一样,那么就调用patchvnode方法,进行上面的步骤 1 2 3 4
import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnode from './patchVnode.js'
export default function patch(oldVnode, newVnode) {
if(oldVnode.sel == '' || oldVnode.sel === undefined) {
// 说明是个真实的dom对象
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined, oldVnode)
}
if(oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
patchVnode(oldVnode, newVnode)
}else {
// 不是同一个节点
let newVnodeElm = createElement(newVnode)
if(oldVnode.elm != undefined) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
// console.log("1111")
}
}
复制代码
patchVnode方法
这里patchVnode主要执行的是我们的 diff 1 2 3 4步骤
步骤5(后文有讲 就是updateChildren方法)
patchVonde方法主要是进行第一层的比较, 如果下面都有children,就走到步骤5(updateChildren)
import createElement from './createElement.js'
import updateChildren from './updateChildren.js'
export default function patchVnode(oldVnode, newVnode) {
if(oldVnode === newVnode) return
if(newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
// 新节点有text
if(newVnode.text != oldVnode.text) {
oldVnode.elm.innerText = newVnode.text
}
}else {
if(oldVnode.children != undefined && oldVnode.children.length > 0) {
// 如果都有子元素,进行精细化比较
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
}else {
oldVnode.elm.innerHTML = ''
for(let i = 0;i<newVnode.children.length;i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
复制代码
createElement 方法
这个方法是用来将我们的虚拟节点转化成真实dom
export default function createElement (vnode) {
let domNode = document.createElement(vnode.sel)
if(vnode.text!= '' && (vnode.children == undefined || vnode.children.length == 0)) {
domNode.innerText = vnode.text
}else if(Array.isArray(vnode.children) && vnode.children.length > 0){
// createElement()
for (let i = 0; i < vnode.children.length; i++) {
const ch = vnode.children[i]
let chDom = createElement(ch)
domNode.appendChild(chDom)
}
}
vnode.elm = domNode
return vnode.elm
}
复制代码
精细化比较(步骤5)
我们把旧节点的一个元素称为旧前节点 旧节点的最后一个元素称为旧后节点
新节点的一个元素称为新前节点 新节点的最后一个元素称为新后节点
精细化比较主要分为五种情况
- 旧前节点 === 新前节点
- 旧后节点 === 新后节点
- 新后节点 === 旧前节点
- 新前节点 === 旧后节点
- 以上四种情况都不满足,遍历旧节点所有子元素,寻找是否有新节点的元素
以上五种情况顺序执行。满足其中一种情况,后续的就不在比较,就会去下一个节点进行比较
情况一
旧前节点=== 新前节点
情况二
旧后节点 === 新后节点
情况三
新后节点 === 旧前节点
情况四
新前节点 === 旧后节点
情况五
以上四种情况都不满足
updateChildren
import patchVnode from "./patchVnode.js";
import createElement from "./createElement.js";
function checkSameVnode(a, b) {
return a.sel === b.sel && a.key === b.key;
}
export default function updateChildren(parentElm, oldCh, newCh) {
// console.log('我是update')
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
// console.log('33333')
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// console.log(oldStartVnode, )
// 新前和旧前相同
if (oldCh[oldStartIdx] === void 0 || oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode === null || oldCh[oldEndIdx] === void 0) {
oldEndVnode = oldCh[++oldEndIdx];
} else if (newStartVnode === null || newCh[newStartIdx] == void 0) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode === null || newCh[newEndIdx] === void 0) {
newEndVnode = newCh[++newEndIdx];
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// console.log('4454')
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// console.log('1111111')
// 新后与旧后
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// 新后与旧前
patchVnode(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前与旧后
patchVnode(oldEndVnode, newStartVnode);
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 都没有匹配
if (!keyMap) {
keyMap = {};
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key !== void 0) {
keyMap[key] = i;
}
}
}
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === void 0) {
// 全新
parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
} else {
// 不是全新
const eleToMove = oldCh[idxInOld];
patchVnode(eleToMove, newStartVnode);
oldCh[idxInOld] = void 0;
parentElm.insertBefore(eleToMove.elm, oldStartVnode.elm);
}
newStartVnode = newCh[++newStartIdx];
}
}
// 结束的时候
if (newStartIdx <= newEndIdx) {
// 还有剩余节点需要处理
for (let i = newStartIdx; i <= newEndIdx; i++) {
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
}
复制代码
完整代码实现请看主页的github