最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue2 源码系列之虚拟DOM

    正文概述 掘金(余居居)   2021-01-27   537

    什么是虚拟DOM?

    虚拟dom即Virtual DOM 本质上是一个js对象,用来描述真实的dom节点,vue中实现了一个vnode类,用来表示虚拟节点。
    源码路径:\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 
       /* 包含当前节点的一些数据信息,类型是vnodeData,详情参考vnodeData中的数据信息 */
       this.data = data
       /* 当前节点的子节点,是个数组 */
       this.children = children
       /* 当前节点的文本 */
       this.text = text
       /* 当前虚拟节点的真实dom节点 */
       this.elm = elm
       /* 当前节点的命名空间 */
       this.ns = undefined
       /* 编译作用域 */
       this.context = context
       /* 函数组件作用域 */
       this.fnContext = undefined
       /* 函数组件的option选项 */
       this.fnOptions = undefined
       this.fnScopeId = undefined
       /* 节点的key属性,作为节点的标识,patch时用来优化 */
       this.key = data && data.key
       /* 组件的option选项 */
       this.componentOptions = componentOptions
       /* 当前节点对应的组件实例 */
       this.componentInstance = undefined
       /* 当前节点的父节点 */
       this.parent = undefined
       /*是否为原生HTML或普通文本,innerHTML的时候为true,textContent的时候为false*/
       this.raw = false
       /*静态节点的标志*/
       this.isStatic = false
       /*是否作为根节点插入*/
       this.isRootInsert = true
       /*是否是注释节点*/
       this.isComment = false
       /*是否是克隆节点*/
       this.isCloned = false
       /*是否有v-once指令*/
       this.isOnce = false
       this.asyncFactory = asyncFactory
       this.asyncMeta = undefined
       this.isAsyncPlaceholder = false
     }
    
     // DEPRECATED: alias for componentInstance for backwards compat.
     /* istanbul ignore next */
     get child (): Component | void {
       return this.componentInstance
     }
    }
    

    这里我移除了一些源码对参数类型的校验。

    为什么使用虚拟DOM?

    因为DOM操作的执行速度远不如JavaScript的运算速度快,所以将大量的DOM操作转变为JavaScript运算,使用diff算法来计算出真正需要更新的节点,最大限度的减少DOM操作,从而达到性能的提升。虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM,所以这里我们主要来看vue中patch的过程。

    patch过程

    patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。对比两个vnode之间的差异只是patch的一部分。patch的目的其实是修改DOM节点,也可以理解为渲染视图。vue中主要通过以下三个方法实现:pathch、pa,chVnode和updateChildren ,其中我们常提到的diff过程在updateChildren方法里,现在我们重点看一下这三个方法。
    源码路径:\src\core\vdom\patch.js

    patch方法

    这里先来看一下patch方法的过程。
    源码比较多,这里我就不贴了,有兴趣的小伙伴自己clone一份源码结合着看一看。 Vue2 源码系列之虚拟DOM 上面流程图提到当oldVnode和vnode“相同”时,会调用patchVnode进行详细对比,这里顺带提一嘴,vue中判断两个vnode是否“相同”,调用的是sameVnode方法,该方法会去比较两个vnode的key是否相等并且tag是否相等、和其他判断条件。这也是为什么v-for渲染元素列表时,需要给一个唯一值key。

    patchVnode方法

    这是patchVnode方法的整体流程,建议结合源码使用更佳。 Vue2 源码系列之虚拟DOM 上图提到当oldVnode和vnode都存在子节点且不相等时,调用updateChildren方法更新子节点,敲黑板,到这里才是真正的重点。

    updateChildren方法

    我们来看下updateChildren方法,虽然看起来挺唬人的,但是建议静下心来过一遍,再结合后面的图看一下,相信聪明的你是能够理解的。

     function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0
        let 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, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(newCh)
        }
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }
    

    我们可以看到程序一开始会在oldChildren、newChildren的头尾各创建一个指针,然后开始遍历,而整个循环的条件是oldStartIdx >= oldEndIdx并且newStartIdx >= newEndIdx,如果有一个条件不满足,说明有一个数组已经遍历完了,循环体内则进行各种情况的判断。 Vue2 源码系列之虚拟DOM

    1、旧头和新头相同时

    Vue2 源码系列之虚拟DOM 当旧头和新头相同时,递归调用patchVnode更新节点,然后将oldStartIdx、newStartIdx向后移,节点位置不需要移动。

    2、旧尾和新尾相同时

    Vue2 源码系列之虚拟DOM 当旧尾和旧头相同时,递归调用patchVnode更新节点,然后将oldEndIdx、newEndIdx向前移,节点位置不需要移动。

    3、旧头和新尾相同时

    Vue2 源码系列之虚拟DOM 当旧头和新尾相同时,递归调用patchVnode更新节点,然后再调用insertBefore钩子将oldStartVnode.elm(对应的真实DOM节点)移动到oldEndVnode.elm的后面,web平台对应方法是insertBefore。最后将oldStartIdx后移一位、newEndIdx前移一位。

    4、旧尾和新头相同时

    Vue2 源码系列之虚拟DOM 当旧尾和新头相同时,递归调用patchVnode更新节点,然后再调用insertBefore钩子将oldEndVnode.elm移动到oldStartVnode.elm的前面,最后将oldEndIdx前移一位、newStartIdx后移一位。

    5、不满足以上条件的

    Vue2 源码系列之虚拟DOM 当出现这种不满足以上条件的,会用oldChildren创建一个哈希表,其中vnode的key作为键,下标作为值。如果newStartVnode.key存在就会在哈希表中查找,不存在的话就遍历oldChildren查找。如果没有找到对应的,则用newStartVnode创建一个新的真实节点放到oldStartVnode.elm的前面,找到对应的则调用sameVnode判断新旧是否相同,相同调用patchVnode更新节点并移动该节点到oldStartVnode.elm的前面,不相同则新建节点。最后将newStartIdx后移一位。

    到这里我们的循环体内的判断结束了,但是如果oldChildren和newChildren长度不一样,则其中一个循环结束了,另一个还会有剩余节点。所以接下来我们要对这部分进行判断。

    如果oldChildren先循环结束,newChildren还有剩余节点,此时oldStartIdx必然大于oldEndIdx,我们将newStartIdx于newEndIdx之间的vnode创建真实节点添加到视图中即可。同理,当newChildren先循环结束,oldChildren还有剩余节点时 ,此时newStartIdx必然大于newEndIdx,我们移除掉oldStartIdx与oldEndIdx之间的节点即可。

    为什么要设置key属性以及为什么不建议用数组的index作为key?

    因为key会作为节点的唯一id,更新子节点时,如果不存在key会遍历查找oldChildren,如果设置了key则会去哈希表中查找,会提升一定的性能。那为什么不建议使用index作为key呢?这里我们要去看sameVnode方法,它在判断两个vnode是否相同时主要就是去比较key和tag。当我们使用index作为key时,例如:oldChildrenKeyArr: 0 , 1 , 2 , 3 如果我们删除了数组的第一项,按道理来说我们只需要移除第一个节点就可以了。但是实际上删除之后是这样的newChildrenKeyArr: 0 , 1 , 2 再调用updateChildren更新子节点,遍历结束反而删除的是最后一项,造成性能的浪费。如果使用id(唯一值)就不会出现这种情况。

    写在最后

    至此,vue的虚拟DOM就结束了,希望大家从这篇文章中能有所收获。
    灵魂拷问: 你学废了吗?(手动狗头)


    起源地下载网 » Vue2 源码系列之虚拟DOM

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元