Vue源码学习(四):子节点比较

初次渲染完毕以后,就到了我们最重要的部分,更新视图时新老节点比较来更新我们的页面。

原理就是用新的虚拟节点和老的虚拟节点做对比,以此来跟新真实dom元素。

下面就是我们要实现的代码,newVnode是更新后的新虚拟节点,oldVnode是更新前的虚拟节点,将新旧虚拟节点传到patch函数中做对比,接下来就是实现一下patch函数了。

import {h,render,patch} from './vdom'
let oldVnode = h('div',{id:'container'},
    h('li',{style:{background:'red'},key:'a'},'a'),
    h('li',{style:{background:'yellow'},key:'b'},'b'),
    h('li',{style:{background:'blue'},key:'c'},'c'),
    h('li',{style:{background:'pink'},key:'d'},'d'),
    
);
let container = document.getElementById('app');
render(oldVnode,container);


let newVnode = h('div',{id:'aa'},
    h('li',{style:{background:'pink'},key:'d'},'d'),
    h('li',{style:{background:'blue'},key:'c'},'c'),
    h('li',{style:{background:'yellow'},key:'b'},'b'),
    h('li',{style:{background:'red'},key:'a'},'a'),

)

setTimeout(() => {
    patch(oldVnode,newVnode);
}, 1000);

首先我们需要对比的是两个虚拟节点的标签是否一样,如果标签不一样就说明父级不同了,也就没有必要去判断子节点了,这种时候就直接把老的节点干掉,替换成新的节点就好了。
替换的时候要注意一下,需要拿到当前节点的父节点才能替换掉当前节点,这里还有个细节,要将新节点通过createElm方法转换为真实节点再替换。
为什么老节点不用替换成真实节点呢?因为老节点在上一次渲染的时候就已经转换成真实节点了,只需要通过 .el 属性来拿到dom元素就可以了。代码如下

// 创建真实节点
function createElm(vnode){
    let {tag,children,key,props,text} = vnode;
    if(typeof tag === 'string'){
        // 标签  一个虚拟节点 对应着他的真实节点  主要是做一个映射关系
        vnode.el = document.createElement(tag);
        updateProperties(vnode);
        children.forEach(child => { // child是虚拟节点
            return render(child,vnode.el); // 递归渲染当前孩子列表
        });
    }else{
        // 文本
        vnode.el = document.createTextNode(text);
    }
    return vnode.el
}

export function patch(oldVnode,newVnode){
    // 先比对 标签一样不一样 
    if(oldVnode.tag !== newVnode.tag){
        // 必须拿到当前元素的父亲 才能替换掉自己
        oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
    }
}

接下来就是比较标签一样的情况,标签一样有一种情况是两者都为undefined,这时候也需要判断一下。两者可能都为undefined说明都没有标签,那么只需要判断老节点不存在tag属性,然后就是判断新老节点的文本内容是否一致,如果不一致,就将新节点的文本直接赋给老节点就行了。

当标签一样时,标签复用就行,但是可能属性不一样了,到这一步的时候,就要调用我们写的updateProperties方法做属性的对比了(该方法详情见上一篇)。

export function patch(oldVnode,newVnode){
    // 1) 先比对 标签一样不一样 
    if(oldVnode.tag !== newVnode.tag){
        // 必须拿到当前元素的父亲 才能替换掉自己
        oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
    }
    // 2) 比较文本了 标签一样 可能都是undefined
    if(!oldVnode.tag){
        if(oldVnode.text !== newVnode.text){ // 如果内容不一致直接根据当前新的元素中的内容来替换到文本节点
            oldVnode.el.textContent = newVnode.text;
        }
    }
    // 标签一样 可能属性不一样了
    let el = newVnode.el =  oldVnode.el; // 标签一样复用即可
    updateProperties(newVnode,oldVnode.props); // 做属性的比对
}

属性比较完了之后,就该比较孩子节点了。这里才是最为重要的部分。这里分成三种情况,第一种是老的有孩子,新的没有;第二种是老的没孩子,新的有;最后一种是新、老都有孩子。最后一种情况最为复杂,也是vue中最核心的代码,解决比对的问题。代码逻辑如下,实现起来也是比较简单的。

首先要拿到新老节点里的孩子节点,通过length属性判断是否存在孩子,如果新老节点的length属性都大于零就需要调用updateChildren的方法不不停递归比较,这个方法稍后会讲。其他两种情况就比较简单了就不多说了。

export function patch(oldVnode,newVnode){
    // 1) 先比对 标签一样不一样 
    if(oldVnode.tag !== newVnode.tag){
        // 必须拿到当前元素的父亲 才能替换掉自己
        oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
    }
    // 2) 比较文本了 标签一样 可能都是undefined
    if(!oldVnode.tag){
        if(oldVnode.text !== newVnode.text){ // 如果内容不一致直接根据当前新的元素中的内容来替换到文本节点
            oldVnode.el.textContent = newVnode.text;
        }
    }
    // 标签一样 可能属性不一样了
    let el = newVnode.el =  oldVnode.el; // 标签一样复用即可
    updateProperties(newVnode,oldVnode.props); // 做属性的比对

    // 必须要有一个根节点
    // 比较孩子 
    let oldChildren = oldVnode.children || [];
    let newChildren = newVnode.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++){
            let child = newChildren[i];
            el.appendChild(createElm(child)); // 将当前新的儿子 丢到老的节点中即可
        }
    }
}
function isSameVnode(oldVnode,newVnode){
    // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
    return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent,oldChildren,newChildren){
    // vue增加了很多优化策略 因为在浏览器中操作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];
    while(oldStartIndex<=oldEndIndex && newStartIndex <= newEndIndex){
        if(isSameVnode(oldStartVnode,newStartVnode)){  // 先开前面是否一样
            patch(oldStartVnode,newStartVnode);// 用新的属性来更新老的属性,而且还要递归比较儿子
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex]
        }else if(isSameVnode(oldEndVnode,newEndVnode)){ // 从后面比较看是否一样
            patch(oldEndVnode,newEndVnode); // 比较孩子 
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        }else if(isSameVnode(oldStartVnode,newEndVnode)){
            patch(oldStartVnode,newEndVnode);
            parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex]
        } // 老的尾巴和新的头去比 将老的尾巴移动到老的头的前面

        // 还有一种情况 
        // 倒叙和正序
    }
    if(newStartIndex<=newEndIndex){ // 如果到最后还剩余 需要将剩余的插入
        for(let i = newStartIndex ; i <=newEndIndex; i++){
            // 要插入的元素
            let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
            parent.insertBefore(createElm(newChildren[i]),ele);
            // 可能是往前面插入  也有可能是往后面插入
            // insertBefore(插入的元素,null) = appendChild
            // parent.appendChild(createElm(newChildren[i]))
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来回答你的问题。 Vue 组件 object_vue 源码系列一是关于 Object.defineProperty 的。Object.defineProperty 是 JavaScript 中的一个方法,可以用来定义对象的属性。这个方法可以让我们定义一个新的属性或者修改一个已经存在的属性。这个方法的语法如下: ```javascript Object.defineProperty(obj, prop, descriptor) ``` 其中,obj 是要定义属性的对象,prop 是要定义或修改的属性名,descriptor 是属性的描述符,它是一个对象,可以包含以下属性: - value:属性的值,默认为 undefined。 - writable:属性是否可写,默认为 false。 - enumerable:属性是否可枚举,默认为 false。 - configurable:属性是否可配置,默认为 false。 使用 Object.defineProperty 方法,可以实现一些高级的对象操作,例如: 1. 将一个属性设置为只读,即无法修改。 2. 将一个属性设置为不可枚举,即无法通过 for...in 循环遍历到该属性。 3. 将一个属性设置为不可配置,即无法删除该属性或者修改该属性的描述符。 在 Vue 中,Object.defineProperty 方法被广泛地应用于组件的实现中,例如: 1. 监听数据变化,通过设置 getter 和 setter 方法,实现数据的响应式更新。 2. 实现 computed 计算属性,通过设置 getter 方法,实现计算属性的缓存和响应式更新。 3. 实现 watch 监听器,通过设置 getter 方法,监听数据的变化并触发回调函数。 以上就是我对你提出的问题的回答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值