Vue源码分析之虚拟DOM详解

为什么需要虚拟dom?

虚拟DOM就是为了解决浏览器性能问题而被设计出来的。例如,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。

  • ----- 元素节点: 元素节点更贴近于我们通常所看到的真实DOM节点,他有描述节点标签名词的tag属性,描述节点属性如class,attributes等的data属性,有描述包含的子节点信息的children属性等,由于元素节点所包含的情况相对而言比较复杂,源码中没有像前三种节点一样直接写死。
  • VNode的作用: 用js的计算性能来换取操作真实DOM所消耗的性能,
  • ----- VNode在Vue的整个虚拟DOM过程起到了什么作用呢。 其实VNode的作用是相当大的,我们在视图渲染之前,把写好的template模板先编译成VNode并缓存下来,等到数据变化页面需要重新渲染的时候,我们把数据发生变化后的生成的VNode与前一次缓存下来的VNode进行对比,找出差异。然后有差异的VNode对应的真实的DOM节点就是需要重新渲染的节点,最后根据有差异的创建出来的DOM节点再插入到视图中,最终完成一次视图更新。就是再数据变化前后生成真实的DOM对应的虚拟DOM节点

为什么要有虚拟DOM:

----- 就是以JS的计算性能来换取操作真实DOM所消耗的性能,Vue是通过VNode类来实例化不同类型的虚拟DOM节点,并且学习了不同类型节点生成的属性的不同,所谓不同类型的节点其本质还是一样的,都属VNode类的实例,只是实例化的时候传入的参数不同罢了。
有了数据变化前后的VNode,我们才能进行后续的DOM-Diff找出差异,最终做到只更新有差异的视图,从而达到尽可能少的操作真实DOM的目的,以节省性能

----- 而找出更新有差异的DOM节点,已达到最少操作真实DOM更新视图的目的。而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程,DOM-Diff算法是整个虚拟DOM的核心所在。

Patch

在Vue中,把DOM-Diff过程就叫做patch过程,patch意思为补丁,一个思想:所谓旧的VNode(odlNode)就是数据变化之前属于所对应的虚拟DOM节点,而新的NVode是数据变化之后将要渲染的视图所对应的虚拟DOM节点,所以我们要以生成的新的VNode为基准,对比旧的oldVNode,如果新的VNode上有的节点而旧的oldVNode没有,那么就在旧的oldVNode上加上去,如果新的VNode上没有的节点而旧的oldVNode上有,那么就在旧的oldVnode上去掉。如果新旧Vnode节点都有,则以新的VNode为准,更新旧的oldVNode,从而让新旧VNode相同。

整个patch:就是在创建节点:新的VNode有,旧的没有。就在旧的oldVNode中创建

删除节点:新的VNode中没有,而旧的oldVNode有,就从旧的oldVNode中删除

更新节点:新的旧的都有,就以新的VNode为准,更新旧的oldVNode

更新子节点

/* 
    对比两个子节点数组肯定是要通过循环,外层循环newChildren,内层循环oldCHildren数组,每循环外层
    newChildren数组里的每一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点
*/
for (let i = 0; i < newChildred.length; i++) {
    const newChild = newChildren[i]
    for (let j = 0; j < oldChildren.length; j++) {
        const oldChild = oldChildren[i]
        if (newChild === oldChild) {
            // ...
        }
    }
}

那么以上这个过程将会存在一下四种情况

  1. 创建子节点,如果newChildren里面的某个子节点在oldChildren里找不到与之相同的子节点,那么说明newChildren里面的这个子节点是之前没有的,是需要此次新增的节点,那就创建子节点
  2. 删除子节点,如果把newChildren里面的每一个子节点都循环完毕后,oldChildren还有未处理的子节点,那就说明未处理的子节点式需要被废弃的,那就把这些节点删除
  3. 移动子节点,如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,这说明此次变化的需要调整该子节点的位置,那以newChildren里的子节点1的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同
  4. 更新节点:如果newChildren里面的某个子节点在oldCHildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同

我们一再强调更新节点要以新Vnode为基准,然后操作旧的oldVnode,使之最后旧的oldVNode与新的VNode相同。

更新的时候分为三个部分:

如果VNode和oldVNode均为静态节点,

我们说了,静态节点无论数据发生任何变化都与它无关,所以都为静态节点的话则直接跳过,无需处理

如果VNode是文本节点

如果VNode是我文本节点即表示这个节点内只包含纯文本,那么只需要看oldVNode是否也是文本节点,如果是那就比较两个文本是否不同,如果不用则把oldVNode里的文本改成跟VNode的文本一样,如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把他改成文本节点,并且文本内容跟VNode相同

如果VNode是元素节点,则又细分以下两种情况

  1. 该节点包含子节点,那么此时要看旧的节点是否包含子节点,如果旧的节点里包含了子节点,那就需要递归对比更新子节点
  2. 如果旧的节点里不包含子节点,那么这个旧节点可能是空节点或者文本节点
  3. 如果旧的节点是空节点就把新的节点里的子节点创建一份然后插入到旧的节点里面,
  4. 如果旧的节点是文本节点,则把文本清空,然后把新的节点里的子节点创建一份然后插入到旧的节点里面
  5. 该节点不包含子节点,如果该节点不包含子节点,同时他又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧的节点之前里面有啥,直接清空即可
// 更新节点
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // vnode 与 oldVnode 是否完全一样,如果是,退出程序
    if (oldVnode === vnode) {
        return
    }
    const elm = vnode.elm = oldVnode.elm
    // vnode 与 oldVnode是否都是静态节点,如果是退出程序
    if (isTrue(vnode.isStatic) && isTrue(vnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
        return
    }
    const oldCh = oldVnode.children
    const ch = vnode.children
    // vnode 有 text属性,若没有
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            // 若都存在,判断子节点是否相同,不同则更新子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        }
        // 若只有vnode的子节点的存在
        else if (isDef(ch)) {
            /**
             * 判断oldVnode是否有文本
             * 若没有,则把Vnode的子节点添加到真实DOM中
             * 若有,则清空DOM中的文本,再把vnode的子节点添加到真实DOM中
             *  */
            if (isDef(oldVnode.text)) nodeOps.setTextContext(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        }
        // 如果只有oldnode的子节点存在
        else if (isDef(oldCh)) {
            // 清空DOM中的所有子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }
        // 若vnode和oldnode都没有子节点,但是oldnode中有文本
        else if (isDef(oldVnode.text)) {
            nodeOps.setTextContext(elm, '')
        }
        // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么清空什么
    } else if (oldVnode.text !== vnode.text) {
        nodeOps.setTextContext(elm, vnode.text)
    }
}

上面的我们了解了Vue的patch也就是DOM-DIFF算法,并且知道了在patch过程之中基本会干三件事,分别是创建节点,删除节点和更新节点。 创建节点和删除节点比较简单,而更新节点因为要处理各种可能出现的情况逻辑就比较复杂一些。 更新过程中九点Vnode可能都包含子节点,对于子系欸但的对比更新会有额外的一些逻辑,那么本篇文章就来学习Vue中是如何对比子节点的

更新子节点

当新的Vnode与旧的oldVnode都是元素节点并且都包含子节点的时候,那么这连个节点VNode实例上的chidlren属性就是所包含的子节点数组,对比两个子节点的通过循环,外层循环newChildren数组,内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,,就去内层oldChiildren数组里找看有没有与之相同的子节点

. 创建子节点

创建子节点的位置应该是在所有未处理节点之前,而并非所有已处理节点之后。 因为如果把子节点插入到已处理后面,如果后续还要插入新节点,那么新增子节点就乱了

. 移动子节点

所有未处理结点之前就是我们要移动的目的的位置

优化更新子节点:

前面我们介绍了当新的VNode与旧的oldVNode都是元素节点并且都包含了子节点的时候,vue对子节点是先外层循环newChildren数组,再内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点,最后根据不同的情况做出不同的操作。这种还存在可优化的地方,比如当包含子节点数量较多的时候,这样循环算法的时间复杂度就会变得很大,不利于性能提升。

方法:

  1. 先把newChildren数组里的所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做对,如果相同,那就直接进入更新节点的操作;
  2. 如果不同,再把newChildren数组里所有未处理子节点的最后一个节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  3. 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到newChildren数组里节点相同的位置;如果不同,
  4. 再把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  5. 最后四种情况都试完如果还不同,那就按照之前循环的方式来查找节点。
    Vue为了避免双重循环数据量大时间复杂度升高带来的性能问题,而选择了从子节点数组中的四个特殊位置互相对比,分别是:新前和旧前,新后和旧后,新后和旧前,新前和旧后

在前面几篇文章中,介绍了Vue中的虚拟DOM以及虚拟DOM的patch(DOM-Diff)过程,而虚拟DOM存在的必要条件是的现有VNode,那么VNode又是从哪里来的。 把用户写的模板进行编译,就会产生VNode

模板编译:

什么是模板编译:把用户template标签里面的写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生的HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数的这一段过程称之为模板编译过程。 render函数会将模板内容生成VNode

整体渲染流程,所谓渲染流程,就是把用户写的类似于原生HTML的模板经过一系列的过程最终反映到视图中称之为整个渲染流程,这个流程在上文中已经说到了。

抽象语法树AST:

  • 用户在template标签中写的模板对Vue来说就是一堆字符串,那么如何解析这一堆字符串并且从中提取出来元素的标签,属性,变量插值 等有效信息呢,这就需要借助一个叫做抽象语法树的东西。
    抽象语法树简称语法树,是源代码语法结构的一种抽象表示,他以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构,之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个析姐,比如,嵌套括号被隐含在树的结构中,并没有以节点的i形式呈现,

具体流程:

  • 将一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完毕之后的AST来生成render函数,其具体三个流程可以分为以下三个阶段

模板解析阶段:将一堆模板字符串用正则表达式解析成抽象语法树AST

优化阶段:编译AST,找出其中的静态节点,并打上标记

代码生成阶段: 将AST转换成渲染函数

有了模板编译,才有了虚拟DOM,才有了后续的视图更新

总结

到此这篇关于Vue源码分析之虚拟DOM的文章就介绍到这了,更多相关Vue虚拟DOM内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Vue源码分析之虚拟DOM详解)