探索Vue渲染器的内部机制-更新子节点

92 阅读4分钟

回顾

前面已经讨论了对元素节点的挂载,下面再来看下如何对节点进行更新,首先先来回顾一下节点是如何被挂载的也就是mountElement函数的实现过程。

function mountElement(vnode,container){
    const el = document.createElement(vnode.type);
    // 挂载的是文本子节点
    if(typeof vnode.children == 'string'){
        el.textContent = vnode.children;
    }else if(Array.isArray(vnode.children)){ // 如果是数组挂载的是一组子节点
        vnode.children.forEach(child=>{
            mountElement(child,el);
        })
    }
    container.appendChild(el);
}

正文

首先我们要先清楚在一个HTML页面中,一个元素的子节点有几种情况,那么对于一个元素节点来说,它的子节点无非有以下三种情况。

  • 没有子节点,此时vnode.children的值为null
  • 文本子节点,此时vnode.children的值为字符串
  • 多个子节点,此时vnode.children的值为数组
// 没有子节点
<div></div>

// 文本子节点
<div>Text</div>

// 一组子节点
<div>
<p></p>
<p></p>
</div>

它们对应的vnode分别是

  const vnode={
    type:'div',
    children:null
  }
  const vnode={
    type:'div',
    children:'Text'
  }
  const vnode={
    type:'div',
    children:[
      {
        type:'p',
      },
      'Text'
    ]
  }

了解了这些后,如果要对一个子节点进行修改那么必然会对应三种情况,所以在更新子节点时总共有九种可能,如下图所示。

基础流程图.png

新的子节点为文本子节点类型

下面就开始着手实现了,先来捋一下思路。首先假设要修改的新子节点是一个文本子节点,然后需要检查旧的子节点的类型,类型可能是没有子节点,文本子节点、一组子节点,如果旧的子节点是没有子节点或者文本子节点类型,那么只需要将新的文本内容设置给容器元素即可,反之它的类型是一组子节点的类型,这是就需要循环遍历它们并调用unmount函数进行卸载。

function patchElement(oldVnode,newVnode){
    const el = newVnode.el = oldVnode.el;
    patchChildren(oldVnode,newVnode,el);
}

function patchChildren(oldVnode,newVnode,container){
    if(typeof newVnode.children == 'string'){
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
        }
        container.textContent = newVnode.children;
    }
}

可以看到只有当旧的节点为一组节点时才需要进行遍历卸载,其他两种情况直接替换旧的节点即可。

新的子节点为没有子节点类型

上面说完新子节点是一个文本子节点的情况,下面再来讨论新的子节点是没有子节点类型的情况,那就需要另外加一个判断分支。新的子节点是没有子节点类型更新的话同样也有没有子节点,文本子节点、一组子节点三种情况,那么对于旧的子节点是一组子节点类型来说还是需要逐个卸载,如果是文本子节点清空其内容即可,如果旧的子节点也是没有子节点类型则什么也不需要做,再来完善一下上面的函数。

function patchChildren(oldVnode,newVnode,container){
    if(typeof newVnode.children == 'string'){
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
        }
        container.textContent = newVnode.children;
    }else{
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
        }else if(typeof oldVnode.children == 'string'){
            container.textContent = "";
        }else{
            // 如果旧的子节点是没有子节点类型则什么也不需要做
        }
    }
}

新的子节点为一组子节点类型

还有最后一种情况就是新的子节点是一组子节点类型,这种情况相对麻烦了一点,我们还需要再增加一个判断分支用来判断是新的子节点是一组子节点类型。同样,旧的子节点还会有三种可能没有子节点,文本子节点、一组子节点,如果旧的子节点类型是没有子节点或者文本子节点类型的话,只需要将容器元素清空然后将新的子节点挂载到容器中即可。但如果旧的子节点也是一组子节点类型则涉及到两组新旧节点的对比,也就是所谓的Diff算法,这个也会在后面说到,目前我们可以先模拟这一操作,即把旧的子节点全部卸载,再将新的子节点全部挂载到容器中。

function patchChildren(oldVnode,newVnode,container){
    if(typeof newVnode.children == 'string'){
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
        }
        container.textContent = newVnode.children;
    }else if(Array.isArray(newVnode.children)){
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
            newVnode.children.forEach(child=>patch(null,child,container))
        }else{
            container.textContent = "";
            newVnode.children.forEach(child=>patch(null,child,container))
        }
    }else{
        if(Array.isArray(oldVnode.children)){
            oldVnode.children.forEach(child=>unmount(child));
        }else if(typeof oldVnode.children == 'string'){
            container.textContent = "";
        }else{
            // 如果旧的子节点是没有子节点类型则什么也不需要做
        }
    }
}

最后

以上就是更新子节点时遇到的三种情况了,后续再来了解更新时所用到的Diff算法。