当我们写出这样的代码之后,我们肯定不希望组件执行四次重新渲染,而是执行一次渲染。那就应该降低更新频率,对 effect 去重
组件多次更新属性只渲染一次
const setupRenderEffect = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
// 每个组件都有一个effect, vue3 是组件及更新,数据变化会重新执行对应组件的effect
instance.update = effect(function componentEffect() {
// 如果没有被挂载,就是初次渲染
if (!instance.isMounted) {
let vnodeHook
const { el, props } = initialVNode
const { bm, m, parent } = instance
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
if (el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
const hydrateSubTree = () => {
if (__DEV__) {
startMeasure(instance, `render`)
}
instance.subTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `hydrate`)
}
hydrateNode!(
el as Node,
instance.subTree,
instance,
parentSuspense,
null
)
if (__DEV__) {
endMeasure(instance, `hydrate`)
}
}
if (isAsyncWrapper(initialVNode)) {
(initialVNode.type).__asyncLoader!().then(
// note: we are moving the render call into an async callback,
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
() => !instance.isUnmounted && hydrateSubTree()
)
} else {
hydrateSubTree()
}
} else {
if (__DEV__) {
startMeasure(instance, `render`)
}
const subTree = (instance.subTree = renderComponentRoot(instance))
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `patch`)
}
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense)
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:activated'),
parentSuspense
)
}
}
instance.isMounted = true
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// 更新逻辑
}
}, scheduler.queueJob)
}
组件更新会走 scheduler.queueJob
export function queueJob(job) {
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
const pos = findInsertionIndex(job)
if (pos > -1) {
queue.splice(pos, 0, job)
} else {
// 存在队列里
queue.push(job)
}
// 执行队列
queueFlush()
}
}
// 执行一次就好
function queueFlush() {
// 如果不是正在更新中
if (!isFlushing && !isFlushPending) {
// 标识正在刷新
isFlushPending = true
// 等当前任务执行完毕,清空队列
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
const getId = (job) =>
job.id == null ? Infinity : job.id
// 清空任务
function flushJobs(seen) {
isFlushPending = false
isFlushing = true
flushPreFlushCbs(seen)
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 清空时 需要根据调用顺序 依次刷新, 先父后子
queue.sort((a, b) => getId(a) - getId(b))
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
jbo();
}
} finally {
flushIndex = 0
queue.length = 0 // 清空
flushPostFlushCbs(seen)
isFlushing = false
}
}
将当前effect存放到队列里,没存放的时候刷新队列,但是队列只能执行一次。。。这里没太懂,
默认两个元素比较
// 创建一个 effect 让 render 执行
const setupRenderEffect = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
// 没有被挂载就是初次渲染
if (!instance.isMounted) {
let proxyToUse = instance.proxy;
// 执行render 他的返回值是 h 函数的执行结果, 就是组件的渲染内容
/**
* setup(props,context){
* return (proxy) => {
* return h('div', {}, 'hello world')
* }
* }
*/
let subTree = instance.subTree = instance.render.call(proxyToUse,proxyToUse);
// 用render 函数的返回值 继续渲染子树
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null
} else {
// 更新组件
let proxyToUse = instance.proxy;
// 新树
const nextTree = instance.subTree = instance.render.call(proxyToUse,proxyToUse);
// 旧树
const prevTree = instance.subTree
instance.subTree = nextTree
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
// hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
}
更新的时候将组件返回的新旧两个 vnode tree 传入 patch 进行对比。会走元素的更新,因为组件执行返回的是我这个组件下有哪些 元素。
元素的更新
元素的更新依旧会走 patch
const patch = (
n1, // 老的虚拟节点
n2, // 新的虚拟节点
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) => {
// 如果类型不一致直接销毁老的节点树
// 如果 两棵树的 type 不同,既不是相同类型的节点就删掉的老的节点。 例如 老的 div -》 新的 span
if (n1 && !isSameVNodeType(n1, n2)) {
/**
* 删除老的节点之前需要获取到新的节点要插入到哪个节点的前面,就是获取到老的节点的下一个兄弟节点, 调用DOM API node.nextSibling获取, 如下:
* <div>
* <a href=""></a>
* <p></p>
* </div>
* <div>
* <span href=""></span>
* <p></p>
* </div>
*/
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
// 会挂载 n2 对应的内容,将anchor 传入
n1 = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// 元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件
processComponent( // 处理组件
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
新旧两个虚拟节点传入,首先判断类型是否一致,不一致就直接销毁老的节点,挂载新的节点。
挂载新节点需要知道将他插入到哪个位置,就需要获取老节点的下一个兄弟节点,将它插入到 这个兄弟节点之前。
const getNextHostNode: NextFn = vnode => {
return hostNextSibling((vnode.anchor || vnode.el)!)
}
hostNextSibling 调用的是 DOM API node.nextSibling获取的
销毁节点就是将老的元素节点从 页面中 删除,调用 dom API parent.removeChild(child) ,如果销毁的是组件,还需要考虑组件的生命周期。
然后就走到了 processElement,
如果是元素类型不同,就会走初次渲染 mountElement,调用 DOM API parent.insertBefore(child, anchor || null),将它插入到参考节点的前面,
如果元素标签类型相同就会节点复用,需要对比,走 patchElement 。
// 处理元素的渲染
const processElement = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
) => {
isSVG = isSVG || (n2.type) === 'svg'
// 初次渲染
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
patchElement 就是对两颗树的对比。对比属性和儿子。
const patchElement = (
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
) => {
// 节点复用
const el = (n2.el = n1.el)
// 拿到元素之后 更新属性 更新儿子
let { patchFlag, dynamicChildren, dirs } = n2
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
// 老的属性
const oldProps = n1.props || EMPTY_OBJ
// 新的属性
const newProps = n2.props || EMPTY_OBJ
let vnodeHook
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps(
el, // 更新哪个元素
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (
next !== prev ||
(hostForcePatchProp && hostForcePatchProp(el, key))
) {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) {
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
}
1.新老属性对比。
const patchProps = (
el,
vnode,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
) => {
// 如果新的老的不一样 就对比
if (oldProps !== newProps) {
// 循环用新的覆盖掉老的
for (const key in newProps) {
const next = newProps[key]
const prev = oldProps[key]
if (
next !== prev ||
(hostForcePatchProp && hostForcePatchProp(el, key))
) {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
vnode.children,
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if (oldProps !== EMPTY_OBJ) {
// 老的有 新的没有 就删掉
for (const key in oldProps) {
if (!isReservedProp(key) && !(key in newProps)) {
hostPatchProp(
el,
key,
oldProps[key],
null, // 新的 为 null 就删除
isSVG,
vnode.children,
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
}
如果新的老的属性不一致就覆盖。
如果老的有,新的没有就 删除。
2.新老儿子对比
/**
* 老的有儿子 新的没有儿子
* 新的有儿子 老的没有儿子
* 新老都有儿子 新老都是文本
*/
const patchChildren = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1,
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1,
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// 新的是文本,不管老的是啥
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 老的是 n 个孩子,但是新的是文本,需要卸载
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 , parentComponent, parentSuspense) // 如果c1 中包含组件会调用组件的销毁方法
}
// 新老都是文本的情况 就设置新的文本
if (c2 !== c1) {
hostSetElementText(container, c2)
}
} else {
/**
* 老:h('div', {style: {color: 'red'}}, [h('span', 'hello'), h('span', 'hello')])
* 新:h('div', {style: {color: 'red'}}, h('span', 'hello'))
*/
// 上一次是 数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 现在是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 当前是数组 之前是数组 diff算法 ***************************************
patchKeyedChildren(
c1,
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 当前没有孩子 特殊情况 当前是 null 删除老的
unmountChildren(c1 , parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null 老的是文本
// new children is array OR null 新的是数组
// 把文本清空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array 挂载新的数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
1.如果新的是文本,老的也是文本,就给元素设置新文本。el.textContent = text
2.如果新的文本,老的是数组,就需要卸载,卸载的时候将所有孩子传入 然后循环遍历 调用 unmount方法,parent.removeChild(child)
// 循环卸载所有孩子
const unmountChildren = (
children,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
start = 0
) => {
for (let i = start; i < children.length; i++) {
unmount(children[i], parentComponent, parentSuspense, doRemove, optimized)
}
}
3.老的是数组,新的也是数组,走 diff 算法*
4.老的是数组,新的没有孩子,就卸载老的。
5.老的是文本,新的是数组,把老的文本清空,挂载新的
diff 算法
核心针对的是 元素的 diff。
当新老都有儿子且不是文本的情况下会走 diff 算法。
比较两个元素的 key 。
没有采用双指针,但是标识了两个children 的 开始 和结尾。 i 表示开始的位置,e1 表示c1 的结束位置,e2 表示c2 的结束位置。
从头开始比,两个 children 有一个比完了就不用比了,遇到不同的就停止。
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]))
// 如果类型相等就 递归 比较儿子
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 类型不同就停止
break
}
i++
}
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2])
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
比对完还剩一个的情况就直接插入
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) { // 老的少 新的多
if (i <= e2) { // 表示有新增部分
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos]).el : parentAnchor
while (i <= e2) {
// 向后追加
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
i > e1 就挂载
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) { // 老的多 新的少
while (i <= e1) { // 表示删除部分
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
组件更新整理
组件是如何更新的,自身属性数据变化,外界属性变化也要更新。
父给儿子传入属性 儿子是否要更新?更新一次 -> 组件本身需要更新属性
儿子使用了属性,属性变化了要不要更新?更新一次 -> 更新页面渲染
这个更新流程:
先更新父组件的属性,产生一个新的 vnode,然后走patch,类型是元素就走 processElement,新旧 vnode 元素类型相同,就会元素复用,有属性就走 patchProps,有children走patchChildren,两个children比较
新的文本,老的数组,卸载老的。
新的文本,老的文本,将新的文本插入覆盖老的。
新的数组,老的数组,就走 patchKeyChildren,也就是diff 算法。
children是组件,就走 组件的处理过程,n1 n2 都存在就是组件更新,走 updateComponent。组件复用,n2.component = n1.component, 然后看组件是否要更新,就看组件的属性和插槽等是否发生了变化。在更新之前要删除组件本身的更新,防止更新两次?这块没看懂。
大概意思是我父组件属性变化并且传给子组件,本身就会让他更新一次,然后子组件用到了这个属性,子组件也会让自己更新一次,这就是两次更新,所以需要删除 子组件本身的更新。