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

vue的diff算法知识点总结

程序员文章站 2022-05-08 17:14:32
源码: 虚拟dom diff算法首先要明确一个概念就是diff的对象是虚拟dom,更新真实dom则是diff算法的结果 vnode基类 constr...

源码:

虚拟dom

diff算法首先要明确一个概念就是diff的对象是虚拟dom,更新真实dom则是diff算法的结果

vnode基类

 constructor (
  。。。
 ) {
  this.tag = tag
  this.data = data
  this.children = children
  this.text = text
  this.elm = elm
  this.ns = undefined
  this.context = context
  this.fncontext = undefined
  this.fnoptions = undefined
  this.fnscopeid = undefined
  this.key = data && data.key
  this.componentoptions = componentoptions
  this.componentinstance = undefined
  this.parent = undefined
  this.raw = false
  this.isstatic = false
  this.isrootinsert = true
  this.iscomment = false
  this.iscloned = false
  this.isonce = false
  this.asyncfactory = asyncfactory
  this.asyncmeta = undefined
  this.isasyncplaceholder = false
 }

这个部分的代码 主要是为了更好地知道在diff算法中具体diff的属性的含义,当然也可以更好地了解vnode实例

整体过程

核心函数是patch函数

  • isundef判断(是不是undefined或者null)
  • // empty mount (likely as component), create new root elementcreateelm(vnode, insertedvnodequeue) 这里可以发现创建节点不是一个一个插入,而是放入一个队列中统一批处理
  • 核心函数samevnode
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)
   )
  )
 )
}

这里是一个外层的比较函数,直接去比较了两个节点的key,tag(标签),data的比较(注意这里的data指的是vnodedata),input的话直接比较type。

export interface vnodedata {
 key?: string | number;
 slot?: string;
 scopedslots?: { [key: string]: scopedslot };
 ref?: string;
 tag?: string;
 staticclass?: string;
 class?: any;
 staticstyle?: { [key: string]: any };
 style?: object[] | object;
 props?: { [key: string]: any };
 attrs?: { [key: string]: any };
 domprops?: { [key: string]: any };
 hook?: { [key: string]: function };
 on?: { [key: string]: function | function[] };
 nativeon?: { [key: string]: function | function[] };
 transition?: object;
 show?: boolean;
 inlinetemplate?: {
  render: function;
  staticrenderfns: function[];
 };
 directives?: vnodedirective[];
 keepalive?: boolean;
}

这会确认两个节点是否有进一步比较的价值,不然直接替换

替换的过程主要是一个createelm函数 另外则是销毁oldvnode

// destroy old node
    if (isdef(parentelm)) {
     removevnodes(parentelm, [oldvnode], 0, 0)
    } else if (isdef(oldvnode.tag)) {
     invokedestroyhook(oldvnode)
    }

插入过程简化来说就是判断node的type分别调用

createcomponent(会判断是否有children然后递归调用)

createcomment

createtextnode

创建后使用insert函数

之后需要用hydrate函数将虚拟dom和真是dom进行映射

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 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
  }

  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)
  }
 }

const el = vnode.el = oldvnode.el 这是很重要的一步,让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。

  1. 比较二者引用是否一致
  2. 之后asyncfactory不知道是做什么的,所以这个比较看不懂
  3. 静态节点比较key,相同后也不做重新渲染,直接拷贝componentinstance(once命令在此生效)
  4. 如果vnode是文本节点或注释节点,但是vnode.text != oldvnode.text时,只需要更新vnode.elm的文本内容就可以
  5. children的比较
  • 如果只有oldvnode有子节点,那就把这些节点都删除
  • 如果只有vnode有子节点,那就创建这些子节点,这里如果oldvnode是个文本节点就把vnode.elm的文本设置为空字符串
  • 都有则updatechildren,这个之后详述
  • 如果oldvnode和vnode都没有子节点,但是oldvnode是文本节点或注释节点,就把vnode.elm的文本设置为空字符串

updatechildren

这部分重点还是关注整个算法

首先四个指针,oldstart,oldend,newstart,newend,两个数组,oldvnode,vnode。

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

  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)
    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)) { // vnode moved right
    patchvnode(oldstartvnode, newendvnode, insertedvnodequeue)
    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)
    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)
      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(parentelm, oldch, oldstartidx, oldendidx)
  }
 }

一个循环比较的几种情况和处理(以下的++ --均指index的++ --)比较则是比较的node节点,简略写法 不严谨 比较用的是samevnode函数也不是真的全等

整体循环不结束的条件oldstartidx <= oldendidx && newstartidx <= newendidx

  1. oldstart === newstart,oldstart++ newstart++
  2. oldend === newend,oldend-- newend--
  3. oldstart === newend, oldstart插到队伍末尾 oldstart++ newend--
  4. oldend === newstart, oldend插到队伍开头 oldend-- newstart++
  5. 剩下的所有情况都走这个处理简单的说也就两种处理,处理后newstart++
  • newstart在old中发现一样的那么将这个移动到oldstart前
  • 没有发现一样的那么创建一个放到oldstart之前

循环结束后并没有完成

还有一段判断才算完

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)
  }

简单的说就是循环结束后,看四个指针中间的内容,old数组中和new数组中,多退少补而已

总结

整体认识还很粗糙,不过以目前的水平和对vue的了解也就只能到这了