作者 | 开课吧效瑞
编辑 | 开三金
来源 | 开课吧前端团队(ID:KKBWeb)
前 言
本篇还是以看得见的思考来分析整个 update 过程,后续还会有总结的文章。
基本的流程是:先用看的见的思考来看源码,然后总结。
scopeId 都有啥用?
初始化逻辑搞定之后,接着就是看看更新逻辑了。
更新逻辑着可是核心中的核心,这个搞懂之后,在去给 vue 提 pr 那可就轻松多了。
好了, 废话不多说了 先看源码。
什么时候会触发 update 的逻辑?
首先第一个问题,我们需要考虑什么时候会触发 update 的逻辑。
先上应用层的代码:
可以看这个组件,当我们点击 button 的时候 视图一定会刷新。
那么在源码里面的流程是怎么样的呢?
还记得我们的 setupRenderEffect 逻辑嘛?
还是贴代码再回顾一下吧:
重点来了,注意这里的 effect。
我们是在 effect 里面调用的 render 函数,
而当我们调用 render 函数的话,肯定会触发响应式对象的 get ,这其实是关于 reactivity 的核心逻辑,如何收集依赖和如何触发依赖的。
这里我们暂时知道当我们调用 render 函数之后,会触发依赖收集,收集的就是当前用 effect 包裹的这个 function,后面当我们的响应式数据变动的时候回再次调用这个 function。
比如上面使用层的代码,点击按钮的时候 count 变了。
当 count 变了之后就会触发依赖,也就调用了我们的这个 function,这也就是 update 逻辑的入口,
这个入口整明白了就和初始化的逻辑结合在一起了。
接着往下看:
现在可以直接挂住 else 里面的逻辑了。
我先整理一下代码:
这里有个疑惑点是 Instance.next 是个什么鬼,
先去查查注释:
好吧,看着注释也没有太看明白。
先过,回头再来看。
不过猜测一下的话,第一次 update 的时候这个 next 应该是个 null ,
那么我先把涉及到处理 next 的逻辑先去掉。
咦,我发现在这里给 next 赋值了。next 等于当前的 vnode 。
调用 renderComponentRoot
然后调用:
renderComponentRoot(instance),
简化逻辑:
我们看看到底都做了啥:
再次调用 render 函数得到新的 vnode ,这里命名为 result;
继承之前 vnode 的父级 scopeId ?这里的 scopeId 都有啥用呢?(记录一下);
继承 directives;
继承 transition data;
继承 ref。
稍微分析分析,其实呢,这里就是再次调用 render 函数,然后返回出去,别的杂七杂八的事先不管。
再次回到 setupRenderEffect 继续往下看:
这也是个关键逻辑,做数据的更替了:
把之前的 vnode 赋值给 prevTree,把现在的 vnode 赋值给 instance.subTree,
接着还需要更新一下 el(实际渲染出来的 element),
接着调用一些 hook:
beforeUpdate hook;
onVnodeBeforeUpdate.
接着就是重点啦,再次调用 patch:
只不过和我们初始化的时候对比,现在的 n1 是有值的了。
我们先把后面的逻辑看完,然后在看是如何在 patch 里面对比两个节点的:
因为 patch 完了之后有可能会生成一个新的 el ,所以需要把新的 el 赋值给新的 vnode 上:
这里还是 hook 的调用:
updated hook;
onVnodeUpdated.
好,接着我们就进入重头戏。
updateComponent
因为会再次调用 patch ,然后会进行 component 类型的处理,这里当然是调用 updateComponent 啦,所以我们直接看着逻辑。
简化逻辑:
这里有几个比较重要的逻辑函数:
shouldUpdateComponent 判断到底需不需要更新;
如果需要更新的话,
调用 updateComponentPreRender ,
或者
invalidateJob 和 instance.update();
不需要更新的话直接把之前的属性拿过来即可。
我们这里主要分析的是 happy path ,所以只会执行到 else 里面的逻辑,也就是调用 instance.update()。
而调用 update 的话,就会再次执行一遍 setupRenderEffect。
我们等等再来看,
先看下 shouldUpdateComponent.
shouldUpdateComponent
其实这个函数的回答的问题是,什么情况下需要更新组件呢?
先简化一下代码:
逐个来分析的话:
如果有 dirs 和 transition 的话,会更新;
接着是判断了 patchFlag ,这个 flag 也是个很值得一说的点,它是在编译阶段生成的,不同的模板类型会生成不一样的值,这个可以单独写个专题来分析,暂时我们先知道有这个 flag 即可。
大概有这么几种情况都需要更新:
PatchFlags.DYNAMIC_SLOTS;
PatchFlags.FULL_PROPS
这种情况的话还会对比一下之前的 props 和现在的 props 有啥不一样的,发现只要有一个 prop 不一样就会更新;
PatchFlags.PROPS 这种情况是检测动态的 props ,这里主要要关注的逻辑点是 nextVNode.dynamicProps 是什么时候给赋值的;
接着就是检测如果有 chilren 的话,那么也需要更新。以上分析的暂时也只是个简单的分析,具体的情况到时候在具体的分析,在回顾一下我们的目标,是了解 update 的流程,暂时先不太关注于细节。
好,最后的结论是对比一下,发现需要更新的话 就返回 true。
这个还思考了一个问题,就是为什么不需要对比 chilren 呢?
应该是因为 component 就是个虚拟的箱子,假如箱子的表面行为都有变动,那么再继续深入,下面要关注的点就是如何触发 updateElement 的。
接下来的逻辑应该是进入了这个分支:
我们在重新读一下 update ,这里和第一次进入 update 时有一点不同,就是 vnode.next 是有值的。(我们在第一次执行 update 的时候给 vnode 赋值的,还记得吗)
哈哈,暂时发现一个有趣的点,默认的代码第一次读的时候感觉好难好复杂,但是你多看它几遍的话,也就那么回事,所以以后看到复杂的代码你就多看它几遍哈哈。
因为现在 next 是有值的,所以应该会进入到 updateComponentPreRender 函数内。
updateComponentPreRender
简化逻辑:
就是更新了 props 和 slots ,这里细节咱就先不看了。
再继续往下看,我们暂时的问题是怎么更新到 component 内部的 element 的呢?
啊哦,并没有发现多余的线索。
看来我需要找个例子来 debug 一下。
这里我选择的策略是先从最简单的逻辑看起来:
这种情况是当前的这个 div (element) 的 id 是动态的,然后 8 代表的是 PatchFlags.PROPS 在后面的数组["id"] 里面是标记着动态的 prop 是什么,这里当然就是 id 了。
有一点要说明的是,我们这里是直接写死的,如果是利用 template 来写的话,它会自动生成。
好,按照这个思路 我看看它是如何处理 component 类型的。
在这种情况下,Parent 里面是嵌套了一个 Child 组件,然后我们是在 parent 的 render 函数内传给 child 的 props ,也就是说当 cId 变化后,它应该是先影响到 Child 。
接着我们来分析一下它整个 runtime 的所有逻辑流程:
cId.value ++ 的时候触发 Parent 的 update 逻辑;
然后再次调用 Parent 的 render 函数,获取到 subTree;
接着会触发 patch ,着时候的参数就是新得到的 subTree ,也就是 createVNode(Child);
因为这个 vnode 是 Child ,类型是 component 所以会走 processComponent 逻辑;
因为 n1 是肯定有值的,所以走 updateComponent 逻辑;
在接下来会触发
shouldUpdateComponent 逻辑,比对两个 vnode ,看看是否需要更新,这里是肯定需要更新的;
然后又触发了 instance.update(),注意一下这里的 instance 可是 Child;
好,我们又一次来到了 instance.update 内,这时候会再次调用 Child.render(),也可以说现在拆箱 Child;
拆箱 Child 得到的 vnode 就是 element p 了;
接着用 p 的 vnode 来调用 patch;
会调用到 patchElement ,继而对比 element。
至此,对于上面的问题,从 component 是如何调用更新到内部的 element 的,就有了答案。
当然我们到现在为止只是分析了两个最简单的更新:组件的更新、element 的更新。
接着把整个流程整理一下:
修改响应式的值,触发 effect 的回调函数(触发依赖);
再次调用 render 函数,获取最新的 vnode 值;
把新的 vnode 和旧的 vnode ,交给 patch;
patch 来基于 vnode 的类型进行处理具体的 update 逻辑;
如果是 component 类型的话,会做一个 updateComponent() 的处理,检测是否可更新,如果可以更新的话会再次调用 update;
如果是 element 类型的话,会调用 patchElement 来检测更新;
接着就是递归的调用当前组件的 render,获取到最新的 subTree(vnode);
重复上面的过程。
我们稍微隐喻一下,如果是 component 类型的话,我们就需要检测要不要开箱,
当需要开箱的话,再处理箱子里面的 element 或者 component ,
如果是 component 那么就重复上面的过程。应该是递归的向下查,截止点就是当前的 component 能不能开箱。
好,这个流程整理完了,怎么对比细节,我们先不管,先把整个流程在 mini-vue 里面实现一遍,看看有没有逻辑落下。
至此整个 update 的流程就都已经分析完了,剩下的就是针对细节来分析了。
后面的策略是基于特定的场景来分析对应的 patch 逻辑,不然的话,逻辑太多,容易在细节中迷路。