副作用渲染函数更新组件
1,初次挂载组件的时候会对该组件创建一个带副作用的渲染函数
2,当组件值有变化的时候,会执行到processComponent,因为不是初次渲染,会执行updateComponent
3,shouldUpdateComponent判断是否需要更新子组件,需要的话会调用instance.update,也就是初始挂载的带副作用的渲染函数
4,带副作用渲染函数:是否有新的组件vnode,有的话标识有变化,更新组件vnode的节点信息,然后renderComponentRoot生成新的子树vnode,patch 对比新旧子树vnode
5,patch过程中又会判断对各种类型的节点进行处理。
6,如果是element,更新--patchElement,更新props/children
7,patchChildren子节点有3中情况:文本,数组,空
文本-->文本:替换新文本
文本-->空: 删除旧子节点
文本-->vnode数组 :清空文本添加多个新子节点
空-->纯文本 : 添加新文本节点
空-->空 : 不做什么
空-->vnode数组: 添加多个新子节点
vnode数组 --> 纯文本 : 删除旧子节点,添加新文本节点
vnode数组 --> 空 : 删除旧子节点
vnode数组 --> vnode数组 : 完成的diff子节点(核心diff算法)
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件
}
else {
// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
}
else {
next = vnode
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树 vnode
instance.subTree = nextTree
// 组件更新核心逻辑,根据新旧子树 vnode 做 patch
patch(prevTree, nextTree,
// 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
hostParentNode(prevTree.el),
// 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG)
// 缓存更新后的 DOM 节点
next.el = nextTree.el
}
}, prodEffectOptions)
}
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 挂载组件
}
else {
// 更新子组件
updateComponent(n1, n2, parentComponent, optimized)
}
}
const updateComponent = (n1, n2, parentComponent, optimized) => {
const instance = (n2.component = n1.component)
// 根据新旧子组件 vnode 判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数
instance.update()
}
else {
// 不需要更新,只复制属性
n2.component = n1.component
n2.el = n1.el
}
}
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
// n1 设置为 null 保证后续都走 mount 逻辑
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
function isSameVNodeType (n1, n2) {
// n1 和 n2 节点的 type 和 key 都相同,才是相同节点
return n1.type === n2.type && n1.key === n2.key
}
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
// 挂载元素
}
else {
// 更新元素
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => {
const el = (n2.el = n1.el)
const oldProps = (n1 && n1.props) || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 更新 props
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 更新子节点
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}
核心diff
新旧节点的操作:更新、删除、添加、移动
更新:首首尾尾对比key和type相同,patch更新。
删除:达到边界条件,旧的vnode有剩余,删除
添加:达到边界条件,新的vnode有剩余,添加
移动:判断节点是否有移动,key和index的映射map,统计新节点对应的老节点索引数组。如果不是递增,需要移动。创建最大递增子序列,移动节点。
vue3的核心diff算法是从两边往中间移动,边界处理,有剩余是做新建还是删除操作。
如果没有达到边界情况,对于新的vnodes创建key和index的映射关系,找对应的key是否在老的vnode中,存在的话记录之前的索引值,最后统计出一个数组,记录中新节点位置对应的老节点的索引。创建最大递增子序列,移动节点。