jpa 不等于 update_vue3 宇宙之 update(看得见的思考)

5523cb8357cca09fb85a3377b53326ff.png

作者 | 开课吧效瑞

编辑 | 开三金

来源 | 开课吧前端团队(ID:KKBWeb)

前 言

本篇还是以看得见的思考来分析整个 update 过程,后续还会有总结的文章。

基本的流程是:先用看的见的思考来看源码,然后总结。

scopeId 都有啥用?

初始化逻辑搞定之后,接着就是看看更新逻辑了。

更新逻辑着可是核心中的核心,这个搞懂之后,在去给 vue 提 pr 那可就轻松多了。

好了, 废话不多说了 先看源码。

什么时候会触发 update 的逻辑?

首先第一个问题,我们需要考虑什么时候会触发 update 的逻辑。

先上应用层的代码:

685a01d82a2e2b5e933be499188a37d8.png

可以看这个组件,当我们点击 button 的时候 视图一定会刷新。

那么在源码里面的流程是怎么样的呢?

还记得我们的 setupRenderEffect 逻辑嘛?

还是贴代码再回顾一下吧:

b5045bffc14f6cc8065a82ec86b5dfb0.png

重点来了,注意这里的 effect。

我们是在 effect 里面调用的 render 函数,

而当我们调用 render 函数的话,肯定会触发响应式对象的 get ,这其实是关于 reactivity 的核心逻辑,如何收集依赖和如何触发依赖的。

这里我们暂时知道当我们调用 render 函数之后,会触发依赖收集,收集的就是当前用 effect 包裹的这个 function,后面当我们的响应式数据变动的时候回再次调用这个 function。

比如上面使用层的代码,点击按钮的时候 count 变了。

当 count 变了之后就会触发依赖,也就调用了我们的这个 function,这也就是 update 逻辑的入口,

这个入口整明白了就和初始化的逻辑结合在一起了。

接着往下看:

e13bb2688514d00e99e06e0b3cf58481.png

现在可以直接挂住 else 里面的逻辑了。

我先整理一下代码:

684d578afba022ee975822ebb6208588.png

这里有个疑惑点是 Instance.next 是个什么鬼,

先去查查注释:

25a0a6b377cdb8d43aafaef47a62330e.png

好吧,看着注释也没有太看明白。

先过,回头再来看。

不过猜测一下的话,第一次 update 的时候这个 next 应该是个 null ,

那么我先把涉及到处理 next 的逻辑先去掉。

3541de0577b4452899e2f04557f00796.png

咦,我发现在这里给 next 赋值了。next 等于当前的 vnode 。

调用 renderComponentRoot

然后调用:

renderComponentRoot(instance),

简化逻辑:

68094db0db62616e3b72afc67a779683.png

我们看看到底都做了啥:

再次调用 render 函数得到新的 vnode ,这里命名为 result;

继承之前 vnode 的父级 scopeId ?这里的 scopeId 都有啥用呢?(记录一下);

继承 directives;

继承 transition data;

继承 ref。

稍微分析分析,其实呢,这里就是再次调用 render 函数,然后返回出去,别的杂七杂八的事先不管。

1838b0ff52e4e5fd68386ee8515721b9.png

再次回到 setupRenderEffect 继续往下看:

da829211159af9dc87443a349ce61110.png

这也是个关键逻辑,做数据的更替了:

把之前的 vnode 赋值给 prevTree,把现在的 vnode 赋值给 instance.subTree,

接着还需要更新一下 el(实际渲染出来的 element),

接着调用一些 hook:

beforeUpdate hook;

onVnodeBeforeUpdate.

ec3bcc1e8a42d247cc25bbc038c2be40.png

接着就是重点啦,再次调用 patch:

d48f673f9613815b40beb3f68d72a253.png

只不过和我们初始化的时候对比,现在的 n1 是有值的了。

我们先把后面的逻辑看完,然后在看是如何在 patch 里面对比两个节点的:

1799105428b970b1e5893da0217a5e8c.png

因为 patch 完了之后有可能会生成一个新的 el ,所以需要把新的 el 赋值给新的 vnode 上:

8961d21dacc2378bc2c33780c8478dc3.png

这里还是 hook 的调用:

updated hook;

onVnodeUpdated.

好,接着我们就进入重头戏。

updateComponent

因为会再次调用 patch ,然后会进行 component 类型的处理,这里当然是调用 updateComponent 啦,所以我们直接看着逻辑。

简化逻辑:

7e16a9a88a849518771f9419ee0d004f.png

这里有几个比较重要的逻辑函数:

shouldUpdateComponent 判断到底需不需要更新;

如果需要更新的话,

调用 updateComponentPreRender ,

或者

invalidateJob 和 instance.update();

不需要更新的话直接把之前的属性拿过来即可。

我们这里主要分析的是 happy path ,所以只会执行到 else 里面的逻辑,也就是调用 instance.update()。

而调用 update 的话,就会再次执行一遍 setupRenderEffect。

我们等等再来看,

先看下 shouldUpdateComponent.

shouldUpdateComponent

其实这个函数的回答的问题是,什么情况下需要更新组件呢?

先简化一下代码:

7d6f8547bc5e2ec64747c68f67f8b8a3.png

逐个来分析的话:

如果有 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 的。

接下来的逻辑应该是进入了这个分支:

e56b012f14d63ddbedfef167d6801450.png

我们在重新读一下 update ,这里和第一次进入 update 时有一点不同,就是 vnode.next 是有值的。(我们在第一次执行 update 的时候给 vnode 赋值的,还记得吗)

哈哈,暂时发现一个有趣的点,默认的代码第一次读的时候感觉好难好复杂,但是你多看它几遍的话,也就那么回事,所以以后看到复杂的代码你就多看它几遍哈哈。

d6f49e4c4a0bf5a179f3b8341d1b8bd2.png

因为现在 next 是有值的,所以应该会进入到 updateComponentPreRender 函数内。

updateComponentPreRender

简化逻辑:

6f55f5387dd3d05c81b03f151f4df2b7.png

就是更新了 props 和 slots ,这里细节咱就先不看了。

再继续往下看,我们暂时的问题是怎么更新到 component 内部的 element 的呢?

啊哦,并没有发现多余的线索。

看来我需要找个例子来 debug 一下。

这里我选择的策略是先从最简单的逻辑看起来:

91ba41c8737317aea3f299805652cb98.png

这种情况是当前的这个 div (element) 的 id 是动态的,然后 8 代表的是 PatchFlags.PROPS 在后面的数组["id"] 里面是标记着动态的 prop 是什么,这里当然就是 id 了。

有一点要说明的是,我们这里是直接写死的,如果是利用 template 来写的话,它会自动生成。

好,按照这个思路 我看看它是如何处理 component 类型的。

cc4223013ef8bea88a6785ad83cb36fb.png

在这种情况下,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 里面实现一遍,看看有没有逻辑落下。

8b7f49f5c0a4c0c214965f3c20419759.png
1caec123ec08e937fe966748981f624d.png

至此整个 update 的流程就都已经分析完了,剩下的就是针对细节来分析了。

后面的策略是基于特定的场景来分析对应的 patch 逻辑,不然的话,逻辑太多,容易在细节中迷路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值