欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

vue 虚拟dom的patch源码分析

程序员文章站 2022-05-25 21:26:32
本文介绍了vue 虚拟dom的patch源码分析,分享给大家,具体如下: 源码目录:src/core/vdom/patch.js function upd...

本文介绍了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]
  let oldendvnode = oldch[oldendidx]
  let newendidx = newch.length - 1
  let newstartvnode = newch[0]
  let newendvnode = newch[newendidx]
  let oldkeytoidx, idxinold, vnodetomove, refelm

    const canmove = !removeonly

  while (oldstartidx <= oldendidx && newstartidx <= newendidx) { // 开始索引大于结束索引,进不了
   if (isundef(oldstartvnode)) {
    oldstartvnode = oldch[++oldstartidx] // vnode已经被移走了。
   } else if (isundef(oldendvnode)) {
    oldendvnode = oldch[--oldendidx]
   } else if (samevnode(oldstartvnode, newstartvnode)) {
    patchvnode(oldstartvnode, newstartvnode, insertedvnodequeue)
    oldstartvnode = oldch[++oldstartidx] // 索引加1。是去对比下一个节点。比如之前start=a[0],那现在start=a[1],改变start的值后再去对比start这个vnode
    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)
    canmove && nodeops.insertbefore(parentelm, oldstartvnode.elm, nodeops.nextsibling(oldendvnode.elm))// 把节点b移到树的最右边
    oldstartvnode = oldch[++oldstartidx]
    newendvnode = newch[--newendidx]
     
   } else if (samevnode(oldendvnode, newstartvnode)) {  old.end.d=new.start.d
    patchvnode(oldendvnode, newstartvnode, insertedvnodequeue)
    canmove && nodeops.insertbefore(parentelm, oldendvnode.elm, oldstartvnode.elm)// vnode moved left,把d移到c的左边。=old.start->old.end
    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)) { 
     createelm(newstartvnode, insertedvnodequeue, parentelm, oldstartvnode.elm) // 创建新节点,后面执行了nodeops.insertbefore(parent, elm, ref)
    } else {
     vnodetomove = oldch[idxinold]
     /* istanbul ignore if */
     if (process.env.node_env !== 'production' && !vnodetomove) {
      warn(
       'it seems there are duplicate keys that is causing an update error. ' +
       'make sure each v-for item has a unique key.'
      )
     }
     if (samevnode(vnodetomove, newstartvnode)) {
      patchvnode(vnodetomove, newstartvnode, insertedvnodequeue)
      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)
     }
    }
    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(parentelm, oldch, oldstartidx, oldendidx) // 删除旧的c,removenode(ch.elm)

  }
 }
function samevnode (a, b) {
 return (
  a.key === b.key && (
   (
    a.tag === b.tag &&
    a.iscomment === b.iscomment &&
    isdef(a.data) === isdef(b.data) &&
    sameinputtype(a, b)
   ) || (
    istrue(a.isasyncplaceholder) &&
    a.asyncfactory === b.asyncfactory &&
    isundef(b.asyncfactory.error)
   )
  )
 )
}

/**
   * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程
   * @param oldvnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updatechildren方法对子节点做更新,
   * @param vnode
   * @param insertedvnodequeue
   * @param removeonly
   */
 function patchvnode (oldvnode, vnode, insertedvnodequeue, removeonly) {
  if (oldvnode === vnode) {
   return
  }

  const elm = vnode.elm = oldvnode.elm

  if (istrue(oldvnode.isasyncplaceholder)) {
   if (isdef(vnode.asyncfactory.resolved)) {
    hydrate(oldvnode.elm, vnode, insertedvnodequeue)
   } else {
    vnode.isasyncplaceholder = true
   }
   return
  }

   // 用于静态树的重用元素。
    // 注意,如果vnode是克隆的,我们只做这个。
    // 如果新节点不是克隆的,则表示呈现函数。
    // 由热重加载api重新设置,我们需要进行适当的重新渲染。
  if (istrue(vnode.isstatic) &&
   istrue(oldvnode.isstatic) &&
   vnode.key === oldvnode.key &&
   (istrue(vnode.iscloned) || istrue(vnode.isonce))
  ) {
   vnode.componentinstance = oldvnode.componentinstance
   return
  }

  let i
  const data = vnode.data
  if (isdef(data) && isdef(i = data.hook) && isdef(i = i.prepatch)) {
   i(oldvnode, vnode)
  }

  const oldch = oldvnode.children
  const ch = vnode.children
  if (isdef(data) && ispatchable(vnode)) {
   for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldvnode, vnode)
   if (isdef(i = data.hook) && isdef(i = i.update)) i(oldvnode, vnode)
  }
  if (isundef(vnode.text)) {
   if (isdef(oldch) && isdef(ch)) {
    if (oldch !== ch) updatechildren(elm, oldch, ch, insertedvnodequeue, removeonly)
   } else if (isdef(ch)) {
    if (isdef(oldvnode.text)) nodeops.settextcontent(elm, '')
    addvnodes(elm, null, ch, 0, ch.length - 1, insertedvnodequeue)
   } else if (isdef(oldch)) {
    removevnodes(elm, oldch, 0, oldch.length - 1)
   } else if (isdef(oldvnode.text)) {
    nodeops.settextcontent(elm, '')
   }
  } else if (oldvnode.text !== vnode.text) {
   nodeops.settextcontent(elm, vnode.text)
  }
  if (isdef(data)) {
   if (isdef(i = data.hook) && isdef(i = i.postpatch)) i(oldvnode, vnode)
  }
 }

function insertbefore (parentnode, newnode, referencenode) {
 parentnode.insertbefore(newnode, referencenode);
}

/**
   *
   * @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createelm方法,
   * @param insertedvnodequeue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法
   * @param parentelm
   * @param refelm
   * @param nested
   */

   let inpre = 0
 function createelm (vnode, insertedvnodequeue, parentelm, refelm, nested) {
  vnode.isrootinsert = !nested // 过渡进入检查
  if (createcomponent(vnode, insertedvnodequeue, parentelm, refelm)) {
   return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isdef(tag)) {
   if (process.env.node_env !== 'production') {
    if (data && data.pre) {
     inpre++
    }
    if (
     !inpre &&
     !vnode.ns &&
     !(
      config.ignoredelements.length &&
      config.ignoredelements.some(ignore => {
       return isregexp(ignore)
        ? ignore.test(tag)
        : ignore === tag
      })
     ) &&
     config.isunknownelement(tag)
    ) {
     warn(
      'unknown custom element: <' + tag + '> - did you ' +
      'register the component correctly? for recursive components, ' +
      'make sure to provide the "name" option.',
      vnode.context
     )
    }
   }
   vnode.elm = vnode.ns
    ? nodeops.createelementns(vnode.ns, tag)
    : nodeops.createelement(tag, vnode)
   setscope(vnode)

   /* istanbul ignore if */
   if (__weex__) {
    // in weex, the default insertion order is parent-first.
    // list items can be optimized to use children-first insertion
    // with append="tree".
    const appendastree = isdef(data) && istrue(data.appendastree)
    if (!appendastree) {
     if (isdef(data)) {
      invokecreatehooks(vnode, insertedvnodequeue)
     }
     insert(parentelm, vnode.elm, refelm)
    }
    createchildren(vnode, children, insertedvnodequeue)
    if (appendastree) {
     if (isdef(data)) {
      invokecreatehooks(vnode, insertedvnodequeue)
     }
     insert(parentelm, vnode.elm, refelm)
    }
   } else {
    createchildren(vnode, children, insertedvnodequeue)
    if (isdef(data)) {
     invokecreatehooks(vnode, insertedvnodequeue)
    }
    insert(parentelm, vnode.elm, refelm)
   }

   if (process.env.node_env !== 'production' && data && data.pre) {
    inpre--
   }
  } else if (istrue(vnode.iscomment)) {
   vnode.elm = nodeops.createcomment(vnode.text)
   insert(parentelm, vnode.elm, refelm)
  } else {
   vnode.elm = nodeops.createtextnode(vnode.text)
   insert(parentelm, vnode.elm, refelm)
  }
 }
function insert (parent, elm, ref) {
  if (isdef(parent)) {
   if (isdef(ref)) {
    if (ref.parentnode === parent) {
     nodeops.insertbefore(parent, elm, ref)
    }
   } else {
    nodeops.appendchild(parent, elm)
   }
  }
 }

function removevnodes (parentelm, vnodes, startidx, endidx) {
  for (; startidx <= endidx; ++startidx) {
   const ch = vnodes[startidx]
   if (isdef(ch)) {
    if (isdef(ch.tag)) {
     removeandinvokeremovehook(ch)
     invokedestroyhook(ch)
    } else { // text node
     removenode(ch.elm)
    }
   }
  }
 }

updatechildren方法主要通过while循环去对比2棵树的子节点来更新dom,通过对比新的来改变旧的,以达到新旧统一的目的。

通过一个例子来模拟一下:

假设有新旧2棵树,树中的子节点分别为a,b,c,d等表示,不同的代号代表不同的vnode,如:

vue 虚拟dom的patch源码分析

在设置好状态后,我们开始第一遍比较,此时oldstartvnode=a,newstartvnode=a;命中了samevnode(oldstartvnode,newstartvnode)逻辑,则直接调用patchvnode(oldstartvnode,newstartvnode,insertedvnodequeue)方法更新节点a,接着把oldstartidxnewstartidx索引分别+1,如图:

vue 虚拟dom的patch源码分析

更新完节点a后,我们开始第2遍比较,此时oldstartvnode=b,newendvnode=b;命中了samevnode(oldstartvnode,newendvnode)逻辑,则调用patchvnode(oldstartvnode, newendvnode, insertedvnodequeue)方法更新节点b,接着调用canmove && nodeops.insertbefore(parentelm, oldstartvnode.elm, nodeops.nextsibling(oldendvnode.elm)),把节点b移到树的最右边,最后把oldstartidx索引+1,newendidx索引-1,如图:

vue 虚拟dom的patch源码分析

更新完节点b后,我们开始第三遍比较,此时oldendvnode=d,newstartvnode=d;命中了samevnode(oldendvnode, newstartvnode)逻辑,则调用patchvnode(oldendvnode, newstartvnode, insertedvnodequeue)方法更新节点d,接着调用canmove && nodeops.insertbefore(parentelm, oldendvnode.elm, oldstartvnode.elm),把d移到c的左边。最后把oldendidx索引-1,newstartidx索引+1,如图:

vue 虚拟dom的patch源码分析

更新完d后,我们开始第4遍比较,此时newstartvnode=e,节点e在旧树里是没有的,因此应该被作为一个新的元素插入,调用createelm(newstartvnode, insertedvnodequeue, parentelm, oldstartvnode.elm),后面执行了nodeops.insertbefore(parent, elm, ref)方法把e插入到c之前,接着把newstartidx索引+1,如图:

vue 虚拟dom的patch源码分析

插入节点e后,我们可以看到newstartidx已经大于newendidx了,while循环已经完毕。接着调用removevnodes(parentelm, oldch, oldstartidx, oldendidx) 删除旧的c,最终如图:

vue 虚拟dom的patch源码分析

updatechildren通过以上几步操作完成了旧树子节点的更新,实际上只用了比较小的dom操作,在性能上有所提升,并且当子节点越复杂,这种提升效果越明显。vnode通过patch方法生成dom后,会调用mounted hook,至此,整个vue实例就创建完成了,当这个vue实例的watcher观察到数据变化时,会两次调用render方法生成新的vnode,接着调用patch方法对比新旧vnode来更新dom.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。