vue的diff算法知识点总结


Posted in Javascript onMarch 29, 2018

源码:https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js

虚拟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的了解也就只能到这了

Javascript 相关文章推荐
Jquery插件写法笔记整理
Sep 06 Javascript
JavaScript创建一个欢迎cookie弹出窗实现代码
Mar 15 Javascript
原生JS和JQuery动态添加、删除表格行的方法
May 28 Javascript
Js类的静态方法与实例方法区分及jQuery拓展的两种方法
Jun 03 Javascript
javascript实现随机生成DIV背景色
Jun 20 Javascript
js实现hashtable的赋值、取值、遍历操作实例详解
Dec 25 Javascript
jQuery实现鼠标响应式淘宝动画效果示例
Feb 13 jQuery
常用的 JS 排序算法 整理版
Apr 05 Javascript
js运算符的一些特殊用法
Jul 29 Javascript
基于Webpack4和React hooks搭建项目的方法
Feb 05 Javascript
微信小程序实现上传多张图片、删除图片
Jul 29 Javascript
vue通过接口直接下载java生成好的Excel表格案例
Oct 26 Javascript
vue文件树组件使用详解
Mar 29 #Javascript
vue全局组件与局部组件使用方法详解
Mar 29 #Javascript
javascript实现文件拖拽事件
Mar 29 #Javascript
vue 父组件调用子组件方法及事件
Mar 29 #Javascript
vue.js element-ui tree树形控件改iview的方法
Mar 29 #Javascript
Vue 源码分析之 Observer实现过程
Mar 29 #Javascript
vue 实现全选全不选的示例代码
Mar 29 #Javascript
You might like
SAE实时日志接口SDK用法示例
2016/10/09 PHP
PHP实现通过二维数组键值获取一维键名操作示例
2019/10/11 PHP
Javascript 入门基础学习
2010/03/10 Javascript
Jquery插件 easyUI属性汇总
2011/01/19 Javascript
30个最好的jQuery 灯箱插件分享
2011/04/25 Javascript
新浪微博字数统计 textarea字数统计实现代码
2011/08/28 Javascript
dojo学习第二天 ajax异步请求之绑定列表
2011/08/29 Javascript
DOM2非标准但却支持很好的几个属性小结
2012/01/21 Javascript
js判断字符长度以及中英文数字等
2013/12/31 Javascript
js中生成map对象的方法
2014/01/09 Javascript
js动态移动滚动条至底部示例代码
2014/04/24 Javascript
jQuery+ajax实现鼠标单击修改内容的方法
2014/06/27 Javascript
基于js实现投票的实例代码
2015/08/04 Javascript
JS面向对象编程详解
2016/03/06 Javascript
AngularJS基础 ng-mouseleave 指令详解
2016/08/02 Javascript
Angularjs按需查询实例代码
2017/10/30 Javascript
vue项目中使用lib-flexible解决移动端适配的问题解决
2018/08/23 Javascript
Vue 路由切换时页面内容没有重新加载的解决方法
2018/09/01 Javascript
jQuery判断自定义属性data-val用法示例
2019/01/07 jQuery
微信小程序人脸识别功能代码实例
2019/05/07 Javascript
一篇文章带你使用Typescript封装一个Vue组件(简单易懂)
2020/06/05 Javascript
[02:19]DOTA2女子战队FOX视频专访:希望更多美眉一起加入
2013/10/15 DOTA
python输入错误密码用户锁定实现方法
2017/11/27 Python
django2用iframe标签完成网页内嵌播放b站视频功能
2018/06/20 Python
python实现将读入的多维list转为一维list的方法
2018/06/28 Python
python——全排列数的生成方式
2020/02/26 Python
pycharm第三方库安装失败的问题及解决经验分享
2020/05/09 Python
tensorflow模型转ncnn的操作方式
2020/05/25 Python
Python使用requests模块爬取百度翻译
2020/08/25 Python
canvas仿写贝塞尔曲线的示例代码
2017/12/29 HTML / CSS
网站美工岗位职责
2014/04/02 职场文书
感谢信怎么写
2015/01/21 职场文书
写给领导的感谢信
2015/01/22 职场文书
python 定义函数 返回值只取其中一个的实现
2021/05/21 Python
Win11查看设备管理器
2022/04/19 数码科技
springcloud整合seata
2022/05/20 Java/Android