
当我们写出这样的代码之后,我们肯定不希望组件执行四次重新渲染,而是执行一次渲染。那就应该降低更新频率,对 effect 去重
组件多次更新属性只渲染一次
const setupRenderEffect = (instance,initialVNode,container,anchor,parentSuspense,isSVG,optimized) => {// create reactive effect for rendering// 每个组件都有一个effect, vue3 是组件及更新,数据变化会重新执行对应组件的effectinstance.update = effect(function componentEffect() {// 如果没有被挂载,就是初次渲染if (!instance.isMounted) {let vnodeHookconst { el, props } = initialVNodeconst { bm, m, parent } = instance// beforeMount hookif (bm) {invokeArrayFns(bm)}// onVnodeBeforeMountif ((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 hookif (m) {queuePostRenderEffect(m, parentSuspense)}// onVnodeMountedif ((vnodeHook = props && props.onVnodeMounted)) {const scopedInitialVNode = initialVNodequeuePostRenderEffect(() => 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-aliveif (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 = trueif (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {devtoolsComponentAdded(instance)}// #2458: deference mount-only object parameters to prevent memleaksinitialVNode = 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 = falseisFlushing = trueflushPreFlushCbs(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 = 0queue.length = 0 // 清空flushPostFlushCbs(seen)isFlushing = false}}
将当前effect存放到队列里,没存放的时候刷新队列,但是队列只能执行一次。。。这里没太懂,
默认两个元素比较
// 创建一个 effect 让 render 执行const setupRenderEffect = (instance,initialVNode,container,anchor,parentSuspense,isSVG,optimized) => {// create reactive effect for renderinginstance.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 memleaksinitialVNode = container = anchor = null} else {// 更新组件let proxyToUse = instance.proxy;// 新树const nextTree = instance.subTree = instance.render.call(proxyToUse,proxyToUse);// 旧树const prevTree = instance.subTreeinstance.subTree = nextTreepatch(prevTree,nextTree,// parent may have changed if it's in a teleport// hostParentNode(prevTree.el!)!,// anchor may have changed if it's in a fragmentgetNextHostNode(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 -》 新的 spanif (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 } = n2switch (type) {case Text:processText(n1, n2, container, anchor)breakcase Comment:processCommentNode(n1, n2, container, anchor)breakcase Static:if (n1 == null) {mountStaticNode(n2, container, anchor, isSVG)} else if (__DEV__) {patchStaticNode(n1, n2, container, isSVG)}breakcase Fragment:processFragment(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)breakdefault:// 元素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 } = n2patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS// 老的属性const oldProps = n1.props || EMPTY_OBJ// 新的属性const newProps = n2.props || EMPTY_OBJlet vnodeHookif (__DEV__ && isHmrUpdating) {// HMR updated, force full diffpatchFlag = 0optimized = falsedynamicChildren = null}if (patchFlag > 0) {if (patchFlag & PatchFlags.FULL_PROPS) {// element props contain dynamic keys, full diff neededpatchProps(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 bindingsif (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-nullconst 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 diffpatchProps(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 diffpatchChildren(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.childrenconst prevShapeFlag = n1 ? n1.shapeFlag : 0const c2 = n2.childrenconst { patchFlag, shapeFlag } = n2// fast pathif (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 arrayspatchKeyedChildren(c1,c2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)return} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {// unkeyedpatchUnkeyedChildren(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 ewhile (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 = 0if (i > e1) { // 老的少 新的多if (i <= e2) { // 表示有新增部分const nextPos = e2 + 1const anchor = nextPos < l2 ? (c2[nextPos]).el : parentAnchorwhile (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 = -1else 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, 然后看组件是否要更新,就看组件的属性和插槽等是否发生了变化。在更新之前要删除组件本身的更新,防止更新两次?这块没看懂。
大概意思是我父组件属性变化并且传给子组件,本身就会让他更新一次,然后子组件用到了这个属性,子组件也会让自己更新一次,这就是两次更新,所以需要删除 子组件本身的更新。


