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