Virtual DOM 的实现原理

Virtual DOM
Virtual DOM(虚拟DOM),是由普通的js对象来描述DOM对象。

  1. 为什么要使用虚拟DOM
    1. 真实DOM的创建需要花费很大代价
    2. 虚拟DOM通过比较前后两次状态差异更新真实DOM,极大的减少了真实DOM的创建。
  2. 虚拟DOM的作用
    1. 维护视图和状态的关系
    2. 复杂视图情况下提升渲染性能
    3. 实现跨平台
      1. 浏览器渲染DOM
      2. 服务端渲染SSRNuxt.js/Next.js
      3. 原生应用(Weex/React Native
      4. 小程序(mpvue/uni-app
  3. 虚拟DOM
    1. Snabbdom
      1. Vue2.x内部使用改造后的Snabbdom
      2. 源码体积小,只有200L
      3. 通过模块可扩展
      4. 源码使用ts开发
      5. 最快的Virtual DOM 之一
    2. virtual-dom:最早的虚拟DOM实现

Snabbdom的基本使用

  1. 安装:yarn add/npm install snabbdom

  2. 引入:通过import引入相关的模块

    import { init } from 'snabbdom/build/package/init'
    import { h } from 'snabbdom/build/package/h'
    

    官方实例写法如下:

    import { init } from 'snabbdom/init'
    import { h } from 'snabbdom/h'
    

    官方写法是因为使用了webpack5exports,设置了子路径映射,node12之后才支持。
    在这里插入图片描述

  3. inithpatch函数的使用

    1. init: 接受一个模块数组,并返回一个patch函数

      const patch = init([])
      
    2. h:创建虚拟DOM,接收三个参数,第一个是字符串类型的标签或者选择器,第二个参数是一个可选的选项对象,第三个参数是表示子元素,可以是一个字符串或一个数组,也是可选的。

      // 创建一个id为main的空div
      vnode = h('div#main')
      // 创建一个id为main的div,文本内容为Hello Vue
      vnode = h('div#main', 'Hello Vue')
      // 创建包含多个子元素的div
      vnode = h('div', [
        'Hello Vue',
        h('h1', 'children')
      ])
      
    3. patchinit函数执行后的返回,接收两个参数,第一个参数是要被替换的真实DOM或虚拟DOM,第二个参数是新的虚拟DOM

      const old = patch(container, vnode)
      patch(old, vnode)
      
  4. 模块

    1. 模块的作用

      1. Snabbdom的核心库不能处理DOM元素的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现
      2. Snabbdom中的模块可以用来扩展Snabbdom的功能
      3. Snabbdom的模块是通过注册全局的钩子函数来实现,这些钩子函数在虚拟DOM的生命周期会被执行
    2. 官方提供的模块

      1. attributes:设置DOM对象的属性,使用DOM的标准方法createAttribute()实现
      2. props:设置DOM对象的属性,只是是通过对象.的形式实现
      3. dataset:处理元素的自定义属性
      4. class:改变元素样式属性
      5. style:设置行内样式
      6. eventlisteners:添加事件监听。
    3. 模块的使用

      1. 导入:导入需要使用的模块
      2. 注册:在init函数中注册
      3. 使用:在h函数的第二个参数中传入选项数据来使用
      	import { init } from 'snabbdom/build/package/init'
      	import { h } from  'snabbdom/build/package/h'
      	// 1、导入模块
      	import { styleModule } from 'snabbdom/build/package/modules/style'
      	import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
      	// 2、注册
      	const pacth = init([
      	  styleModule,
      	  eventListenersModule,
      	])
      	
      	const container = document.getElementById('app')
      	
      	// 在h函数的第二个参数中传入选项数据
      	const vnode = h('div', [
      	  h('h1', { style: { color: 'red' } }, 'Style Test'),
      	  h('p', { on: { click: clickHandler } }, 'Click Test')
      	])
      	
      	function clickHandler() {
      	  console.log('click');
      	}
      	
      	pacth(container, vnode)
      	```
      
  5. Snabbdom源码

    1. h 函数 :h函数最早见于hyperscript,用于创建超文本。Snabbdom在此基础上对h函数进行改造,用于创建虚拟DOMvnode)对象。h函数内部使用重载来实现函数的多种参数情况的调用。处理函数参数,并调用vnode函数生成vnode对象。

    2. patch函数整体执行过程分析:

      1. patch(oldVnode, newVnode):把新节点中变化的内容渲染到真实DOM,并返回新节点作为下一次处理的旧节点。
      2. 对比新旧VNode,是否是相同节点,比较节点的keysel是否相同
      3. 如果不是相同节点,删除之前的内容,重新渲染newVnode
      4. 如果是相同的节点,newVnode中有text,再判断新旧节点的text是否相同,如果不同,直接更新文本内容。
      5. 如果newVnode中有children,在判断子节点是否有变化。··
    3. init(modules[, domApi])modules表示引用的模块,domApi接收传入的dom操作方法对象,默认为htmlDomApi,也可以传入其他平台处理dom的方法对象,也是虚拟DOM实现跨平台的基础。

      1. 首先定义了一个接收钩子回调函数的对象,用来接收各个钩子的回调函数。

        const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
        let i: number
        let j: number
        const cbs: ModuleHooks = {
          create: [],
          update: [],
          remove: [],
          destroy: [],
          pre: [],
          post: []
        }
        
      2. domApi进行处理,设置domApi的默认值。

        const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
        
      3. 遍历 modules,并将模块中的钩子函数保存到ModuleHooks中。

        // 对注册的模块进行遍历,并将模块中定义的钩子函数存入到保存钩子函数的对象中。
        for (i = 0; i < hooks.length; ++i) {
          cbs[hooks[i]] = []
          for (j = 0; j < modules.length; ++j) {
            const hook = modules[j][hooks[i]]
            if (hook !== undefined) {
              (cbs[hooks[i]] as any[]).push(hook)
            }
          }
        }
        
      4. 返回 patch 函数。

    4. patch(oldVnode, vnode)oldVnode可以是真实的DOM或者虚拟DOMvnode是要替换显示的虚拟DOM

      1. 定义变量,执行模块的pre钩子函数。

            let i: number, elm: Node, parent: Node
            const insertedVnodeQueue: VNodeQueue = []
            for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
        
      2. 判断oldVnode是否是Vnode节点,如果不是则将它转化为Vnode

           if (!isVnode(oldVnode)) {
             oldVnode = emptyNodeAt(oldVnode)
           }
        

        判断是否是Vnode只要判断对象中是否具有sel属性。

        function isVnode (vnode: any): vnode is VNode {
          return vnode.sel !== undefined
        }
        

        由真实DOM创建Vnode只需要拼接选择器,然后调用vnode函数创建Vnode对象即可。

          function emptyNodeAt (elm: Element) {
            const id = elm.id ? '#' + elm.id : ''
            const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
            return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
          }
        
      3. 判断转化后的oldVnodevnode是否是相同的Vnode

        1. 如果相同,则比较两个Vnode的差异,并将差异更新到视图。
        2. 如果不相同,则先根据vnode创建真实DOM,然后将创建的真实DOM通过oldVnode的父元素添加到oldVnode的后面,并删除oldVnode
        if (sameVnode(oldVnode, vnode)) {
          patchVnode(oldVnode, vnode, insertedVnodeQueue)
        } else {
          elm = oldVnode.elm!
          parent = api.parentNode(elm) as Node
        
          createElm(vnode, insertedVnodeQueue)
        
          if (parent !== null) {
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
            removeVnodes(parent, [oldVnode], 0, 0)
          }
        }
        
      4. 遍历更新到视图的Vnode,并执行用户传入的insert钩子函数(通过vnode.data.hook指定)。

        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
        }
        

      这里的属性名后面加上!的写法是typescript的语法,表示属性不是undefinednull

      1. 执行模块的post钩子函数。并返回vnode对象。

        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
        return vnode
        
      2. 这里有两种钩子函数,一种是模块提供的vnode的生命周期钩子函数,以及用户传入的生命周期钩子,类似Vue的生命周期钩子函数。

        1. vnode生命周期钩子函数是注册模块中提供的,然后保存在cbs对象中,包括['create', 'update', 'remove', 'destroy', 'pre', 'post']
        2. 用户传入的生命周期钩子函数是创建vnode对象时通过第二个参数data传入的。定义在data.hook中,包括initcreateinsertremove等。
    5. createElm(vnode, insertedVnodeQueue):根据虚拟DOM创建真实DOM,并将真实DOM对象保存在vnode对象的elm属性中。vnode是要创建为真实DOMVnode对象,insertedVnodeQueue保存用户传入了insert钩子的vnode对象。

      1. 首先定义变量,并执行用户传入的init钩子函数,这里执行用户传入的init钩子时可能修改data,所有需要对data进行重新赋值。

        let i: any
        let data = vnode.data
        // 判断并执行init钩子
        if (data !== undefined) {
          const init = data.hook?.init
          if (isDef(init)) {
            init(vnode)
            data = vnode.data//可能修改`data`,所有需要对`data`进行重新赋值
          }
        }
        
      2. 然后要根据vnode对象来创建真实的DOM节点,并保存到vnode.elm中。 这里按节点的类型分为三种情况,注释节点、元素节点以及文本节点。

        1. 注释节点,当sel属性为!时,认为要创建注释节点,然后需要处理一下vnode.text,没有定义时需要给一个空字符的默认值。然后通过createComment来创建一个注解节点。

          if (sel === '!') { // 判断是否是注释节点
            if (isUndef(vnode.text)) { // 是否有注释文本
              vnode.text = ''
            }
            vnode.elm = api.createComment(vnode.text!) // 创建注释DOM
          }
          
        2. 文本节点,当sel没有定义时,即为undefined,认为要创建一个文本节点。然后根据vnode.text创建文本节点即可。

          vnode.elm = api.createTextNode(vnode.text!)
          
        3. 元素节点,除了上述两种情况外,都认为要创建元素节点。

          1. 首先根据sel选择器解析出需要创建的元素标签名,以及idclass
          2. 再根据data.ns命名空间属性来判断是否要根据命名空间来创建节点,将idclass属性添加到节点上;
          3. 判断是否具有子元素或文本内容,如果存在子元素节点,递归遍历并创建子元素节点添加到当前节点,如果存在文本内容,则根据文本内容创建子节点并添加到当前节点。如果存在子元素就不存在文本内容,存在文本内容就不存在子元素,这两者是互斥的。
          4. 最后如果用户传入了insert钩子函数,则将vnode对象添加到insertedVnodeQueue中。
          if (sel !== undefined) { // 根据选择器创建真实DOM元素, div#conatienr.main.active
            // Parse selector
            // 创建DOM元素
            const hashIdx = sel.indexOf('#') // #字符所在下标
            const dotIdx = sel.indexOf('.', hashIdx) // 第一个 . 字符所在下标
            const hash = hashIdx > 0 ? hashIdx : sel.length
            const dot = dotIdx > 0 ? dotIdx : sel.length
            const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel // 解析标签名
            const elm = vnode.elm = isDef(data) && isDef(i = data.ns) // ns表示命名空间
              ? api.createElementNS(i, tag) // 如果有命名空间,根据命名空间创建DOM元素
              : api.createElement(tag)
          
            if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)) // 设置id属性
            if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')) // 设置class样式
            for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode) // 执行create钩子函数
            // 判断是否存在子节点,如果存在子节点,递归向本节点添加子节点创建的真实DOM
            if (is.array(children)) {
              for (i = 0; i < children.length; ++i) {
                const ch = children[i]
                if (ch != null) {
                  api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
                }
              }
            } else if (is.primitive(vnode.text)) { // 判断是否有文本内容,如果有文本内容,创建文本节点,并将其添加到当前节点
              api.appendChild(elm, api.createTextNode(vnode.text))
            }
            const hook = vnode.data!.hook
            if (isDef(hook)) {
              hook.create?.(emptyNode, vnode)
              if (hook.insert) {
                insertedVnodeQueue.push(vnode)
              }
            }
          }
          
      3. 返回vnode.elm

    6. removeVnodes (parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number):从父节点中删除子节点,parentElm父元素节点,是一个真实的DOM节点,vnodes是要删除的子节点的数组,startIdx是要删除的子节点的起始下标,endIdx是要删除的子节点的结束下标。

      1. startIdx 开始遍历每个要删除的子节点,对非空节点进行处理。

      2. 如果子节点是文本节点,则直接删除节点即可。

      3. 子节点为非文本节点时,在删除每个子节点之前,需要先执行当前节点及其子节点中用户传入的destroy钩子函数。

        // 执行虚拟DOM及其子元素的destroy钩子,和cbs中的destroy钩子
          function invokeDestroyHook (vnode: VNode) {
            const data = vnode.data
            if (data !== undefined) {
              data?.hook?.destroy?.(vnode)
              for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
              if (vnode.children !== undefined) {
                for (let j = 0; j < vnode.children.length; ++j) {
                  const child = vnode.children[j]
                  if (child != null && typeof child !== 'string') {
                    invokeDestroyHook(child)
                  }
                }
              }
            }
          }
        
      4. 然后执行模块的remove钩子函数以及用户传入的remove钩子函数,最后再移除节点元素。这里为了防止重复删除节点,将生成的删除节点的函数rm传入到remove钩子函数中,在remove钩子函数中调用rm删除节点。这样设计的原因是在remove钩子中可能存在异步任务(如动画),需要等待异步任务执行完毕之后才能移除节点。在rm函数内部使用闭包的原理记录模块中remove钩子的数量listeners,每次调用rm函数时,listeners会自减,当listeners为零时才会执行从真实DOM移除节点的操作。

        // 创建删除元素的方法
          function createRmCb (childElm: Node, listeners: number) {
            return function rmCb () {
              if (--listeners === 0) { // 判断remove钩子是否都执行完了,防止重复删除,模块的remove钩子中可能存在异步的任务,需要等待这些任务执行完成之后才能删除元素。
                const parent = api.parentNode(childElm) as Node
                api.removeChild(parent, childElm) // 删除元素
              }
            }
          }
        

      removeVnodes完整代码

      // 从父节点移除虚拟数组中指定范围的元素对应的真实DOM
        function removeVnodes (parentElm: Node,
          vnodes: VNode[],
          startIdx: number,
          endIdx: number): void {
          for (; startIdx <= endIdx; ++startIdx) {
            let listeners: number
            let rm: () => void
            const ch = vnodes[startIdx]
            if (ch != null) {
              if (isDef(ch.sel)) { // 元素节点和注释节点
                invokeDestroyHook(ch) // 执行destroy钩子
                listeners = cbs.remove.length + 1 // 记录remove钩子个数
                rm = createRmCb(ch.elm!, listeners)
                for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm) // 执行虚拟DOM remove钩子函数
                // 用户传入的 remove 钩子
                const removeHook = ch?.data?.hook?.remove
                if (isDef(removeHook)) {
                  removeHook(ch, rm) // 这里为什么不执行子节点用户传入的remove的钩子呢
                } else {
                  rm()
                }
              } else { // Text node
                api.removeChild(parentElm, ch.elm!)
              }
            }
          }
        }
      
    7. patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue):对比oldNodevnode,并更新差异部分视图。

      1. 首先执行用户传入的 prepatch 钩子函数。判断两个节点是否相同,如果不相同则执行模块和用户传入的update钩子函数。

        const hook = vnode.data?.hook
        hook?.prepatch?.(oldVnode, vnode)
        
        const elm = vnode.elm = oldVnode.elm!
        const oldCh = oldVnode.children as VNode[]
        const ch = vnode.children as VNode[]
        if (oldVnode === vnode) return
        // 执行模块及用户传入的update
        if (vnode.data !== undefined) {
          for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          vnode.data.hook?.update?.(oldVnode, vnode)
        }
        
      2. 开始比较新旧节点。根据vnode中是否具有文本内容来进行判断,文本内容和子元素节点时互斥的。

        1. 如果vnode中没有文本内容,再根据是否具有子元素节点来进行处理。结合oldVnode的子节点和文本内容是否存在有以下几种情况。

          1. vnode存在子元素节点,oldVnode也存在子元素节点,需要对比二者的子元素节点,更新差异部分。
          2. vnode存在子元素节点,oldVnode存在文本内容,设置文本内容为空,并添加vnode的子元素节点。
          3. vnode不存在子元素节点,oldVnode存在子元素节点,删除oldVnode的子元素节点。
          4. vnode不存在子元素节点,oldVnode存在文本内容,将文本内容设置为空。
          // 更新元素子节点
          if (isUndef(vnode.text)) { // 新节点中不存在文本节点
            if (isDef(oldCh) && isDef(ch)) { // 新旧节点都存在子节点,对比子节点
              if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
            } else if (isDef(ch)) {// 新节点中存在子节点,旧节点中存在文本节点
              if (isDef(oldVnode.text)) api.setTextContent(elm, '') // 先将文本节点内容设置为空
              addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 将新节点中的子节点添加到真实DOM中。
            } else if (isDef(oldCh)) { // 如果旧节点中存在子节点,移除旧节点的子节点
              removeVnodes(elm, oldCh, 0, oldCh.length - 1)
            } else if (isDef(oldVnode.text)) { // 如果旧节点中存在文本节点,将文本内容设置为空
              api.setTextContent(elm, '')
            }
          }
          
        2. 如果vnode中有文本内容,则判断一下oldVnode中是否有子元素节点,如果有的话需要移除oldVnode中的子元素节点,来触发节点移除相关的钩子函数。然后再设置文本内容为vnode的文本内容。

          else if (oldVnode.text !== vnode.text) {// 新旧节点文本内容不同,
            if (isDef(oldCh)) { // 如果旧节点中存在子元素节点,移除子元素节点
              removeVnodes(elm, oldCh, 0, oldCh.length - 1)
            }
            api.setTextContent(elm, vnode.text!) // 设置文本内容为新节点的文本内容。
          }
          
      3. 最后执行用户传入的postpatch钩子。

    8. function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue):更新子元素的差异部分。函数函数内部定义了四个指针变量,分别是指向oldCh开始位置的oldStartIdx以及结束位置的oldEndIdx,和指向newCh开始位置的newStartIdx以及结束位置的newEndIdx。然后通过判断指针指向的节点是否相同来进行相应的操作,有以下几种判断情况:

      1. oldStartIdxnewStartIdx指向的节点是相同节点,更新节点,oldStartIdxnewStartIdx向后移动一位,再次进行判断。
      2. oldEndIdxnewEndIdx指向的节点是相同节点,更新节点,oldEndIdxnewEndIdx向前移动一位,再次进行判断。
      3. oldStartIdxnewEndIdx指向的节点是相同节点,更新节点,将oldStartIdx位置的节点移动到旧子节点的最后位置, oldStartIdx向后移动一位,newEndIdx向前移动一位。然后再次重复上面的判断过程。
      4. oldEndIdxnewStartIdx指向的节点是相同节点,更新节点,将oldEndIdx位置的节点移动到旧子节点的最前面。oldEndIdx向前移动一位,newStartIdx向后移动一位。
      5. 如果上面的情况都不满足,则根据key来查找旧子节点的[oldStartIdx, oldEndIdx]范围内中有没有相同的key
        1. 如果能找到相同的key, 则根据sel属性来判断是否是同一节点,如果是同一节点,则更新节点,并将更新后的节点移动到newStartIdx之前的位置。 需要注意的是,更新节点只会更新子节点或文本内容,不会更新节点的状态。 如果不是同一节点,则直接在newStartIdx之前插入新节点。
        2. 如果找不到到相同的key,表示是一个新增的节点,则直接在newStartIdx之前插入新节点。
        3. 最后经过上述处理之后,newStartIdx向后移动一位。
      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
       if (oldStartVnode == null) { // 将oldStartIdx定位到第一个非空子节点
          oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
        } else if (oldEndVnode == null) {// 将oldEndIdx定位到最后一个非空子节点
          oldEndVnode = oldCh[--oldEndIdx]
        } else if (newStartVnode == null) {// 将newStartIdx定位到第一个非空子节点
          newStartVnode = newCh[++newStartIdx]
        } else if (newEndVnode == null) {// 将newEndIdx定位到最后一个非空子节点
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) { // 新旧子节点的开始节点是相同节点,则更新节点差异,并同时移动到下一个节点
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {// 新旧子节点的结束节点是相同节点,则更新节点差异,并同时移动到上一个节点
          patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
          oldEndVnode = oldCh[--oldEndIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) { // 旧子节点的开始节点和新子节点的结束节点相同,
          patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 在旧子节点的基础上,更新开始节点的内容,
          // 将旧子节点的开始节点移动到旧子节点的结束节点之后
          api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
          oldStartVnode = oldCh[++oldStartIdx] // 旧子节点开始指针向后移
          newEndVnode = newCh[--newEndIdx] // 新子节点结束指针向前移
        } else if (sameVnode(oldEndVnode, newStartVnode)) { // 旧子节点的结束节点和新子节点的开始节点相同,
          patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 在旧子节点的基础上,更新开始节点的内容,
          // 将旧子节点的结束节点移动到旧子节点的开始节点之前
          api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
          oldEndVnode = oldCh[--oldEndIdx]// 旧子节点结束指针向前移
          newStartVnode = newCh[++newStartIdx]// 新子节点开始指针向后移
        } else {
          if (oldKeyToIdx === undefined) { // 创建一个旧子节点的key和index映射关系的对象。
            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
          }
          idxInOld = oldKeyToIdx[newStartVnode.key as string] // 在旧子节点中定位新子节点开始节点的位置
          if (isUndef(idxInOld)) { // 新子节点开始节点是一个新增节点
            // 创建节点,并将节点插入到旧子节点开始节点之前
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {// 新子节点开始节点不是一个新增节点
            elmToMove = oldCh[idxInOld] // 定位到新子节点开始节点对应位置的节点
            if (elmToMove.sel !== newStartVnode.sel) { // 对比选择器,如果选择器不相同,创建新节点,并将节点插入到旧子节点开始节点之前
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
            } else { // 选择器相同,对比两个节点的差异,更新节点
              patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
              oldCh[idxInOld] = undefined as any // 将对应位置上的节点置空
              // 移动更新的节点到旧节点的开始位置
              api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
            }
          }
          newStartVnode = newCh[++newStartIdx]// 新子节点开始指针向后移
        }
      }
      

      循环进行上述判断,当oldCh遍历完成(oldStartIdx > oldEndIdx),或newCh遍历完成(newStartIdx > newEndIdx)时标志循环结束。这里又分为3中情况:

      1. oldStartIdx > oldEndIdxnewStartIdx > newEndIdx都成立,表示oldChnewCh都遍历完成,所以不需要进行其他处理。
      2. 只有oldStartIdx > oldEndIdx成立,表示newCh还没有处理完,[newStartIdx , newEndIdx]范围内节点需要添加到对应的位置,即前一个节点之前。
      3. 只有newStartIdx > newEndIdx成立,表示表示oldCh还没有处理完,需要移除[newStartIdx , newEndIdx]范围内节点。
      if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {// 循环处理结束
        if (oldStartIdx > oldEndIdx) { // 新子节点中还有新的节点需要添加
          before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else {// 旧子节点中存在多余节点
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
      }
      
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue的组件化实现原理基于Vue的核心概念——虚拟DOMVirtual DOM)。组件化是指将页面拆分为多个独立、可复用的组件,每个组件都有自己的模板、样式和行为。Vue通过提供组件系统来支持这种开发方式。 在Vue中,我们可以使用Vue.component()方法创建一个全局组件,或者使用components选项在一个父组件中注册子组件。 当组件被创建时,Vue会根据组件的模板生成一个虚拟DOM树。在数据变化时,Vue会对比新旧虚拟DOM树的差异,并通过最小化地修改真实DOM来更新页面。 组件的模板通常使用Vue的模板语法来描述,包括插值表达式、指令、事件绑定等。当组件被渲染时,模板中的表达式会被动态地计算和更新。 组件还可以定义自己的样式和行为。样式可以使用普通的CSS或CSS预处理器编写,并通过scoped属性限定作用域,确保样式只应用于当前组件。 组件之间可以通过props属性和自定义事件进行通信。props属性用于父组件向子组件传递数据,子组件可以通过props选项声明接收的数据类型和默认值。自定义事件则用于子组件向父组件发送消息,子组件可以通过$emit方法触发事件,并传递数据给父组件。 通过组件化开发,我们可以将页面拆分为多个独立的组件,使代码更加模块化、可复用和易于维护。同时,通过虚拟DOM的高效更新机制,Vue能够在数据变化时高效地更新页面,提升性能和用户体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值