Open theydy opened 3 years ago
基础用法
var B = {
template: `<div class="B"><slot name="b-slot">BBB</slot></div>`
}
var vm = new Vue({
el: '#app',
components: {
B
},
data () {
return { message: 'hello world' }
}
template: `<div id="app"><B><template #b-slot>{{ message }}</template></B></div>`
})
// 最终渲染结果
<div class="B">hello world</div>
父组件处理的不同
此时 vm 构造函数如下,可以看到具名插槽分发的内容不是做为 vnode 的 children,而是放在 vnode 的 data.scopedSlots
属性下,具体的内容通过一个 fn 函数返回。
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ attrs: { id: "app" } },
[
_c("B", {
scopedSlots: _vm._u([
{
key: "b-slot",
fn: function() {
return [_vm._v(_vm._s(_vm.message))]
},
proxy: true
}
])
})
],
1
)
}
通过 _u
函数即 resolveScopedSlots
处理后,返回的 scopedSlots 格式如下:
{
scopedSlots: {
$stable: true
'b-slot': function() {
return [_vm._v(_vm._s(_vm.message))]
}
}
}
最后在子组件占位 vnode 中 data.scopedSlots
就是这样,这里和普通插槽不同的是,普通插槽父组件分发的内容最后存在 vnode.componentOptions.children
下,而具名插槽是放在 vnode.data.scopedSlots
下。
子组件处理的不同
vnode 上的 scopedSlots
最后经过处理会保存在子组件实例的 $scopedSlots
上。
子组件的渲染函数:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ staticClass: "B" },
[_vm._t("b-slot", [_vm._v("BBBBBB")])],
2
)
}
_t
即 renderSlot
, scopedSlotFn = this.$scopedSlots[name];
通过 b-slot
这个 key 找到对应父组件分发的插槽函数,最后返回的 nodes 就是函数的返回值即 [_vm._v(_vm._s(_vm.message))]
。
function renderSlot (
name,
fallback, // slot 的默认内容
props,
bindObject
) {
// ...
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
// ...
nodes = scopedSlotFn(props) || fallback;
}
// ...
return nodes
}
后续和普通插槽 patch 相同。
基础用法
var C = {
template: `<div class="C"><slot name="msg" :message="message" /></div>`
data () {
return {
message: 'CCCC'
};
}
}
var vm = new Vue({
el: '#app',
components: {
C
},
data () {
return { message: 'hello world' }
}
template: `<div id="app"><C><template #msg="slotProps">{{ slotProps.message }}</template></C></div>`
})
// 最终渲染结果
<div class="C">CCCC</div>
作用域插槽在 2.6 后和具名插槽原理类似。
父组件处理
vm render 函数如下,可以看到和具名插槽差不多,只是在 fn
函数多了个参数。
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ attrs: { id: "app" } },
[
_c("C", {
scopedSlots: _vm._u([
{
key: "msg",
fn: function(slotProps) {
return [_vm._v(_vm._s(slotProps.message))]
}
}
])
})
],
1
)
}
子组件处理
子组件的 render 函数如下,可以看到 _t
即 renderSlot
多了第三个参数 props,里面记录的就是我们在子组件中传入的数据。
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ staticClass: "C" },
[_vm._t("msg", null, { message: _vm.message })],
2
)
}
接着进入 renderSlot
,前面贴的都是简化后的 renderSlot
逻辑,现在来看下完整的 renderSlot
函数代码。
function renderSlot (
name,
fallback, // slot 的默认内容
props,
bindObject
) {
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
if (scopedSlotFn) { // scoped slot
props = props || {};
if (bindObject) {
if (!isObject(bindObject)) {
warn(
'slot v-bind without argument expects an Object',
this
);
}
props = extend(extend({}, bindObject), props);
}
nodes = scopedSlotFn(props) || fallback;
} else {
nodes = this.$slots[name] || fallback;
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
首先通过 render 函数可以知道此时 name
的值是 msg
,fallback
是 null
因为我们没有默认插槽内容,props 是 { message: _vm.message }
,bindObject
是 undefined
。
最重要的就是这两句。
var scopedSlotFn = this.$scopedSlots[name];
nodes = scopedSlotFn(props)
把 props 做为参数传给 scopedSlotFn
,所以父组件中可以访问子组件的作用域。
2.6 后其实可以不看 $slots 只看 $scopedSlots。
所有的 $slots 现在都会作为函数暴露在 $scopedSlots 中。如果你在使用渲染函数,不论当前插槽是否带有作用域,我们都推荐始终通过 $scopedSlots 访问它们。这不仅仅使得在未来添加作用域变得简单,也可以让你最终轻松迁移到所有插槽都是函数的 Vue 3。
普通插槽
基础用法
父组件处理
首先 vm new Vue 根实例后,进入 vm 的挂载阶段,首先需要执行 render 函数生成 vnode,然后 patch 生成真实 DOM,此时的 render 函数如下:
可以看到 vm 最后
_c
执行前需要先执行 childern 的 vnode 生成(其实应该是 A 组件的占位 vnode)_c("A", [_vm._v("普通插槽")])
,_c
、_h
函数都是对于 createElement 函数的封装,在initRender
中都通过闭包保存了当前的 vm 实例做为上下文。接着进入 A 组件占位 vnode 生成流程,在
createElement
函数中,vnode = createComponent(Ctor, data, context, children, tag);
这里暂时只需要知到传入的 Ctor、propsData 这些都放在
componentOptions
即可。到此为止,父组件的处理完成。
子组件处理
在父组件 patch 的过程中,碰到子组件占位符 vnode,createComponent → init 钩子
进入子组件初始化、挂载阶段。
在
_init
函数中,因为是子组件,所以会执行initInternalComponent
方法,拿到父组件拥有的相关配置信息,并赋值给子组件自身的配置选项。可以看到,最终占位 vnode 上的
componentOptions
都被放进子组件实例的$options
中了。children
放在opts._renderChildren
上。继续 _init 函数的流程,进入
initRender
方法,在这个过程中会将配置的_renderChildren
属性做规范化处理,并将它赋值给子组件实例上的$slot
属性。随后继续子组件的挂载流程,先生成 render 函数,这里会对
slot
标签做处理,使用_t
函数包裹。_t
其实是renderSlot
函数的缩写,对于普通插槽,renderSlot
其实就是通过default
取到 vnode 返回即可。然后就是正常的子组件 patch 过程。
普通插槽使用默认内容
区别在与子组件的 render 函数现在变成这样
多了
[_vm._v("普通插槽默认内容")]
这段。对应在renderSlot
的第二个参数就是fallback
。如果插槽中使用了父组件的响应式属性
此时 vm 的构造函数是这样:
可以看到
[_vm._v(_vm._s(_vm.message))]
这里通过_vm.message
取到了父组件的属性值。父组件模板的内容在父组件编译阶段就确定了,并且保存在
componentOptions
属性中,而子组件有自身初始化init
的过程,这个过程同样会进行子作用域的模板编译,因此两部分内容是相对独立的。