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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一篇文章带你搞懂Vue虚拟Dom与diff算法

    前言 使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色.由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它. 虚拟DOM 什么是虚拟DOM? 虚拟DOM(Virtual   Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的. 真实的元素节点: <div id="wr

  • 深入理解Vue2.x的虚拟DOM diff原理

    前言 经常看到讲解Vue2的虚拟Dom diff原理的,但很多都是在原代码的基础上添加些注释等等,这里从0行代码开始实现一个Vue2的虚拟DOM 实现VNode src/core/vdom/Vnode.js export class VNode{ constructor ( tag, //标签名 children,//孩子[VNode,VNode], text, //文本节点 elm //对应的真实dom对象 ){ this.tag = tag; this.children = children

  • vue 虚拟dom的patch源码分析

    本文介绍了vue 虚拟dom的patch源码分析,分享给大家,具体如下: 源码目录:src/core/vdom/patch.js function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0]

  • vue 源码解析之虚拟Dom-render

    vue 源码解析 --虚拟Dom-render instance/index.js function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } renderMixin

  • vue 虚拟DOM的原理

    为什么需要虚拟DOM? 如果对前端工作进行抽象的话,主要就是维护状态和更新视图,而更新视图和维护状态都需要DOM操作.其实近年来,前端的框架主要发展方向就是解放DOM操作的复杂性. 运行js的速度是很快的,大量的操作DOM就会很慢,时常在更新数据后会重新渲染页面,这样造成在没有改变数据的地方也重新渲染了DOM 节点,这样就造成了很大程度上的资源浪费. 在jQuery出现以前,我们直接操作DOM结构,这种方法复杂度高,兼容性也较差.有了jQuery强大的选择器以及高度封装的API,我们可以更方便的

  • 关于Vue虚拟dom问题

    一.什么是虚拟dom? 虚拟dom本质上就是一个普通的JS对象,用于描述视图的界面结构 在vue中,每个组件都有一个render函数, 没有render找template,没有template找el,有el就会把el.outHTML作为template,然后把这串字符串编译成render函数. 有template就不往下找了.有render同理. 每个render 函数都会返回一个虚拟dom树,这也就意味着每个组件都对应一棵虚拟DOM树. 也就是说render目的就是创建虚拟dom,这个组件到底

  • 探究Vue.js 2.0新增的虚拟DOM

    你可能早就已经听说了 Vue.js 2.0.一个主要的令人兴奋的新特性就是更新页面的"虚拟DOM"的加入. 虚拟 DOM 可以做什么? React 和 Ember 都使用了虚拟DOM来提升页面的刷新速度.为了理解其如何工作,让我们先讨论一下几个概念: 更新DOM的花费时间非常长 当我们使用 JavaScript 来改变页面的时候,浏览器不得不做一些工作来找到需要的DOM节点,并且做出类似这样的改变: document.getElementById('myId').appendChild

  • 解决vue中虚拟dom,无法实时更新的问题

    碰到的问题:使用jq获取元素节点的个数时一直为0 解决方法:使用vue的nextTick()函数即可解决 原理:nextTick可以在下一次更新dom之后进行回调,我的问题在于,在页面加载完成时无法获取虚拟dom,而使用回调函数后就可以获取到正确的dom数量,所以只需要在nextTick函数中执行jq函数就可以正确获取了. self.$nextTick(function () { // DOM 更新了 $("#myCarousel").carousel(0); }) 以上这篇解决vue

  • vue2.0的虚拟DOM渲染思路分析

    1.为什么需要虚拟DOM 前面我们从零开始写了一个简单的类Vue框架(文章链接),其中的模板解析和渲染是通过Compile函数来完成的,采用了文档碎片代替了直接对页面中DOM元素的操作,在完成数据的更改后通过appendChild函数将真实的DOM插入到页面. 虽然采用的是文档碎片,但是操作的还是真实的DOM. 而我们知道操作DOM的代价是昂贵的,所以vue2.0采用了虚拟DOM来代替对真实DOM的操作,最后通过某种机制来完成对真实DOM的更新,渲染视图. 所谓的虚拟DOM,其实就是 用JS来模

  • Vue使用虚拟dom进行渲染view的方法

    前提 vue版本:v2.5.17-beta.0 触发render vue在数据更新后会自动触发view的render工作,其依赖于数据驱动:在数据驱动的工作下,每一个vue的data属性都被监听,并且在set触发时,派发事件,通知收集到的依赖,从而触发对应的操作,render工作就是其中的一个依赖,并且被每一个data属性所收集,因此每一个data属性改变后,都会触发render. vue更新监听 看一段代码 // 来自mountComponent函数 updateComponent = fun

随机推荐