「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
零零总总的,更文活动写了 35 天了,说句实话,要不是有个活动盯着,真的很难坚持下去,不多说啦——致敬未来~
上一篇小作文的重点放在了 v-if/v-else-if/v-else 这几个条件渲染指令,条件渲染的实现核心流程如下:
- 生成
ast的parse阶段parseHTML的过程中会将v-if、v-else-if、v-else解析成el.if/el.elseif/el.else,同时会条件和条件成立时渲染的元素组成对象:{ exp, block }push进el.ifConditions中; - 接着就是利用
ast生成render 函数的generate阶段,调用genElement,当判断el.if存在时调用genIf处理条件渲染并标记el.ifProcessed为true防止重复处理;
这个阶段我们讲的是 generate 阶段,所谓 generate 就是利用前面 parse 解析 html 模板获得的 ast 节点,将其还原成真实的 DOM 节点的过程。这个过程分为两部分:
- 一仍属于部分属于编译时工作,将
ast编译成DOM对应的渲染函数,所谓渲染函数是描述调用运行时辅助函数生成真正DOM的代码; - 另一部分属于运行时的辅助,这一部分是
Vue调用第一步生成的render 函数时生效,render 函数的执行就会生成真正DOM;
在日常开发通用组件时,大家肯定用过的一个标签 —— <slot></slot>,今天的笔墨就说说她的 render 函数;
二、genElement 的调用
在说 genSlot 的调用过程之前,先来个例子:
test.html中引用some-com组件,并且some-come组件中的 i 标签作为插槽内容分发给组件的具名插槽nameSlot
<some-com :some-key="forProp">
<slot aaa="b" bbb="c" name="namedSlot"><div>哒哒哒哒哒哒哒哒</div></slot>
{{ someKey.a + foo }}
</some-com>
- 注册
some-com组件,组件内置了一个具名插槽namedSlot
const someCom = {
template: `
<div style="color: red;background: #5cb85c;display: inline-block">
<slot aaa="b" bbb="c" name="namedSlot"><div>哒哒哒哒哒哒哒哒</div></slot>
{{ someKey.a + foo }}
</div>`,
props: {
someKey: {
type: Object,
default: () => 'hhhhhhh'
}
},
inject: ['foo']
}
2.1 genElement 调用 genSlot
genElement 如果判断 el.tag 即标签名为 slot,说明就是一个预置的插槽,则调用 genSlot 处理并返回结果;
export function genElement (): string {
if (el.parent) {
} else if (el.tag === 'slot') {
// 调用 genSlot 获取 slot 标签的渲染函数,形如:slotName, children, attrs, bind)
return genSlot(el, state)
} else {
return code
}
}
2.2 genSlot
方法位置:src/compiler/codegen/index.js -> genSlot
方法参数:
el,ast节点对象;state,CodegenState对象
方法作用:处理 slot 标签的 ast 对象,生成插槽对应的渲染函数,形如:
_t(slotName, children, attrs, bind),_t 也是一个运行时渲染函数的辅助函数,这个当然会放到后面一起讲;具体工作如下:
- 获取插槽名,匿名的
slot插槽自动兜底一个default作为插槽名; - 拼接
slot的渲染函数主体:_t(....);
function genSlot (el: ASTElement, state: CodegenState): string {
// 获取具名插槽的插槽名称,如果没有则兜底 default
const slotName = el.slotName || '"default"'
// 处理 slot 标签的所有子节点,生成子节点的 render 函数
const children = genChildren(el, state)
// <slot /> 标签渲染函数结果字符串 _t(slogName, children
let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
// slot 标签上的 attrs
const attrs = el.attrs || el.dynamicAttrs
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
})))
: null
获取 slot 标签上的 v-bind
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
// _t(slotName, children, attrs, bind)
return res + ')'
}
从上面的代码中我们可以看出,slot 标签的子元素仍然会被处理成渲染函数。但是大家使用某个组件的插槽时,你会发现原来插槽标签 slot 和它的子元素都会被替换成分发内容,那这个 children 渲染还有什么意义呢?
它的意义在于当组件没有接收到分发的插槽内容,比如 <some-com></some-com>,此时some-com 就没有分发插槽内容,即some-com标签是空的;这个时候 slot 的子元素仍然会被渲染处理,起到默认的占位提示作用。
-
slot标签的children渲染函数如图: -
some-com没有插槽内容时的占位
2.3 说明
我写 Vue 源码阅读的方式和其他人最大的不同在文章的顺序和方法的列出顺序是按照代码的执行顺序组织的,可以这么说,Vue 的代码在浏览器中以什么样的顺序运行,我的文章就是以什么顺序组织的。
但是这一篇有一点例外,是因为接下来就准备聊一聊渲染函数的运行时帮助函数(_c/_v/_c/_t 等)的作用,所以这里是提前说了 genSlot 方法,并不代表我们的例子代码已经执行到 genSlot 了。为什么这么说?
这是因为 slot 标签处于子组件 <some-com /> 内部,一个子组件也是一个全新的 Vue 实例,这个实例现在还没有创建,只有当根实例(test.html 中的 script 标签下的 new Vue 是根实例)的渲染函数被执行时,执行渲染 some-com 组件的时候才会重新走创建子组件实例,编译子组件模板、生成子组件的渲染函数,生成子组件渲染函数时就会解析到这个 slot 标签,此时则会调用 genSlot 处理他;
三、总结
本篇小作文作为一个独立的小部分存在,提前介绍了 Vue 在编译时对 <slot> 这个占位符标签的一个处理,编译 slot 标签最终得到一个叫做 _t 的渲染函数的运行时帮助函数的调用:_t(slotName, children, attrs, bind);
其中 slotName 即为具名插槽 slot 标签上的 name 属性值,如果是匿名插槽,则 slotName 为 "default" ;
children 是 slot 标签的子元素;虽然插槽最终使用时不会展示,但是当组件被引用时没有传递插槽内容时,slot 的子元素仍然会被渲染用以占位提示。