精华内容
下载资源
问答
  • VUE中的虚拟DOM

    2021-03-29 16:59:44
    VUE中的虚拟DOM 1. 首先先讲一下什么是虚拟DOM,我们把组成一个DOM节点的必要的东西通过一个JS对象表示出来,那么这个JS对象就可以用来...3. VUE中的虚拟DOM怎么实现的: 3.1.VNode类 :在VUE中存在一个VNode类,通

    VUE中的虚拟DOM

    1. 首先先讲一下什么是虚拟DOM,我们把组成一个DOM节点的必要的东西通过一个JS对象表示出来,那么这个JS对象就可以用来描述这个DOM对象,我们把这个JS对象称为这个DOM对象的虚拟DOM节点.

    2. 为什么要有虚拟DOM:直接操作真实DOM是非常消耗性能的,为了尽可能的在更新视图的时候减少DOM操作,我们可以通过JS的计算性能来判断数据变化前后的状态,只更新那些需要更新的部分。

    3. VUE中的虚拟DOM是怎么实现的:

    3.1.VNode类 :在VUE中存在一个VNode类,通过这个类,我们可以实例化出不同类型的虚拟DOM节点:

    	// 源码位置:src/core/vdom/vnode.js
    	export default class VNode {
    	  constructor (
    	    tag?: string,
    	    data?: VNodeData,
    	    children?: ?Array<VNode>,
    	    text?: string,
    	    elm?: Node,
    	    context?: Component,
    	    componentOptions?: VNodeComponentOptions,
    	    asyncFactory?: Function
    	  ) {
    	    this.tag = tag                                /*当前节点的标签名*/
    	    this.data = data        /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    	    this.children = children  /*当前节点的子节点,是一个数组*/
    	    this.text = text     /*当前节点的文本*/
    	    this.elm = elm       /*当前虚拟节点对应的真实dom节点*/
    	    this.ns = undefined            /*当前节点的名字空间*/
    	    this.context = context          /*当前组件节点对应的Vue实例*/
    	    this.fnContext = undefined       /*函数式组件对应的Vue实例*/
    	    this.fnOptions = undefined
    	    this.fnScopeId = undefined
    	    this.key = data && data.key           /*节点的key属性,被当作节点的标志,用以优化*/
    	    this.componentOptions = componentOptions   /*组件的option选项*/
    	    this.componentInstance = undefined       /*当前节点对应的组件的实例*/
    	    this.parent = undefined           /*当前节点的父节点*/
    	    this.raw = false         /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    	    this.isStatic = false         /*静态节点标志*/
    	    this.isRootInsert = true      /*是否作为跟节点插入*/
    	    this.isComment = false             /*是否为注释节点*/
    	    this.isCloned = false           /*是否为克隆节点*/
    	    this.isOnce = false                /*是否有v-once指令*/
    	    this.asyncFactory = asyncFactory
    	    this.asyncMeta = undefined
    	    this.isAsyncPlaceholder = false
    	  }
    		get child (): Component | void {
    	    return this.componentInstance
    	  }
    }
    
    

    3.2:通过属性之间不同的搭配,VNode类可以描述出各种类型的真实DOM节点,再源码里:可以描述出以下几种类型的节点

    ○ 注释节点
    ○ 文本节点
    ○ 克隆节点
    ○ 元素节点
    ○ 组件节点
    函数式组件节点

    3.2.1:注释节点 :只需要两个属性,注释的文本text属性以及注释节点isComment属性

    		// 创建注释节点
    export const createEmptyVNode = (text: string = '') => {
      const node = new VNode()
      node.text = text
      node.isComment = true
      return node
    }
    

    3.2.2:文本节点 :只需要一个文本text属性

    		// 创建文本节点
    export function createTextVNode (val: string | number) {
      return new VNode(undefined, undefined, undefined, String(val))
    }
    
    

    3.2.3:克隆节点 :克隆节点就是把已有节点的属性全部复制到新节点中去,其中,克隆节点中的isCloud为true

    		// 创建克隆节点
    export function cloneVNode (vnode: VNode): VNode {
      const cloned = new VNode(
        vnode.tag,
        vnode.data,
        vnode.children,
        vnode.text,
        vnode.elm,
        vnode.context,
        vnode.componentOptions,
        vnode.asyncFactory
      )
      cloned.ns = vnode.ns
      cloned.isStatic = vnode.isStatic
      cloned.key = vnode.key
      cloned.isComment = vnode.isComment
      cloned.fnContext = vnode.fnContext
      cloned.fnOptions = vnode.fnOptions
      cloned.fnScopeId = vnode.fnScopeId
      cloned.asyncMeta = vnode.asyncMeta
      cloned.isCloned = true
      return cloned
    }
    
    

    3.2.4:元素节点 :元素节点更贴近于真实DOM节点,它有描述节点标签名词的tag属性,描述节点属性像class,attributes等的data属性,还有包含子节点信息的children属性等,由于所包含的情况相比很复杂,因此源码中也不能写死。下面举例子看一下

    		// 真实DOM节点
    <div id='a'><span>VUE源码</span></div>
    		// VNode节点
    {
      tag:'div',
      data:{},
      children:[
        {
          tag:'span',
          text:'VUE源码'
        }
      ]
    }
    
    

    从这个例子看出,真实DOM节点中,DIV标签里包含了一个span标签,span标签里包含有一段文字,反映到VNode节点上就如上所示,tag表示标签名字,data表示标签的属性id等,children表示子节点数组.

    3.2.5:组件节点 :除了有元素节点具有的属性之外,它还具有两个特有的特性,componentOptions:组件的option选项,例如组件中的props等,componentInstance:表示当前组件节点对应的VUE实例.

    3.2.6:函数式组件节点 :相比较于组件节点,它又具有两个特有的特性,fnContext:函数式组件对应的VUE实例.fnOptions:函数式组件的option选项.

    3.3:VNode的作用: 说了那么多,那么VNode在虚拟DOM的过程中到底起了什么作用呢?总的来说,VNode就是我们在视图渲染之前,把写好的template模板编译成VNode并且缓存起来,等待数据发生变化页面需要重新渲染的时候,我们把数据变化后生成的VNode与前一次缓存下来的VNode进行对比,找出差异,然后有差异的VNode对应的真是DOM节点就是需要重新渲染的节点,最后再根据有差异的VNode创建出真实的DOM节点再插入到视图中去,最终完成一次视图的更新.

    展开全文
  • 目标:写一篇关于虚拟domvue怎么去处理节点(diff算法) 目录: 1.虚拟dom 1.1 什么是虚拟dom 1.2 为什么要引入 1.3 vuejs里的虚拟dom 1.4 总结 2.patch 2.1 patch介绍 2.2 创建节点 2.3 删除节点 2.4 更新节点 ...

    前言
    目标:写一篇关于虚拟dom及vue怎么去处理节点(diff算法)

    目录:
    1.虚拟dom
    1.1 什么是虚拟dom
    1.2 为什么要引入虚拟dom
    1.3 vuejs中的虚拟dom
    1.4 总结

    2.patch
    2.0 vnode是什么
    2.1 diff比较方式
    2.2 patch介绍
    2.3 创建节点
    2.4 删除节点
    2.5 更新节点
    2.6 更新子节点
    2.7 源码&总结

    1.1 、什么是虚拟dom

    在web早期,页面交互效果比较简单,没有那么多复杂的状态需要管理,不需要频繁的去操作dom,js jq就能满足我们的需求。
    随着时代发展,页面的功能越来越多,实现的需求越多,要维护的状态也越来越多,对dom的操作也越来越频繁。
    vuejs 通过描述状态和dom之间的映射关系是怎么样的,就能将状态渲染成视图。状态可以是javascript总的任意类型。
    通常程序在改变时,状态会不断发生变化,每当发生变化时,都会重新渲染。用简单粗暴的方式去解决,不关心状态发生了什么变化,也不关心在哪里更新了dom,只把所有的dom删了,重新生成一份dom,并将其输出到页面显示出来就行。
    但是这种方式,会造成相当多的性能浪费。
    (了解)不同框架对于这种问题的处理:
    angular:脏检查,React:虚拟dom, vue1.0通过细粒度的绑定。

    虚拟DOM的解决方法:通过状态生成一个虚拟节点树,然后使用虚拟节点数进行渲染。
    在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比,只渲染不同的部分。

    1.2、为什么要引入虚拟dom

    vue1.0中,使用细粒度,因为粒子太细,每个绑定都有一个对应的watcher来观察状态的变化,会有一些内存开销以及一些依赖追踪的开销。
    vue2.0中,选择中等粒度的解决方案引入虚拟dom。组件级别是一个watcher实例,就是说即便一个组件内10个节点使用了某个状态,其实也只有一个watcher在观察这个状态的变化。当这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟dom去进行比对与渲染。

    1.3 vuejs中的虚拟dom

    在vuejs中,使用模版来描述状态与dom之间的映射关系。通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就能渲染页面。
    在这里插入图片描述

    虚拟dom在vuejs中所做的事:

    • 提供与真是dom节点所对应的虚拟节点vnode
    • 将虚拟节点vnode和就虚拟节点oldVnode进行比对,然后更新视图

    对两个虚拟节点进行比对是虚拟DOM中最核心的算法(即patch)

    总结:之所以需要先使用状态生成虚拟节点,是因为如果直接用状态生成真实DOM,会有一定程度上的性能浪费。而先创建虚拟节点再渲染视图,就可以将虚拟节点缓存,然后使用新创建的虚拟节点和上一次渲染时缓存的虚拟节点进行对比,然后根据对比结果只更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省一定的性能开销。

    2. patch

    2.0 vnode

    了解patch之前,先简单的了解下vnode
    vnode:就是一个普通的javascript对象,可理解为节点描述对象。

    // src/core/vdom/vnode.js
    export default class VNode {
      constructor (
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) {
        this.tag = tag // 当前节点标签名
        this.data = data // 当前节点数据(VNodeData类型)
        this.children = children // 当前节点子节点
        this.text = text // 当前节点文本
        this.elm = elm // 当前节点对应的真实DOM节点
        this.ns = undefined // 当前节点命名空间
        this.context = context // 当前节点上下文
        this.fnContext = undefined // 函数化组件上下文
        this.fnOptions = undefined // 函数化组件配置项
        this.fnScopeId = undefined // 函数化组件ScopeId
        this.key = data && data.key // 子节点key属性
        this.componentOptions = componentOptions // 组件配置项 
        this.componentInstance = undefined // 组件实例
        this.parent = undefined // 当前节点父节点
        this.raw = false // 是否为原生HTML或只是普通文本
        this.isStatic = false // 静态节点标志 keep-alive
        this.isRootInsert = true // 是否作为根节点插入
        this.isComment = false // 是否为注释节点
        this.isCloned = false // 是否为克隆节点
        this.isOnce = false // 是否为v-once节点
        this.asyncFactory = asyncFactory // 异步工厂方法 
        this.asyncMeta = undefined // 异步Meta
        this.isAsyncPlaceholder = false // 是否为异步占位
      }
    }
    

    vnode类型有:

    • 注释节点: 只有两个属性 isComment 是true,text为注释内容
    • 文本节点: tag data children为undefined, text不为空
    • 元素节点 有tag data children
    • 组件节点,有以下独有属性:componentOptions 组件节点的选项参数,componentInstance组件的实例,vuejs的实例。
    • 函数式节点:独有属性:functionalContext, functionalOptions
    • 克隆节点:将现有节点的属性赋值到新节点中,让新创建的节点和被克隆节点的属性保持一直。作用:优化静态节点和插槽节点。

    vnode 和真实dom的区别:

    <div>
        <p>123</p>
    </div>
    // 对应的virtual DOM(伪代码):
    
    var Vnode = {
        tag: 'div',
        children: [
            { tag: 'p', text: '123' }
        ]
    };
    

    2.1 diff 比较方式

    diff可是逐层比较的,如果第一层不一样那么就不会继续深入比较第二层了。

    在这里插入图片描述
    举个栗子:

    <!-- 之前 -->
    <div> <!-- 层级1 -->
     <p> <!-- 层级2 -->
     <b> aoy </b> <!-- 层级3 -->
     <span>diff</Span>
     </P>
    </div>
    <!-- 之后 -->
    <div> <!-- 层级1 -->
     <p> <!-- 层级2 -->
     <b> aoy </b> <!-- 层级3 -->
     </p>
     <span>diff</Span>
    </div>
    

    我们可能期望将<span>直接移动到<p>的后边,这是最优的操作。但是实际的diff操作是移除<p>⾥的
    <span>在创建⼀个新的<span>插到<p>的后边。因为新加的<span>在层级2,旧的在层级3,属于不同层级的⽐较

    2.2 patch

    虚拟DOM最核心的部分是patch,patch也可以叫patching算法,实际作用是在现有DOM上进行修改来实现视图的目的。
    原因:DOM操作的执行速度远不如javascript的运算速度快。因此,把大量的DOM操作搬运到js中,使用patching算法来计算出真正需要更新的接单,最大限度地减少DOM操作,从而显著提升性能。本质上其实是使用js的运算成本来替换DOM操作的执行成本,而js的运算速度要比DOM快很多。

    patch的目的其实是修改DOM节点,也可理解为渲染视图(对比两个Vnode的差异只是一部分)
    对现有DOM上进行修改需要做三件事:
    创建新增的节点;删除已经废弃的节点;修改需要更新的节点。

    新增节点:当oldVnode不存在而vnode存在时,需要用vnode生成真是的DOM元素并将其插入到视图当中去。
    删除节点:当oldVnode存在而Vnode不存在时,需要把他从dom中删除。
    更新节点:当新旧两个节点时相同的节点时,需要对这两个节点进行比较细致的对比。

    2.3 创建节点

    事实上,只有三种类型的节点会被创建并插入到DOM中:元素节点,注释节点,文本节点。
    判断是否为元素节点:判断tag,有该属性则为元素节点,接着就调用当前环境的createElement方法(浏览器环境下,document.createElement来创建真实的节点。当一个元素节点被创建成功后,接下来要做的就是将它插入到指定的父节点中,parentNode.appendChild,插入到指定的父节点中。如果元素节点有子节点,它的子节点也创建出来并插入到这个创建出来的节点下,创建接子节点的过程时一个递归过程。
    判断是否为注释节点:没有tag,存在唯一标识isComment,createComment
    判断是否为文本节点:createTextNode
    以下创建接待你并渲染到视图的过程:

    创建节点
    vnode是元素节点?
    创建元素节点
    vnode是注释节点?
    插入到指定父节点中
    创建注释节点
    创建文本节点

    2.4 删除节点

    oldVnode有,vnode没有的情况下,需要将元素从视图中删除

    // 删除一组指定的节点
    function removeVnodes (vnodes, startIdx, endIdx) {
    	for (; startIdx <= endIdx; ++startIdx) {
    		const ch = vnode[startIdx]
    		if (isDef(ch)) {
    			removeNode(ch.elm)
    		}
    	}
    }
    // 删除视图中单个节点
    const nodeOps = {
    	removeChild(node, child) {
    		node.removeChild(child)
    	}
    }
    function removeNode (el) {
    	const parent = nodeOps.parentNode(el)
    	if (isDef(parent)) {
    		nodeOps.removeChild(parent, el)
    	}
    }
    

    Q:为什么不直接使用parent.removeChild(child)删除节点,而是将这个节点操作封装成函数放在nodeOps中呢?
    A:涉及跨平台渲染知识,跨平台渲染的本质是在涉及框架的时候,要让框架的渲染机制和DOM解耦。

    2.5 更新节点

    2.5.1 静态节点
    更新节点时,先判断新旧两个虚拟节点是否为静态节点,是,则不需要更新操作,可以直接跳过更新节点的过程。静态节点指的是那些一旦渲染到界面上后,无论日后状态如何变化,都不会发生任何变化的节点。(比如无任何数据绑定、判断之类的)

    2.5.2 新虚拟节点有文本属性
    根据新节点是否有text属性,
    1)若新节点有text属性,那不论之前旧的是什么,都用setTextContent(浏览器环境下是node.textContent)
    2)如果之前的旧节点也有文本,且与新节点相同,那么不需要执行setTextContent。

    2.5.3 新虚拟节点没有文本属性
    那么他就是一个元素节点。元素节点通常有子节点,children属性,但也有可能没有子节点。存在两种情况
    1)有children:两种情况,旧节点中是否也有children,
    如果也有children,新旧两个虚拟节点的children进行进一步的详细的对比并更新。更新坑会移动某个子节点的位置,也可能会删除或新增某个节点,具体更新children过程在2.5 中详细介绍。
    如果无children:说明旧节点,要么是一个空标签,要么是有文本的文本节点,如果是文本节点,那么文本清空让它变成空标签,然后将新虚拟节点中的children挨个创建成真是的dom元素节点并将其插入到视图中的DOM节点下面。
    2)无children:新创建的,既没有text,也没children,说明这是个空节点。这是不管旧节点是什么,对视图进行操作,有什么删什么,最后达到视图中是空标签的目的。

    2.6 更新子节点

    对比两个子节点列表,首先需要做的事情是循环。循环newChildren,每循环到一个新姐爱你,就去就子节点列表中,找到和当前节点相同的那个旧子节点。如果找不到,说明当前子节点是由于状态改变而新增的节点,要惊醒创建接待你并插入视图的操作;如果找到了,就做更新操作,如果找到的旧子节点的位置跟新子节点的位置不同,则需要移动节点。

    2.6.1 更新策略

    针对新增、更改、移动、删除节点等操作进行讨论。
    1)创建子节点
    前面提到,新旧两个子节点列表是通过循环进行对比的。所以创建节点的操作是在循环体内执行的,其具体的实现是在oldChildren中寻找本次循环所指向的新子节点。如果没有找到说明是本次循环所指向的新子节点是一个新增节点。对于新增节点,需要执行创建节点的操作,并将新创建的节点插入到oldChildren中所有未处理节点(未处理就是没有进行温和更新操作的节点)的前面,当节点成功插入DOM,这一轮的循环。
    Q:为什么插入到oldChildren的所有未处理节点的前面?
    A : 如图,最上面的Dom节点是视图中真实的Dom节点。左下角是新创建的虚拟节点,右下角是旧的虚拟节点。下图表示已经对前两个子节点进行了更新,当前正在处理第三个节点。当右下角的虚拟子节点中找不到与左下角的第三个节点相同的节点时,证明他是新增的节点,这是需要创建节点并插入到DOM中,插入的位置是所有未处理节点的前面。
    在这里插入图片描述
    Q:插入到已处理节点的后面不行吗?
    A:不行的,如果是新节点后面也是新节点呢,那么这两个新增节点的顺序会反了。因为我们使用虚拟节点进行对比,而不是真是的DOM节点做对比,所以左下角的和右下角的虚拟节点做对比,而右下角的节点中,表示已处理的节点只有两个,不包括我们新插入的节点,所以用插入到已处理节点后面这样的逻辑来插入节点,会插入一个错误的位置。

    2)更新子节点,本质上是当一个节点同时存在与newChildren 和oldChildren中时需要执行的操作。
    如果两个节点是同一个节点,并且位置相同,这种情况喜爱只需要进行更新节点的操作即可。若位置不一致,除更新真实节点外,还要对这个真实Dom节点进行移动节点的操作

    3)移动子节点。
    同一节点不同位置,以新虚拟节点的位置为基准进行移动。
    通过node.insertBefore(),可以成功将一个已有节点到一个指定的位置。
    把需要移动的节点移动到所有未处理节点的最前面。

    4)删除子节点
    当newChildren中的左右节点都被循环一边后,也就是循环结束后,如果OldChildren中还有剩余的没有被处理的节点,那么这些节点就是被抛弃,需要删除的节点。

    2.6.2 优化策略

    针对一些位置不变的或者说位置可以预测的节点,不需要循环来查找。只需要尝试相同位置的两个节点来对比是否是同一个节点;如果恰巧是同一个,直接进入更新节点的操作;如果不是,则用循环的方式来查找。
    很大程度地避免循环oldChildren来查找节点,从而使执行速度得到很大的提升。
    共有4种查找方式:
    新前与旧前、新后与旧后、新后与旧前、新前与旧后

    新前:newChildren 新虚拟节点中所有未处理的第一个节点。
    新后:newChildren 新虚拟节点中所有未处理的最后一个节点。
    旧前: oldChildren 新虚拟节点中所有未处理的第一个节点。
    旧后: oldChildren 新虚拟节点中所有未处理的最后一个节点。

    1)新前与旧前
    若是同一节点,因为他们位置相同,所以只需要更新节点即可,如果不是,下一种查找方法。

    2)新后与旧后
    若是同一节点,因为位置也相同,所以只需要更新节点即可。

    3)新后与旧前
    如果是同一节点,因为他们位置不同,除了更新节点外,还需要执行移动节点的操作,移动位置为oldChidren中所有为处理节点的最后面。更新节点是以新虚拟节点为基准,子节点也不例外。
    Q:为什么是oldChidren中所有为处理节点的最后面
    A:当真实DOM子节点左右两侧已经有节点被更新,只有中间这部分节点未处理时,“新后”这个节点是未处理节点的最后一个节点。所以真实DOM节点移动位置时,需要移动到oldchildren中所有未处理节点的最后面。只有移动到未处理节点的最后面,他的位置才能与新后这个节点位置相同。

    4)新前与旧后
    这两个节点的位置不同,所以除了更新节点外的操作外,还需要移动节点的操作。移动位置到oldChildren中所有未处理节点的前面,与上面的新后与旧前的逻辑一样。
    也就是说,已更新过的节点都不用管,因为更新过的节点无论是节点的内容或者节点的位置,都是正确的,更新完后面旧不用管了。所以,只需要在所有未更新的节点区间内进行移动和更新操作即可。

    比较图解:
    在这里插入图片描述
    第一步:旧前新前:1比1 同,oldStartIdx++,newStartIdx++
    第二步:旧前新前:2比5,不同, 旧后新后:6比6,同,oldEndIdx–,newEndIdx–,
    在这里插入图片描述
    第三步:旧前新前:2:5 不同,旧后新后:5:2,不同,旧前新后,2:2,同,oldEndIdx–,newEndIdx–,把2放到oldEndIdx后面
    在这里插入图片描述
    第四步:旧前新前:3:5 不同,旧后新后:5:7,不同,旧前新后,3:7,不同,旧后新前:5:5,同,oldEndIdx–,newStartIdx++,把5放到oldStartIdx前面
    在这里插入图片描述
    第五步:用四种方案比较,7在old Vnode中都没有,那就将7放到oldStartIdx前面,然后newEndIdx–, 此时newEndIdx<newStartIdx,那么比较结束,此时oldStartIdx和oldEndIdx之间(包括这两个)是删除的节点,将其删除即可
    在这里插入图片描述
    如果设置了key:
    在第五步,不会去循环虚拟列表,而是在一个map中查找该key,然后移动。
    算法复杂度为O(n)

    没设置key:最好O(n),最坏的O(n^2)

    2.6.3 哪些节点是未处理的

    Q:怎么分辨哪些节点是处理过的,哪些是未处理过的?
    A:逻辑都是在循环体内处理的,所以只要让循环条件保证只有未处理过的节点才能进入循环体,就能达到忽略已处理过的节点从而只对未处理节点进行对比和更新操作。

    Q:从上面的策略看出,虚拟节点对比都是从最前或最后进行比较的,那怎么实现从两边到中间的循环呢?
    A:准备四个变量oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分别表示旧虚拟节点的开始位置的下标,结束位置的下标,新虚拟节点开始位置的下标,结束位置的下标。开始位置所表示的节点被处理后,就向后移一个位置,结束位置的节点被处理后,就向前移动一个位置。
    当开始位置大于等于结束为止,说明所有接待你都遍历过了

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    	// 做点什么
    }
    

    Q:怎么知道那个是新增的节点?
    A:如果oldChidren先循环完了,newChidren中还有节点,那么这些节点就是新增的节点,即newStartIdx 到oldStartIdx这区间的节点就是新的。所以如果是newChidren先循环完毕,oldChildren还没,则表示有删除的节点,删除节点的区间也在oldStartIdx 到oldEndIdx

    2.7 总结

    贴下patch源码

    function patch (oldVnode, vnode) {		// 参数:新节点 旧节点
        // some code
        if (sameVnode(oldVnode, vnode)) {	// 是否为同一个节点
            patchVnode(oldVnode, vnode)
        } else {
            const oEl = oldVnode.el 				// 当前oldVnode对应的真实元素节点
            let parentEle = api.parentNode(oEl)  // 父元素
            createEle(vnode)  // 根据Vnode生成新元素
            if (parentEle !== null) {
                api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
                api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
                oldVnode = null
            }
        }
        // some code 
        return vnode
    }
    
    function sameVnode (a, b) {
      return (
        a.key === b.key &&  // key值
        a.tag === b.tag &&  // 标签名
        a.isComment === b.isComment &&  // 是否为注释节点
        // 是否都定义了data,data包含一些具体信息,例如onclick , style
        isDef(a.data) === isDef(b.data) &&  
        sameInputType(a, b) // 当标签是<input>的时候,type必须相同
      )
    }
    
    patchVnode (oldVnode, vnode) {
        const el = vnode.el = oldVnode.el
        let i, oldCh = oldVnode.children, ch = vnode.children
        if (oldVnode === vnode) return
        if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
            api.setTextContent(el, vnode.text)
        }else {
            updateEle(el, vnode, oldVnode)
            if (oldCh && ch && oldCh !== ch) {
                updateChildren(el, oldCh, ch)
            }else if (ch){
                createEle(vnode) //create el's children dom
            }else if (oldCh){
                api.removeChildren(el)
            }
        }
    }
    
    

    patchVnode 这个函数做了以下事情:

    • 找到对应的真实dom,称为el
    • 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
    • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
    • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
    • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
    • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要

    更新子节点:

    updateChildren (parentElm, oldCh, newCh) {
        let oldStartIdx = 0, newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx
        let idxInOld
        let elmToMove
        let before
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {   	// 对于vnode.key的比较,会把oldVnode = null
                oldStartVnode = oldCh[++oldStartIdx] 
            }else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx]
            }else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx]
            }else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
            }else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode)
                api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                oldStartVnode = oldCh[++oldStartIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode)
                api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
                oldEndVnode = oldCh[--oldEndIdx]
                newStartVnode = newCh[++newStartIdx]
            }else {
               // 使用key时的比较
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
                }
                idxInOld = oldKeyToIdx[newStartVnode.key]
                if (!idxInOld) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    newStartVnode = newCh[++newStartIdx]
                }
                else {
                    elmToMove = oldCh[idxInOld]
                    if (elmToMove.sel !== newStartVnode.sel) {
                        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    }else {
                        patchVnode(elmToMove, newStartVnode)
                        oldCh[idxInOld] = null
                        api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                    }
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
        if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
        }else if (newStartIdx > newEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
    }
    

    这个函数的作用:

    • 将Vnode的子节点Vch和oldVnode的子节点oldCh提取出来
    • oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。

    Q:在上面的循环中一开始就先判断oldStartVnode oldEndVnode是否存在,如果不存在则直接跳过本次循环,进行下一轮的循环,(也就是说,如果这个节点不存在,则跳过这个节点,处理下一个节点)
    A:主要是为了处理旧节点已经被移动到其他位置的情况。移动节点时,真正移动的时真实的DOM节点。移动后,为了防止后续重复处理同一节点,旧虚拟节点就会被设置成undefined,用来标记这个节点已经被处理并且移动到其他位置。

    使用key:
    在vuejs的模版中,如果渲染时使用了key,这个属性可以标示一个节点的唯一ID,vue官方非常推荐使用这个属性。在前面提到,更新子节点时,需要从oldChildren中循环去找一个节点。vue会去建立key 与index索引的对应关系时,就会去生成一个key对应这一个节点下标这样一个对象。也就是如果在节点设置了属性key,那么在oldchildren中找到相同节点时,可以直接通过key拿到下标,从而获取key,这样就不需要根据循环来查找节点。

    展开全文
  • 虚拟DOM

    2016-10-28 11:27:34
    学习React或Vue以及其它前端的框架时,“Virtual DOM”这个词汇就会很常见,总是听说虚拟DOM对性能的提升很有帮助,可是一直都不清楚虚拟DOM是个啥玩意,它的原理是什么。 在参考了以下链接中的博文后对虚拟DOM有个...

    学习React或Vue以及其它前端的框架时,“Virtual DOM”这个词汇就会很常见,总是听说虚拟DOM对性能的提升很有帮助,可是一直都不清楚虚拟DOM是个啥玩意,它的原理是什么。
    在参考了以下链接中的博文后对虚拟DOM有个大概的印象:

    全面理解虚拟DOM,实现虚拟DOM
    如何实现 Virtual DOM
    怎么更好的理解虚拟DOM?- 知乎
    网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?罗志宇的回答-知乎

    展开全文
  • 因为今天在给一个项目做一个小demo,用到了jQuery,想起用了VUE之后就没怎么用过jQuery做DOM操作了;于是感慨到virtual DOM的好处,于是想着回家整理整理 什么时候 virtual DOM ?为什么存在virtual DOM? 用JS模拟...

    因为今天在给一个项目做一个小demo,用到了jQuery,想起用了VUE之后就没怎么用过jQuery做DOM操作了;于是感慨到virtual DOM的好处,于是想着回家整理整理

    什么是virtual DOM ?为什么存在virtual DOM?

    • 用JS模拟虚拟的DOM结构,生成虚拟的DOM,当数据更新是,对比DOM的变化,只更新需要更新的数据,从而减少"昂贵"的DOM操作;
    • 为了提高DOM重绘制的性能

    HTML代码如下:
    在这里插入图片描述
    对应的vdom如下:(class是关键字,所以会别名叫做className)
    在这里插入图片描述

    vdom如何应用,核心API是什么?

    使用snabbdom 来实现vdom==>dom

    • 使用snabbdom ,把一个创建好的虚拟DOM生成为浏览器页面上的真实DOM;

    • snabbdom有以下重要的API

      • h(‘<标签名>’, {…属性…}, […子元素…])
      • h(‘<标签名>’, {…属性…}, ‘….’)
      • patch(container, vnode)
      • patch(vnode, newVnode)
    • 创建一个div和一个button按钮,div放入一个虚拟dom生成的UI li 列表,点击button按钮,实现数据的diff对比更新

    // html
    <div id="container"></div>
    <button id="changeNode">change</button>
    
    // 全局注册
    var snabbdom = window.snabbdom;
    
    var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ]);
    // 定义h函数
    var h = snabbdom.h
    
    // 生成VNODE
    var vnode = h('ui#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item 2'),
    ])
    
    // 获取容器
    var container = document.getElementById('container');
    // 第一次把VNODE全部更新到容器中去
    patch(container, vnode)
    
    // 准备新节点
    var newVnode = h('ui#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item222 2'),
        h('li.item', {}, '33232 2'),
    
    ])
    
    document.getElementById("changeNode").addEventListener("click", function() {
    // 点击按钮,通过diff算法更新节点数据
        patch(vnode, newVnode)
    })
    

    diff算法

    • vdom中找本次DOM需要更新节点来更新
    • patch函数,首次渲染,新旧对比,打补丁更新
    • patch(container, vnode) 和 patch(vnode, newVnode)
    • createElment & updateChildren

    createElment 的实现

    • 实现虚拟dom节点核心逻辑思想是:创建根节点,如果有子节点,继续递归调用createElment方法,只到把节点append进上一级节点。从而在页面上出现真实的DOM结构;
    • 对应着 snabbdom patch(container, vnode)

    伪代码逻辑如下:

    function createElement(vnode){
    	var tag=vnode.tag
    	var attr=vnode.attrs||{}
    	var children=vnode.children||[]
    
    	if(!tag){
    		return null;
    	}
    
    	// 真实的DOM元素
    	var elem=document.createElement(tag);
    	var attrName
    
    	for(attrName in attrs){
    		elem.setAttribute(attrName,attrs[attrName]);
    	}
    
    	// 子元素
    	children.forEach(childVnode=>{
    		// 给Element 添加子元素,递归
    		elem.appendChild(createElement(childVnode))
    	});
    	// 返回创建好的DOM元素
    	return elem;
    }
    

    updateChildren & diff

    • diff只更新当前需要更新的数据
    • 对比节点的数据,相同则跳过,不相同replace掉久的节点
    • 对应着 snabbdom patch(vnode, newVnode)

    伪代码逻辑如下:

    function updateElement(vnode,newVnode){
    	var children=vnode.children||[];
    	var newChildren=newChildren.children||[];
    
    	children.forEach((item,index)=>{
    		var newItem=newChildren[index];
    		if(item==newItem){
    			// 递归深层次对比
    			updateElement(item,newItem);
    		}else{
    			// 不同则替换
    			replaceNode(item,newItem);
    		}
    	})
    
    }
    

    React的DIFF算法为什么时间复杂度是O(n)复杂度而不是常规的O(n^3)

    时间复杂度的理解见我这篇文章:算法与数据结构 | 时间复杂度分析 / 更准确的描述代码的时间复杂度

    为啥呢?

    首先:常规的O(n^3);

    • 传统的diff需要出了上面的比较之外,还需要跨级比较。
    • 两个新旧节点的遍历是O(n^2)
    • 执行节点的更新再一次,所以为O(n^3)

    React 的O(n)

    • react树diff对比是按照层级,会给树编号0,1,2,3,4… 然后相同的编号层级进行比较,所以复杂度是n

    diff 算法非常复杂,以上只能算是浅谈了,了解了大概的设计逻辑;

    diff还在一些场景用过,就是git 提交的时候常常 git diff XXX 看文件对比哈哈哈哈

    展开全文
  • Virtual DOM实现原理

    2021-05-06 10:35:39
    Snabbdom的基本使用(Vue内部的虚拟Dom是改造了开源库Snabbdom) Snabbdom的源码解析 在面试的时候经常会问到虚拟DOM怎么工作的,通过查看Snabbdom源码,可以对这块内容有更加深入的了解。 1、什么是Virtual DOM ...
  • virtual-dom 虚拟dom

    2019-10-08 12:03:02
    一直在用Vue进行项目开发,很好奇这框架是怎么做到数据和视图之间的快速响应绑定的,因为如果是单纯的dom操作是很耗费性能的,所以在一番搜索之后才发现是用了一种”虚拟dom”的思路,就是用js去模拟dom,监听变化,...
  • 前言从上篇文章《vue编译过程分析》中,我们了解到HTML模板经过编译,最终会生成一个render...vnode创建大家知道一个复杂的页面会包含大量的DOM节点,为了高效地更新这些DOM节点,vue设计了虚拟DOM的概念。虚拟DOM是...
  • 我们该怎么实现??? 简单的模板渲染 虚拟DOM 目标: 怎么将真正的DOM转换为虚拟DOM 怎么将虚拟DOM转换为真正的DOM 思路与深拷贝类似 函数科里化 参考资料: 概念: 科里化:一个函数原本有多个参数,然后加入一个参
  • 本文会分析什么是虚拟DOM,为什么要引入它,它又是怎么工作的。 之后再谈谈什么是响应式,它又是如何实现的。以及响应式与虚拟DOM之间的关联。 我会尽量详细的阐述文中可能涉及的一切知识,文章可能稍显冗长,我会将...
  • 前端面试之vue

    2020-04-06 18:41:31
    目录vue的特点(优势)vue和react的区别computed和watch有什么区别数据双向绑定(响应式原理),订阅者update是怎么实现的vue数组和对象的更新方式vue生命周期vue虚拟dom和diff算法模板是怎么解析的vue路由的实现...
  • vue面试题

    千次阅读 2020-07-07 02:14:05
    虚拟 DOM 实现原理 既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异? Vue 中 key 值的作用? Vue 的生命周期 Vue 组件间通信有哪些方式? watch、methods 和 computed 的...
  • Vue源码学习路线

    2020-05-20 14:32:33
    学习Vue内部是怎么把template模板编译成虚拟DOM,从而渲染出真实DOM 实例方法篇 学习Vue中所有实例方法(即所有以$开头的方法)的实现原理 全局API篇 学习Vue中所有全局API的实现原理 生命周期篇 学习
  • vue的component标签是个虚拟dom,在真实dom树上需要vue进行渲染,显示。而我最近遇到的问题,我通过LoadJS方法,从A项目读取B项目的对象,页面并已组件的形式渲染到A项目页面上,组件没法渲染。 我第一反应 是this.$...
  • 前言 这是本人的学习的记录,因为最近在准备面试,很多情况下会被提问到:请简述 mvvm ?...那么问题来了,你知道 mvvm 是怎么实现的? 回答: mvvm 主要通过 Object 的 defineProperty 属性,重写 data...
  • vue diff算法理解

    2019-04-17 17:58:00
    1. vue怎么利用虚拟dom进行渲染优化的? 如果修改任意一个值都重刷整个页面,这样的开销将会十分的巨大。那么怎么精确快速的定位被修改节点呢?使用diff算法(该算法来源于snabbdom,复杂度为O(n)) 2. 简单介绍...
  • 前言 我认为diff算法具备两个特点。 一、高效性:有虚拟dom,必然需要diff算法。...下面我们通过手写简易diff算法,来看看具体是怎么实现的吧? 比较新旧虚拟节点 patch(vnode,newVnode)分为以下几种情况 1.标签名
  • vue render函数进阶学习

    2019-10-06 05:11:49
    这篇博客我们来简单讲一讲render函数他是怎么实现得 先来一张官方得图 在实例初始化得时候,html通过render函数编译生成了一个虚拟dom,视图层会根据虚拟dom生成一个真实dom 然后如果响应数据发生变化得时候,...
  • vue2.0 diff算法

    2019-11-06 16:41:11
    研究一下虚拟dom具体如何实现 先来了解几个点... 1. 当数据发生变化时,vue怎么更新节点的? 要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘...
  • 目录理解Vue的MVVMVue双向绑定原理vue怎么操作DOM虚拟DOM(virtual DOM)diff算法为什么data在组件中必须是一个函数$nextTickVue 不能检测数组和对象的变化v-for与v-if不能同时使用 理解Vue的MVVM Vue双向绑定原理...
  • vue必背面试题

    2020-09-20 20:30:44
    Vue练习手册及答案 第一天vue基础 1、简述MVVM和MVC ...2、简述虚拟DOM 对复杂的文档DOM结构,提供一种方便的工具,进行最小化的DOM操作 3、怎么创建vue的实例 #### 4、列举常用指令以及作用 (1)V-for循环 (2)v-
  • 教你阅读vue源码的正确姿势,看完就学废!

    千次阅读 热门讨论 2021-04-30 16:49:30
    简介一下个人阅读vue源码的姿势,有建议欢迎评论区补充哈~ 一、源码阅读姿势 1. 先整体 - 后细节 ...多数情况不需要逐行代码的细究,但针对某些核心功能的实现需要细究,例如:虚拟dom、diff算法、.
  • 好久没写东西,博客又长草了,这段时间身心放松了好久,都没什么主题可以写了 上周接到一个需求,优化...使用了vue框架,框架内部的虚拟DOM和组件缓存已经做了一些优化,比起原生实现是有了一些优化处理。 但这个页..
  • 前端面试题(VUE

    2019-09-26 21:04:41
    虚拟DOM的作用 vue@3.0中的preset配置? 父组件A和其子组件B/子组件C,B/C进行通信的方式(怎么通信) 组件如何设置并被使用多个组件$message如何实现最后触发的在最上面 Vue 如何监听一个不会触发 render 的...
  • Vue 练习手册及答案

    2020-09-20 19:02:26
    Vue练习手册及答案 1、简述MVVM和MVC ...2、简述虚拟DOM 对复杂的文档DOM结构,提供一种方便的工具,进行最小化的DOM操作 3、怎么创建vue的实例 ## 4、列举常用指令以及作用 (1)V-for循环 (2)v-on绑定事件(3)v-
  • Vue 源码解读-2.6.11Vue 基础原理new Vue 做了什么各种初始化初始化 $mount("#app") 做了什么data 值是怎么绑定的呢生命周期 是怎么绑定的,各个阶段都在干什么数据绑定是怎么实现的事件绑定是怎么实现的虚拟Dom 干...
  • Vue 一套用于构建用户界面的渐进式框架,采用MVVM开发模式,实现数据双向绑定,虚拟DOM提高性能。 1、渐进式怎么理解? vue是自底向上逐层应用:声明式渲染—组件系统—客户端路由—大数据状态管理—构建工具; 2...
  • 【面试题】整理8/15

    2020-08-15 12:14:19
    vue虚拟dom 虚拟DOM是通过一个JavaScript对象来描述真实DOM。 虚拟DOM**并不是说比原生DOM API的操作快,而是不管数据怎么变化,都可以以最小的代价来进行DOM的更新 **。 vue双向数据绑定 ...vue实现数据双向绑定主要是...
  • 面试内容 小米-小米应用商店(过) 一面 小米的面试官给人的感觉很亲切很...项目里面的前端鉴权是怎么实现的vue里面的虚拟dom是怎么回事? vue双向绑定讲一讲 手写函数防抖和函数节流 讲讲常用的es6语法,比如...

空空如也

空空如也

1 2 3
收藏数 46
精华内容 18
关键字:

vue虚拟dom怎么实现的

vue 订阅