虚拟DOM-diff算法

  • 本次掌握:
    • 虚拟DOM如何被渲染函数(h函数)产生?
      • 要手写h函数
    • diff算法原理?
      • 要手写diff算法
    • 虚拟DOM如何通过diff变为真正的DOM的
      • 虚拟DOM变回真正的DOM,是涵盖在diff算法里面的

学习的尚硅谷Vue源码解析课程,做个笔记让自己更理解

虚拟DOM

虚拟Dom:用JavaScript对象来描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性

diff算法是发生在虚拟DOM上的,新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应如何最小量更新,最后反映到真正的DOM

真实DOM如何编译成虚拟DOM见尚硅谷Vue源码解析mustache课程

h函数

h函数用来产生虚拟节点(vnode)

比如这样调用h函数:

h('a', {props: {href: 'http://www.baidu.com'}}, '百度')

将得到这样的虚拟节点:

{sel: 'a', data: { props: { href: 'http://www.baidu.com' } }, text: '百度',}

它表示的真正的DOM节点:

<a href="http://www.baidu.com">百度</a>

一个虚拟节点的属性有哪些在这里插入图片描述

让虚拟节点变为真实dom节点(具体细节后面):

// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
	
// 创建虚拟节点
var myVnode1 = h('a', {props: {href: 'http://www.baidu.com'}}, '百度')
console.log(myVnode1);
	
// 让虚拟节点上树
const container = document.getElementById('container')
	
patch(container, myVnode1)

h函数可以嵌套使用,从而得到虚拟DOM树(重要)

比如这样嵌套使用h函数:

h('ul', {}, [ 
	h('li', {}, '牛奶'),
	h('li', {}, '咖啡'),
	h('li', {}, '可乐')
]);

将得到这样的虚拟DOM树:

 {
	"sel": "ul",
	"data": {}, 
	"children": [ 
		{ "sel": "li", "text": "牛奶" },
		{ "sel": "li", "text": "咖啡" },
		{ "sel": "li", "text": "可乐" } 
	] 
}

h函数的多种用法(利用重载):

h('div')
h('div', '文字')
h('div', [])
h('div', h())
h('div', {}, '文字')
h('div', {}, [])
h('div', {}, h())

手写h函数

// 	vnode.js(返回节点对象):
export default function(sel, data, children, text, elm) {
    return {
        sel, data, children, text, elm
    }
}
import vnode from './vnode.js';
	
// 编写低配的h函数,必须接受三个参数:标签,数据对象,c【text/[]/h()】
// 重载功能较弱
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
// 形态2举例:
// h('ul', {}, [ 
// 	h('li', {}, '牛奶'),
// 	h('li', {}, '咖啡'),
// 	h('li', {}, '可乐')
// ]);
export default function (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++) {
	           // 检查c[i]必须是一个对象(h函数调用的结果)
	           if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
	               throw new Error('传入的数组参数中有项不是h函数')
	           }
	           // 直接放入children中
	           children.push(c[i])
	       }
	       return vnode(sel, data, children, undefined, undefined)
	   } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
	       // 形态③
	       let children = [c]
	       return vnode(sel, data, children, undefined, undefined)
	   } else {
	       throw new Error('传入的第三个参数类型不对')
	   }
}

diff算法

初步理解

  1. 最小量更新太厉害啦!真的是最小量更新!当然,key很重要。key是这个节点的
    唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。

  2. 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。

    延伸问题:如何定义是同一个虚拟节点?
    答:选择器相同且key相同

  3. 只进行同层比较,不会进行跨层比较。 即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的、然后插入新的。
    请添加图片描述

diff处理新旧节点不是同一个节点时

在这里插入图片描述

  1. 如何定义“同一个节点”
    旧节点的key要和新节点的key相同且旧节点的选择器要和新节点的选择器相同
  2. 创建节点时,所有子节点需要递归创建的
  3. 不是同一个节点:
    import vnode from './vnode';
    import createElement from './createElement'
    
    export default function (oldVnode, newVnode) {
        // 判断第一个参数是否为虚拟节点,不是则包装为虚拟节点
        if (oldVnode.sel === '' || oldVnode.sel === undefined) {
            oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
        }
        // 判断oldVnode和newVnode是否为同一个虚拟节点【比较key和sel】
        if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
    
        } else {
            // 直接全部删除,重新创建节点
            console.log('直接全部删除,重新创建节点');
            let newVnodeElm = createElement(newVnode)
            // 将新创建的孤儿节点上树
            if (oldVnode.elm.parentNode != undefined && newVnodeElm) {
                oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
                // 删除老节点
                oldVnode.elm.remove()
            }
        }
    }
    
    // 创建真实DOM,是孤儿节点不进行插入
    export default function createElement (vnode) {
        // 创建dom节点,此时该节点还是孤儿节点
        let domNode = document.createElement(vnode.sel)
        // 有子节点还是有文本
        // 是文本节点就不用递归
        if ((vnode.text != '' || 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 child of vnode.children) {
                domNode.appendChild(createElement(child))
            }
        } 
        // 补充elm属性
        vnode.elm = domNode
        // 返回elm, elm属性是一个纯DOM对象
        return vnode.elm // return domNode
    }
    

diff处理新旧节点是同一个节点时

在这里插入图片描述

  1. 新旧节点text的不同情况
    // 判断newVnode和oldVnode是否为同一个对象
        if (oldVnode === newVnode) {
            return
        }
        // 判断newVnode是否有text属性
        if ((newVnode.text != '' || newVnode.text != undefined) &&
            (newVnode.children == undefined || newVnode.children.length == 0)) {
            if (newVnode.text != oldVnode.text) {
                // 替换成新节点的text
                oldVnode.elm.innerText = newVnode.text
            }
        } else {
            if(oldVnode.children != undefined && oldVnode.children.length > 0) {
                // 要比较oldVnode和newVnode的子节点【递归】
                
            } else {
                oldVnode.elm.innerHTML = ''
                // oldVnode要添加newVnode的子节点
                for(let child of newVnode.children) {
                    oldVnode.elm.appendChild(createElement(child))
                }
            }
        }
    
  2. diff算法的子节点更新策略

    经典的diff算法优化策略【四种命中查找】:
    新前与旧前(新前就是新的虚拟DOM中未处理的开头节点)
    新后与旧后(新后就是新的虚拟DOM中未处理的最后一个节点)
    新后与旧前(此种发生了,涉及移动节点,那么新后指向的节点,移动到所有未处理的旧后之后
    新前与旧后(此种发生了,涉及移动节点,那么新前指向的节点,移动到所有未处理的旧前之前

    命中一种就不再进行命中判断了
    如果都没有命中,就需要用循环来寻找了。移动到oldStartIdx之前。

    1. 新增情况
      第一种方式命中查找:就比较旧前与新前,节点相同,新前与旧前指针往后移动;依次比较,旧虚拟DOM先遍历完,那么新虚拟DOM中还未遍历到的节点就是需要新增的节点
      在这里插入图片描述

    2. 删除情况
      在这里插入图片描述

    3. 多删除的情况
      在这里插入图片描述

    4. 复杂的情况
      在这里插入图片描述
      旧虚拟DOM:ABCDE; 新虚拟DOM: ECM
      新前E 与旧前A 比较,未命中;
      新后M 与旧后E 比较,未命中;
      新后M 与旧前A 比较,未命中;
      新前E 与旧后E 比较,命中:将当前新前指向的节点E插入到旧前指向的节点A前面,新前++【指向C】,旧后–【指向D】【即将虚拟DOM的E节点置为undefined,移动真实节点E到A前面】;
      新前C 与旧前A 比较,未命中;
      新后M 与旧后D 比较,未命中;
      新后M 与旧前A 比较,未命中;
      新前C 与旧后D 比较,未命中;
      四种命中查找都未命中,则在旧虚拟DOM中循环查找新前指向的节点C,找到之后将旧虚拟DOM的C节点置为undefined,且移动节点到旧前节点A前面,新前++【指向M】
      再重复进行四种命中查找:
      节点M未命中,进行循环查找,没有找到则将M节点插入到所有旧前节点前;新前++
      新前 > 新后 退出循环
      此时旧虚拟DOM还有剩余节点未遍历处理过,删除剩余节点A、B、D【即旧前旧后之间的真实节点】

    5. 复杂的情况
      在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值