虚拟DOM是用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。
真实dom:
虚拟dom:
为什么要使用虚拟dom:
我们知道,Vue
是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM
,而操作真实DOM
又是非常耗费性能的。所以我们可以用JS
模拟出一个DOM
节点,称之为虚拟DOM
节点。当数据发生变化时,我们对比变化前后的虚拟DOM
节点,通过DOM-Diff
算法计算出需要更新的地方,然后去更新需要更新的视图。
虚拟DOM由h函数渲染产生。
h函数用来产生虚拟节点(vnode)
//比如这样调用h函数
h('a', {props: {href: 'http://www.test.com'}}, '举个例子');
//将得到这样的虚拟节点
{"sel": "a", "data": {props: {href: 'http://www.test.com'}}, "text": "举个例子")
//他表示的真正的dom节点
举个例子
h函数可以嵌套使用,从而得到虚拟dom树。
嵌套后得到虚拟dom树:
源码是typeScript实现的,以下用js实现一个简易版的h函数,简单易懂。
//编写一个低配版本的h函数,这个函数必须接收3个参数,缺一不可
//也就是调用的形态必须是下面三种之一
//1:h('div', {}, '文字')
//2:h('div', {}, [])
//3:h('div', {}, h())
function h(sel, data, c){
if(arguments.length !== 3)
throw new Error('对不起,h函数必须传三个参数,我们是低配版')
//检查参数c的类型
if(typeof c == 'string' || typeof c == 'number'){
//说明现在调用h函数是形态1
return vnode(sel, data, undefined, c, undefined)
}else if(Array.isArray(c)){
//说明现在调用h函数是形态2
let children = []
//遍历c,收集children
for(let i = 0; i < c.length; i++){
//检查c[i]必须是一个对象
if(!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))){
throw new Error('传入的数组参数中有项不是h函数')
}
children.push(c[i])
}
//循环结束说明children收集完了
return vnode(sel, data, children, undefined, undefined)
}else if(typeof c == 'object' && c.hasOwnProperty('sel')){
//说明现在调用h函数是形态3
//传入的c是唯一的chilren
let children = [c]
return vnode(sel, data, c,undefined, undefined)
}else{
throw new Error('第三个参数类型不对')
}
}
//实现vnode函数,用来处理返回虚拟DOM树对象
function vnode(sel, data, children, text, elm){
return {
//es6语法,相当于sel:sel,data:data
sel, data, children, text, elm
}
}
接下来测试h函数的功能:
let vnode1 = h('div',{}, [
h('p', {}, '1'),
h('p', {}, '2')
])
console.log(vnode1)
成功得到虚拟dom,一个简易版h函数就实现了。
key属性是diff算法中重要的属性。key是节点的唯一辨识,告诉diff算法,更改前后他们是同一个DOM节点。
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的,插入新的。而如何判断是否为同一个虚拟节点,通过判断选择器相同且key相同。
只进行同层比较,不进行跨层比较。
diff算法核心patch函数用来将节点上树和更新
虚拟dom如何变成真正的dom,是涵盖在diff算法里的。
接下来是精细化比较的详细步骤:
接下来是五角星中具体undateChildren方法:
四种命中查找按顺序:
1.新前与旧前 2.新后与旧后 3.新后与旧前 4.新前与旧后
如果四种都没有命中,就需要用循环来寻找了。
按照上面的流程用js实现patch函数:
export default function patch(oldVnode, newVnode){
//判断传入的第一个参数是dom节点还是虚拟节点
if(oldVnode.sel == '' || oldVnode.sel == undefined){
//传入的第一个参数是DOM节点,此时要包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
//判断oldVnode和newVnode是不是同一个节点
if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel){
console.log('是同一个节点')
patchVnode(oldVnode,newVnode)
}else {
console.log('不是同一个节点,暴力删除新的,插入旧的')
let newVnodeElm = createElement(newVnode)
//插入到老节点之前
if(oldVnode.elm.parentNode && newVnodeElm){
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
//删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
//真正创建节点,将vnode创建为DOM
export default function createElement(vnode){
//创建一个DOM节点
let domNode = document.createElement(vnode.sel)
//判断有子节点还是有文本
if(vnode.text != undefined || vnode.children == undefined || vnode.children.length == 0){
//新节点内部是文字
domNode.innerText = vnode.text
}else if(Array.isArray(vnode.children) && vnode.children.length > 0){
//新节点内部是子节点,就要递归创建节点
for(let i = 0; i < vnode.children.length; i++){
let ch = vnode.children[i]
//创建出它的DOM,一旦调用createElement意味着创建出Dom了,并且它的elm属性指向了创建出的dom,但是还没有上树
let chDom = createElement(ch)
//上树
domNode.appendChild(chDom)
}
}
//补充elm属性
vnode.elm = domNode
return vnode.elm
}
export default function patchVnode(oldVnode, newVnode) {
//判断新旧vnode是不是同一个对象
if(oldVnode === newVnode) return
//判断新的vnode有没有text属性
if(newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)){
if(newVnode.text != oldVnode.text){
//如果新的text和老的text不同,直接让新的text写入老的elm中,如果老的elm中有children也会消失掉
oldVnode.elm.innerText = newVnode.text
}
}else {
//新vnode没有text属性,意味着有children
//判断老的有没有children
if(oldVnode.children != undefined && oldVnode.children.length > 0){
//老节点有children,新节点也有children,此时是最复杂的情况
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
}else{
//老的没有children,新的有children
//清空老节点的内容
oldVnode.elm.innerHTML = ''
//遍历新vnode的子节点,创建DOM,上树
for(let i = 0; i < newVnode.children.length;i++){
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
//判断是否为同一个虚拟节点
function checkSameVnode(a,b){
return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parentElm, oldCh, newCh){
//旧前
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]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
if(checkSameVnode(oldStartVnode, newStartVnode)){
//新前与旧前
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if(checkSameVnode(oldEndIdx, newEndIdx)){
//新后与旧后
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.nextSibling)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else{
//四种都没有命中
}
}
//继续看看有没有剩余的
if(newStartIdx <= newEndIdx){
//插入的标杆
const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
for (let i = newStartIdx; i <= newEndIdx; i++){
//newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
parentElm.insertBefore(createElement(newCh[i]), before)
}
}else if(oldStartIdx <= oldEndIdx){
//批量删除oldStart和oldEnd指针之间的项
for(let i = oldStartIdx; i <= oldEndIdx; i++){
parentElm.removeChild(oldCh[i].elm)
}
}
}