坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

我们通过createApp了解到了render function到真实dom这个过程。

坏蛋Dan:vue runtime源码分析学习——day3:确定后续分析流程

坏蛋Dan:vue runtime源码分析学习——day4:createApp

不过有几个点没有分析:

  1. hmr
  2. createVNode
  3. patch

hmr其实我们稍微的接触到了一下,但是还没接触到核心,后面我们遇到了再说。

今天我们来看下createVnode的代码里具体做了什么。


createVnode

选择测试用例

在分析之前,我们来找下测试用例

本来打算自己写一个的,但是模拟对应的runtime环境不是一件简单的事情,尤其是几个全局变量,其中还涉及到webpack对于chunck的分割,这又是一个难点。如果不涉及webpack相关的功能,其实还是有可能模拟的。

所以这里依旧是退而求其次,直接用测试用例里的,这些用例自然都是没经过编译的代码。

(我开始有些后悔了,如果直接基于浏览器调试,也就是调试build之后的dist代码,那么就会很舒服,但是可读性小了很多,如果你不喜欢这种调试方式,可以直接基于浏览器调试,也就没这么麻烦了,另外由于同个包中所有代码都在同一个文件中,所以不用各个文件翻阅也是一件不错的事情)

这里我们不使用昨天我们在vue/index.spec.ts中的测试用例了。

我们进入到createVNode函数所在的包中runtime-core中找,这里面有个vnode.spec.ts,专业对口。

就这货了,看着有缘。

话不多说,开始调试。


openBlock

openBlock我们在编译阶段也经常遇到,由于将一块node-tree标记为block,这样方便跟踪,比如slot等就是开block的。

也是栈结构,因为node-tree也是嵌套的结构,那么block标记自然也是嵌套的结构,所以用栈来保证嵌套层级的正确和顺序的正确再适合不过了。


_createVNode

在分析之前,我们要记住,vnode是针对于每个节点的(组件自身也有vnode),不然看到后面可能就有些混了。

这个createVNodeWithArgsTransform方法就不看代码了,它是用来搭配test-utils的,我之前关于单元测试相关的文章中也有用到,感兴趣的大佬可以去看下。

如果这个传入的节点不对,比如是个symbol或者undefined,那么它将被当作是一个注释节点。

如果它自身就是一个vnode,比如``,它自身就是vnode,那么这个时候就直接copy它,然后加上一些内置的东西。

如果此时允许blockTree并且当前节点不是block节点而是block节点里的子节点,那么这个时候它就会被放到currentBlock里。

然后将这个cloneNodeflag赋值为BAIL

BAIL我们来看下描述:

一个特殊标志,指示dff算法应退出优化模式。例如,在 renderSlot() 创建的block片段上,当遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全diff)时 或手动克隆VNodes

简单的说就是遇到这种直接乱棍打死,而不是根据flag等方式跳过diff

然后返回这个cloneVNode

如果不是vnode,那么继续判断它是否是一个class component

如果是一个class component,那么它会变成它的__vccOpts,官方给的class component我试了下发现并没有效果,看了下对应的测试用例张这样:

不纠结,我们接着往下看。

2.x兼容的一如既往的就跳过了

然后处理domprops也就是属性

  • guardReactiveProps

这里对于响应式的属性或者被proxy代理的对象都需要clone一份出来,为了可修改。

  • normalizeClass:这个方法我们在编译阶段其实有说过,_normalizeClass辅助函数,简单的说就是把所有的class做拼接。
  • normalizeStyle:同理,这个方法之前编译阶段有说过,这里也不多说了,和class一样是在做拼接。注意则合理都覆盖了原props的属性。

你可能会有疑惑,为啥这里可以直接拼接,不是有动态的属性吗?因为此时是runtime阶段,部分已经经过之前的辅助函数处理(script部分的执行早就开始了,此时的数据应该已经暂时固定下来了),都已经确定下来了。

然后判断这个vnode的类型,可能就是一个节点,也可能是一个组件,比如我们的sfc组件在runtime的样子,也有可能是内部组件,比如Teleport或者Suspense

另外认识下ShapeFlags

最后调用createBaseVNode


createBaseVNode/createElementVNode

是的,createBaseVNode也就是我们编译阶段遇到的createElementVNode这个辅助函数。

所以这里其实不是每个vnode都是通过createVNode进入的,比如原生元素就直接调用的createBaseVNode生成vnode,而App这样的组件就是通过createVNode生成的,因为它自身不是使用的createElementVNode包裹的

其实感觉代码没必要贴出来的,因为这里就是组合数据,返回vnode

但是这里面有几个东西是有必要了解的。

  1. normalizeChildren:简单的说就是用来处理这个vnodechildren node的,但是一般是不需要处理的, 代码分析放后面。
  2. SuspenseImpl.normalize(vnode):这个代码分析也放到后面,简单的说就是包裹它的子dom,等到可以渲染的时候再render

如果不需要处理子vnode,那就是说明子节点都是确定的了,要么就是文本节点,要么就是数组节点。

一般也不需要处理,因为编译阶段我们已经处理完毕了。

接着就是回收这个vnode,毕竟是在block里的。当然,只有需要patch的才会被放到block里面(组件除外,即使组件不需要更新,但是它依旧需要被patch,毕竟你不能让一个组件突然就没得了,得先保留它的实例,不然到时候通过unmount的方式卸载时找不到)。

最后返回这个vnode


normalizeChildren

这个方法简单的说就是用来处理一些子节点类型的,大部分在编译阶段就已经被处理好了,但也不是所有。

那么什么情况会进入到这个方法中呢?

任何通过createVNode的方式创建vnode的节点,比如文本节点调用的辅助函数是createTextVNode,又比如注释节点使用的createCommentVNode,它们都是调用的createVNode

createVNode传给createBaseVNodeneedFullChildrenNormalizationtrue,所以就会调用这里的normalizeChildren。另外,我们的slotteleport代码也都会进入这里,它们需要确定上下文,需要使用_withCtx包裹。


总结

今天写的有些水。。。这俩迭代有个需求很搞心态。。

这里有一点注意的,那就是编译阶段最后render function里的一个个node和这个vnode的关系。

vnoderender function里的vnodeCall执行后的产物。以_createTextVNode("1111")为例子,它并不是一个vnode,它是一个vnodeCall,只有它被执行之后它才变成了vnode。而这个_createTextVNode内部就是基于我们今天的主角createVNode的。

编辑于 2023-02-22 10:21・IP 属地广东