Vue源码解析——组件更新过程

源码调试地址

https://github.com/KingComedy/vue-debugger

Vue初始化以及首次渲染过程

https://blog.csdn.net/comedyking/article/details/115551173

组件依赖收集过程

  • 在组件初始化时,实例化watcher,会触发一次组件的更新函数updateComponent,并且此时Dep.target设置为当前watcher
  • updateComponent执行vm._render时,即创建vnode过程中,需要获取到属性的值时,触发了属性的get,将当前watcher存入当前属性的dep中,完成依赖收集
  • 组件初始化创建了watcher,因此每个组件对应至少一个watcher(组件computed和watch也会生成watcher)
  • 依赖收集完成后,以后组件内属性触发更新时,就会通知所订阅的watcher,执行updateComponent(如果此时组件已经执行过mounted,执行updateComponent之前会先执行beforeUpdate生命周期)
  • 详细过程:https://blog.csdn.net/comedyking/article/details/115125904

Vue组件更新过程

  • 每个组件都有属于自己的更新函数updateComponent
  • 执行updateComponent => 最终执行patch函数(在src\core\vdom\patch.js定义)
  • 通过sameVnode判断当前是否是相同节点(组件更新只会对比相同节点),执行patchVnode对比相同节点(在src\core\vdom\patch.js中定义)
  • 执行patchVnode
    • 执行组件的prepatch钩子
    • 更新操作:
      • 如果新的是文本节点,并且新旧文本内容不同,执行文本节点更新setTextContent
      • 如果新的不是文本节点
        • 判断新旧节点 是否都有子节点
          • 都有子节点:执行updateChildren更新子节点
          • 新的有子节点,旧的没有,则执行批量插入
          • 新的没有子节点,旧的有,则执行批量删除
          • 都没有子节点,且旧的是文本节点,则设置文本内容为空
    • 执行组件的postpatch钩子

diff算法图解

  • 执行执行patchVnode,更新节点过程中,如果新旧的dom都有子节,则执行updateChildren更新子节点,即执行diff操作
<!-- list: [1,2,3,4,5,6,7] => [1,6,4,8,2,7] -->
<ul>
  <li v-for="item in list">{{item}}</li>
</ul>
  • 创建两组子节点数组的开头索引和结束索引,分别为newStartIndex,newEndIndex,oldStartIndex,oldEndIndex4个索引,通过索引获取数组中的节点进行对比
// 前后4个游标和4个对应节点
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]
  • 开始循环对比:(直到两个开始索引 都小于等于 两个结束索引,即oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
// 循环条件:新节点开始索引小于等于结束索引 && 旧节点开始索引小于等于结束索引 
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // ...diff对比
}

sameVnode对比是否相同节点,相同节点执行patchVnode操作

  • 判断旧的开始节点是否存在,不存在旧的开始索引向后移,继续循环,存在则继续执行下面判断
  • 判断旧的结束节点是否存在,不存在旧的结束节点向前移,继续循环,存在则继续执行下面判断
  • 判断新旧的开始节点是否相同, 相同则执行更新,且两个开始索引都向后移,即++oldStartIndex,++oldStartIndex, 不相同则继续执行下面判断

  • 判断新旧的结束节点是否相同, 相同则执行更新,且两个结束索引都向前移,即--oldEndIndex,--newEndIndex, 不相同则继续执行下面判断

  • 判断旧的开始节点和新的结束节点,相同则执行更新节点后,旧的开始索引后移,新的结束节点前移,即++oldStartIdx, --newEndIndex,并将更新后的旧的开头节点移动到数组末位

  • 判断旧的结束节点和新的结束节点,相同则执行更新节点后,旧的结束索引前移,新的开始节点后移,即--oldEndIdx, ++newStartIdx, 并将更新后的旧的结束节点移动到数组首位

  • 如果以上判断都不成立:
    • 取出新的开始节点的所设置的key 到 旧的节点数组找,如果没找到用开始节点的vnode执行createElm创建新的dom
    • 如果在旧节点中找到了相同的key节点,再用sameVnode对比是否是相同节点
      • 如果是相同节点,执行节点更新并且移动到旧的数组首位, 并且原来的位置设置为undefined
      •  

      • 如果不是则createElm创建新的dom,并插入到当前旧开始索引指向的dom节点的前面新节点索引向后移
      •  

    • 新节点索引向后移
  • 当前newStartIndex > newEndIndex, 循环结束
  • 循环结束后,先判断旧的开始索引是否大于旧的结束索引,即oldStartIdx > oldEndIdx,是即代表新的子节点数组长度大于旧的长度,则批量新增剩下未对比的新节点
  • 判断新的开始索引是否大于新的结束索引,即newStartIdx > newEndIdx,代表新的子节点数组长度小于旧的长度,则批量删除未对比的旧节点
  •  

sameVnode判断是否是相同节点

// sameVnode是通过key、tag还有一些属性判断是否是相同节点的。
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)
      )
    )
  )
}

v-for为什么要设置key

  • 如果不设置key,则节点的key都为undefined,节点对比时,key都相同,都为undefined,会导致错误判断为相同节点。
  • 如图:假设列表执行了插入操作,如[0,1,2,3] => [0,4,1,2,3]
  • 不设置key的情况:因为key都为undefined,所以在其他属性不变的情况下,错误的判断了节点相同,因为节点相同但文本内容不同,所以导致3次更新dom节点操作 + 1次插入操作
  •  

  • 设置key的情况:因为判断的相同节点且文本内容也相同,无需做dom更新操作,只需要一次插入操作即可以完成
  • 所以,设置key可以大概率的减少dom更新操作

总结

diff主要过程:

  • 创建4个索引,循环对比,索引往中间移动,当开始索引 > 结束索引 结束循环
  • 每次对比:先寻找相同节点,顺序为先对比新旧子节点的 首首节点 => 尾尾节点 => 旧首新尾 => 旧尾新首 => 没有就取出新的首节点 遍历 旧的节点数组 => 找到了做更新节点操作,找不到则执行插入
  • 当循环结束后,如果是旧的开始索引大于 旧的结束索引,则批量新增 新节点
  • 如果是新的开始索引大于 新的结束索引,则批量删除 旧节点

v-for要设置key,大概率可以减少dom的更新操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值