「这是我参与2022首次更文挑战的第36天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文提前介绍了 Vue
在编译时对 <slot>
这个占位符标签的一个处理,编译 slot 标签最终得到一个叫做 _t
的渲染函数的运行时帮助函数的调用:_t(slotName, children, attrs, bind)
;
其中 slotName
即为具名插槽 slot
标签上的 name
属性值,如果是匿名插槽,则 slotName
为 "default"
;
children
是 slot
标签的子元素;虽然插槽最终使用时不会展示,但是当组件被引用时没有传递插槽内容时,slot
的子元素仍然会被渲染用以占位提示。
我们前面说了以下几种情况的渲染函数:
- 静态根节点的渲染函数,提升到
staticRenderFns
数组; - 带有
v-model
的input
渲染函数,主要处理不同元素、input
动态绑定type
时不同的事件处理类型和绑定值的类型; - 带有
v-if/v-else-if/v-else
的条件渲染,根据el.ifConditons
处理成套娃式的三元表达式; - 带有
v-for
的列表渲染,根据el.for
和el.alias
处理成_l(el.for, function (it1,it2) {return _c(...)})
的调用; - 处理 标签的渲染,如果
el.tag
,获取其slotName
处理成_t(slotName,children...)
除了动态组件(<component :is="someCompName" />
)、自定义的组件渲染函数我们没有聊,常见的场景我们已经说的差不多了,今天我们着重看下动态组价、自定义组件的渲染函数;
二、自定义组件的处理
在我们的 test.html
中有一个 <some-com></some-com>
自定义组件,我们以他为例子看下自定义组件的渲染函数;
2.1 例子 test.html
<div id="app">
<some-com :some-key="forProp">
<div>someCom 的插槽内容</div>
</some-com>
</div>
复制代码
2.2 genElement 处理自定义组件
这里要说的是,在根实例的渲染函数中,处理自定义组件和处理原生的 HTML
元素的差异并不大,都是在 genElement
的 else
中处理的,包括后面要说的动态组件也是在这里处理的;
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
} else {
// 这个 else 处理:动态组件、普通元素、自定义组件
let code
if (el.component) {
// el.component 标识的不是普通组件,是动态组件 <component :is="someComp" /> 这种
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 非普通元素或者带有 v-pre 指令的组件走这里,处理节点的所有属性,返回一个 JSON 字符串,
// 比如:'{ key: xx, ref: xx, ....}'
data = genData(el, state)
}
// 处理子节点,得到所有的子节点字符串格式的代码组成的数组,格式:
// `['_c(tag, data, children)', ....], normalizationType`
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
复制代码
先上一站图,可以看下此时 <some-com></some-com>
组件 的 ast
对象如图:
从图上可以看出 genElement
进入到了 else
,并且 el的标签名
el.tag是
some-com,从例子中可以知道,
some-comp 组件引用时有传入插槽内容进行分发:
,所以
el.children` 就是这个插槽内容了;
插槽内容也是作为正常的子节点进行渲染的,上面的插槽的渲染函数如下:
"[_c('div',[_v(\"someCom 的插槽内容\")])]"
复制代码
综合上面的结果,我们就得到了 some-com
组件的渲染函数代码:
"_c(
'some-com',
{ attrs: {\"some-key\":forProp} },
[_c(
'div',
[_v(\"someCom 的插槽内容\")]
)
]
)"
复制代码
三、genElement 处理动态组件
你日常开发的时候总是会遇到使用动态组件的场景,这个东西说实话是个好东西,给了开发人员巨大的想象和发挥空间,服务端甚至可以返回一个字符串,就可以对应一个具体的视图;
不过这也使得它看起来很神秘,今天咱们一探究竟吧;
3.1 test.html 的示例
<div id="app">
<component is="someCom">
<div>componet 的插槽</div>
</component>
</div>
复制代码
3.2 genElement 中的 genComponent
genElement
处理动态组件的逻辑同样是在 else
中处理的,判断 el.component
有值时执行的逻辑就是处理动态组件的部分
export function genElement (): string {
if (el.parent) {
} else {
let code
if (el.component) {
// el.component 标识的不是普通组件,是动态组件 <component :is="someComp" /> 这种
// el.componet 是动态组件绑定的 is 属性的值,是个组件名
code = genComponent(el.component, el, state)
} else {
return code
}
}
复制代码
3.3 genCompoent
方法位置:src/compiler/codegen/index.js -> genComponent
方法参数:
componentName
: 组件名,即is
属性绑定的值;el
:ast
节点对象;state
:CodegenState
实例;
方法作用:生成 <component></component>
动态组件的渲染函数,具体工作如下:
- 处理
component
的子元素,获取子元素的render 函数
代码组成的数组; - 拼接当前动态组件指向的
componentName
组件的渲染函数代码,形如:_c(compName, data, children)
function genComponent (
componentName: string,
el: ASTElement,
state: CodegenState
): string {
const children = el.inlineTemplate ? null : genChildren(el, state, true)
return `_c(${componentName},${genData(el, state)}${
children ? `,${children}` : ''
})`
}
复制代码
四、总结
本篇小作文讨论了另外两个常用的功能对应的渲染函数:自定义组件、动态组件;
二者的处理都是处理成 _c(componentName, data, children)
的调用形式;
到这里多少有点失望,我们发现这和渲染个普通的 div
并无二致,说好的自定义组件呢?还有生命周期呢?
先别急,前面我说过现在我们正在处理根实例的渲染函数,这个自定义组件是个子实例,它现在没有进行初始化。那么什么时候才能到这个子实例的初始化呢?此时此刻尚在根实例编译阶段
,只有当根实例的编译
阶段结束,得到根实例的渲染函数
,接下来创建根实例
的渲染 watcher
就会调用根实例的渲染函数。
根实例的渲染函数执行,也就是上面那一串串 _c/_l/_t/_s/_v...
执行的时候,这些帮助函数的执行,如果遇到是自定义组件
,就会接着走重新创建一个新的 Vue 实例
,而创建一个新的 Vue 实例
就会重新走一遍 Vue
的_init
逻辑包含处理数据响应式、编译模板生成 ast
得到子实例 render 函数
,接着创建子实例渲染 wathcer
执行子实例的 render
函数得到 DOM
然后完成挂载;