我们通过createApp
了解到了render function
到真实dom
这个过程。
坏蛋Dan:vue runtime源码分析学习——day3:确定后续分析流程
坏蛋Dan:vue runtime源码分析学习——day4:createApp
不过有几个点没有分析:
hmr
createVNode
patch
hmr
其实我们稍微的接触到了一下,但是还没接触到核心,后面我们遇到了再说。
今天我们来看下createVnode
的代码里具体做了什么。
在分析之前,我们来找下测试用例
本来打算自己写一个的,但是模拟对应的runtime
环境不是一件简单的事情,尤其是几个全局变量,其中还涉及到webpack
对于chunck
的分割,这又是一个难点。如果不涉及webpack
相关的功能,其实还是有可能模拟的。
所以这里依旧是退而求其次,直接用测试用例里的,这些用例自然都是没经过编译的代码。
(我开始有些后悔了,如果直接基于浏览器调试,也就是调试build
之后的dist
代码,那么就会很舒服,但是可读性小了很多,如果你不喜欢这种调试方式,可以直接基于浏览器调试,也就没这么麻烦了,另外由于同个包中所有代码都在同一个文件中,所以不用各个文件翻阅也是一件不错的事情)
这里我们不使用昨天我们在vue/index.spec.ts
中的测试用例了。
我们进入到createVNode
函数所在的包中runtime-core
中找,这里面有个vnode.spec.ts
,专业对口。
就这货了,看着有缘。
话不多说,开始调试。
openBlock
我们在编译阶段也经常遇到,由于将一块node-tree
标记为block
,这样方便跟踪,比如slot
等就是开block
的。
也是栈结构,因为node-tree
也是嵌套的结构,那么block
标记自然也是嵌套的结构,所以用栈来保证嵌套层级的正确和顺序的正确再适合不过了。
在分析之前,我们要记住,vnode
是针对于每个节点的(组件自身也有vnode
),不然看到后面可能就有些混了。
这个createVNodeWithArgsTransform
方法就不看代码了,它是用来搭配test-utils
的,我之前关于单元测试相关的文章中也有用到,感兴趣的大佬可以去看下。
如果这个传入的节点不对,比如是个symbol
或者undefined
,那么它将被当作是一个注释节点。
如果它自身就是一个vnode
,比如``,它自身就是vnode
,那么这个时候就直接copy
它,然后加上一些内置的东西。
如果此时允许blockTree
并且当前节点不是block
节点而是block
节点里的子节点,那么这个时候它就会被放到currentBlock
里。
然后将这个cloneNode
的flag
赋值为BAIL
BAIL
我们来看下描述:
一个特殊标志,指示dff
算法应退出优化模式。例如,在 renderSlot()
创建的block
片段上,当遇到非编译器生成的插槽(即手动编写的渲染函数,应始终完全diff
)时 或手动克隆VNodes
简单的说就是遇到这种直接乱棍打死,而不是根据flag
等方式跳过diff
。
然后返回这个cloneVNode
。
如果不是vnode
,那么继续判断它是否是一个class component
如果是一个class component
,那么它会变成它的__vccOpts
,官方给的class component
我试了下发现并没有效果,看了下对应的测试用例张这样:
不纠结,我们接着往下看。
2.x兼容的一如既往的就跳过了
然后处理dom
的props
也就是属性
guardReactiveProps
:这里对于响应式的属性或者被proxy
代理的对象都需要clone
一份出来,为了可修改。
normalizeClass
:这个方法我们在编译阶段其实有说过,_normalizeClass
辅助函数,简单的说就是把所有的class
做拼接。normalizeStyle
:同理,这个方法之前编译阶段有说过,这里也不多说了,和class
一样是在做拼接。注意则合理都覆盖了原props
的属性。你可能会有疑惑,为啥这里可以直接拼接,不是有动态的属性吗?因为此时是runtime
阶段,部分已经经过之前的辅助函数处理(script
部分的执行早就开始了,此时的数据应该已经暂时固定下来了),都已经确定下来了。
然后判断这个vnode
的类型,可能就是一个节点,也可能是一个组件,比如我们的sfc
组件在runtime
的样子,也有可能是内部组件,比如Teleport
或者Suspense
。
另外认识下ShapeFlags
最后调用createBaseVNode
是的,createBaseVNode
也就是我们编译阶段遇到的createElementVNode
这个辅助函数。
所以这里其实不是每个vnode
都是通过createVNode
进入的,比如原生元素就直接调用的createBaseVNode
生成vnode
,而App
这样的组件就是通过createVNode
生成的,因为它自身不是使用的createElementVNode
包裹的
其实感觉代码没必要贴出来的,因为这里就是组合数据,返回vnode
。
但是这里面有几个东西是有必要了解的。
normalizeChildren
:简单的说就是用来处理这个vnode
的children node
的,但是一般是不需要处理的, 代码分析放后面。SuspenseImpl.normalize(vnode)
:这个代码分析也放到后面,简单的说就是包裹它的子dom
,等到可以渲染的时候再render
。如果不需要处理子vnode
,那就是说明子节点都是确定的了,要么就是文本节点,要么就是数组节点。
一般也不需要处理,因为编译阶段我们已经处理完毕了。
接着就是回收这个vnode
,毕竟是在block
里的。当然,只有需要patch
的才会被放到block
里面(组件除外,即使组件不需要更新,但是它依旧需要被patch
,毕竟你不能让一个组件突然就没得了,得先保留它的实例,不然到时候通过unmount
的方式卸载时找不到)。
最后返回这个vnode
。
这个方法简单的说就是用来处理一些子节点类型的,大部分在编译阶段就已经被处理好了,但也不是所有。
那么什么情况会进入到这个方法中呢?
任何通过createVNode
的方式创建vnode
的节点,比如文本节点调用的辅助函数是createTextVNode
,又比如注释节点使用的createCommentVNode
,它们都是调用的createVNode
。
而createVNode
传给createBaseVNode
的needFullChildrenNormalization
是true
,所以就会调用这里的normalizeChildren
。另外,我们的slot
和teleport
代码也都会进入这里,它们需要确定上下文,需要使用_withCtx
包裹。
今天写的有些水。。。这俩迭代有个需求很搞心态。。
这里有一点注意的,那就是编译阶段最后render function
里的一个个node
和这个vnode
的关系。
vnode
是render function
里的vnodeCall
执行后的产物。以_createTextVNode("1111")
为例子,它并不是一个vnode
,它是一个vnodeCall
,只有它被执行之后它才变成了vnode
。而这个_createTextVNode
内部就是基于我们今天的主角createVNode
的。
编辑于 2023-02-22 10:21・IP 属地广东