function initRender(vm) {
...
var options = vm.$options;
var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
var renderContext = parentVnode && parentVnode.context;
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}
function resolveSlots (
children,
context
) {
var slots = {};
if (!children) {
return slots
}
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
var name = data.slot; // 获取 slot 的名
var slot = (slots[name] || (slots[name] = []));
if (child.tag === 'template') { // 如果 tag 是 template 的 slot,那么就会取 template 的 children 作为 slot 的实际内容
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 设置 slots 的默认名为 default
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}
作用域插槽在模板的编译过程当中,并非直接编译成生成 VNode,并挂载至自定义组件 my-component 的 children 当中,而是缓存至 my-component 的 data.scopedSlots 属性中:
function resolveScopedSlots (
fns, // see flow/vnode
res
) {
res = res || {};
for (var i = 0; i < fns.length; i++) {
if (Array.isArray(fns[i])) {
resolveScopedSlots(fns[i], res);
} else {
res[fns[i].key] = fns[i].fn;
}
}
return res
}
slot插槽
在日常的开发过程当中,slot 插槽应该是用的较多的一个功能。Vue 的 slot 插槽可以让我们非常灵活的去完成对组件的拓展功能。接下来我们可以通过源码来看下 Vue 的 slot 插槽是如何去实现的。
Vue 提供了2种插槽类型:
普通插槽
首先来看一个简单的例子:
定义了一个 my-component 全局组件,这个组件内部包含了一个名字为 demo 的插槽。当页面开始渲染时,首先完成模板的编译功能,生成对应的 render 函数:
并由这个 render 函数生成对应的 VNode,其中在生成自定义组件 my-component 的时候,有其对应的children VNode,即在模板当中的 template 节点。最终在生成的 my-component 的 VNode当中,在 componentOptions 属性当中存储了 VNode 子节点的信息。
当整个 VNode 生成完毕后,开始递归将 VNode 渲染成真实的 DOM 节点,并挂载至文档对象中。在将 my-component 的 VNode 进行渲染的过程中:
在 initRender 函数当中,首先从 vm 实例上获取这个自定义组件模板当中嵌入的子节点(options._renderChildren),然后通过 resolveSlots 方法获取子节点对应的 slot,其中会根据这个 slot 是否有单独定义插槽名返回不同的插槽内容,比如说例子当中提供的为具名 demo 的插槽,所以最终返回的为具名插槽:
这里如果为非具名的插槽,那么会默认返回:
同时在模板当中定义的 template 的标签,最终不会渲染到真实的 DOM 节点当中,而是取其子节点进行渲染。当执行完 initRender 方法后,vue 实例上已经有相关 slot 对应的节点信息,接下来开始完成 my-component 的渲染工作。
首先完成对应 my-component 的模板的编译工作,并生成对应的 render 函数:
render 函数执行后生成对应的 VNode,其中
_t("demo")
方法即完成 slot 的渲染工作:在 renderSlot 方法中首先判断是否为 scopedSlot,如果不是那么便获取 vue 实例上 $slots 所对应的具名 slot 的 VNode 并返回。后面的流程便是走正常的组件渲染的过程。不过需要注意的是这里获取到的 VNode 实际上在父组件的作用域当中就已经生成好了,即 slot 的作用域属于父组件。
作用域插槽
有时候我们希望插槽能在子组件的作用域中进行编译,这样自定义组件能获得更多的拓展功能。在讲作用域插槽前还是先看一个作用域插槽的相关例子:
在 my-component 组件当中传递了一个 message 属性进去,然后在 slot 当中通过 slotProps.message 去获取从父组件传递到插槽内部的属性值。
首先在模板编译成 render 函数的生成 VNode 的过程当中:
作用域插槽在模板的编译过程当中,并非直接编译成生成 VNode,并挂载至自定义组件 my-component 的 children 当中,而是缓存至 my-component 的 data.scopedSlots 属性中:
这个时候 slot 的 VNode 并没有生成,而是被一个函数包裹起来,缓存在 scopedSlots 属性上。接下来进行 my-component 组件的渲染,完成模板编译成 render 函数:
调用 _t(即 renderSlot)方法来完成对具名的作用域插槽的渲染,这里需要注意的是传入了在 my-component 作用域当中定义的 message,再回到上面的 renderSlot 方法,在作用域插槽生成 VNode 的过程当中,即接收来自父组件传入的数据,所以在作用域插槽当中能通过 slotProps.message 访问到父组件上定义的 message 属性的值。当作用域插槽在父组件作用域内完成 VNode 的生成后,接下来仍然就是组件的递归渲染了,在这里就不赘述了。
总结
以上通过源码分析了解了关于普通插槽和作用域插槽的不同在于,普通插槽是在自定义组件的父组件编译和生成 VNode 的时候便直接生成了自身的 VNode,因此其作用域处于自定义组件的父组件当中,而作用域插槽在自定义组件的父组件编译和生成 VNode 的时候并没有直接生成自身的 VNode,而是作为自定义组件 data.scopedSlots 属性缓存起来。当自定义组件自身开始编译渲染的时候,这时会取出对应的作用域插槽函数并执行生成对应的 VNode,这个时候所处的作用域为自定义组件内,因为作用域插槽可以获取自定义组件传递进来的数据。