插槽
在Vue 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法(即v-slot指令)。它取代了 slot和slot-scope。
匿名和具名插槽slot
父子组件分别以
<app-layout>另一个主要段落</app-layout>
和
<div class="container"><slot></slot></div>
为例。 父组件生成的render函数为:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('app-layout',[_v("另一个主要段落")])],1)}
})
调用:
// Ctor为app-layout的子组件构造函数
vnode = createComponent(Ctor, data, context, children, tag);
// 创建Vnode
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
app-layout组件生成的Vnode为:
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options)
children: [VNode],
listeners: undefined,
propsData: undefined,
tag: "app-layout"
},
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {on: undefined, hook: {…}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
app-layout的子组件生成的render函数为:
(function anonymous(
) {
with(this){return _c('div',{staticClass:"container"},[_t("default")],2)}
})
this指向app-layout父组件的构造函数。在子组件初始化过程中的initRender会对slot进行如下处理:
vm.$slots = resolveSlots(options._renderChildren, renderContext);
其中options._renderChildren表示一个包含app-layout子组件VNode的数组,具体函数为
function resolveSlots (
children,
context
) {
if (!children || !children.length) {
return {}
}
var 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;
var slot = (slots[name] || (slots[name] = []));
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
(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
}
返回{default: [VNode]}
,此时vm.$slot = {default: [VNode]}
和vm.$scopedSlots = emptyObject
,接下来进入编译阶段,其中的默认插槽slot被genSlot函数解析成
_t("default")
在子组件中的_render函数中会格式化插槽的表示
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
生成
vm.$scopedSlots = {
default: ƒ (),
$hasNormal: true,
$key: undefined,
$stable: false
}
接着看_t函数:
// name1为default, 其他为undefined
function renderSlot (
name,
fallback,
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 {
// 从this.$slots中获取default值,是app-layout组件的text虚拟节点内容[VNode]
nodes = this.$slots[name] || fallback;
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
接着将子组件内容挂载到父级,然后挂载到body上,最后再删除初始标签内容。具名插槽同理,只是将default改成对应的名字xx。
作用域插槽slot-scope
父子组件分别以
<div id="app"><app-layout :items="items"><template slot-scope="list"><div>{{ list.data }}</div></template></app-layout></div>
和
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
为例。 在模板解析template尾标签时,执行closeElement会对template模板组件会作如下处理:
function processSlotContent (el) {
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope');
/* istanbul ignore if */
if (slotScope) {
warn$2(
"the \"scope\" attribute for scoped slots have been deprecated and " +
"replaced by \"slot-scope\" since 2.5. The new \"slot-scope\" attribute " +
"can also be used on plain elements in addition to <template> to " +
"denote scoped slots.",
el.rawAttrsMap['scope'],
true
);
}
// 此时 el.slotScope = 'list'
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');
}
}
此时会在element元素上挂载: { slotScope: "list" } 接着closeElement函数继续处理:
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
var name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
currentParent.children.push(element);
element.parent = currentParent;
生成:
{
scopedSlots: {
"default": {
attrsList: []
attrsMap: {slot-scope: "list"},
children: [{…}],
end: 106,
parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: false,
rawAttrsMap: {
slot-scope: {
end: 68,
name: "slot-scope",
start: 51,
value: "list"
}
},
slotScope: "list",
start: 41,
tag: "template",
type: 1
}
}
}
生成代码过程中经过genScopedSlots, genScopedSlot处理
function genScopedSlot (
el,
state
) {
var isLegacySyntax = el.attrsMap['slot-scope'];
if (el.if && !el.ifProcessed && !isLegacySyntax) {
return genIf(el, state, genScopedSlot, "null")
}
if (el.for && !el.forProcessed) {
return genFor(el, state, genScopedSlot)
}
var slotScope = el.slotScope === emptySlotScopeToken
? ""
: String(el.slotScope);
var fn = "function(" + slotScope + "){" +
"return " + (el.tag === 'template'
? el.if && isLegacySyntax
? ("(" + (el.if) + ")?" + (genChildren(el, state) || 'undefined') + ":undefined")
: genChildren(el, state) || 'undefined'
: genElement(el, state)) + "}";
// reverse proxy v-slot without scope on this.$slots
var reverseProxy = slotScope ? "" : ",proxy:true";
return ("{key:" + (el.slotTarget || "\"default\"") + ",fn:" + fn + reverseProxy + "}")
}
生成
"{
attrs:{"items":items},
scopedSlots:_u([{
key:"default",
fn:function(list){
return [_c('div',[_v(_s(list.data))])]}}]),"
完整的render函数为:
with(this){
return _c('div',
{
attrs:{"id":"app"}
},
[
_c('app-layout',{
attrs:{"items":items},
scopedSlots:_u([{key:"default", fn:function(list){
return [_c('div',[_v(_s(list.data))])]
}}])
})
] ,1)
}
其中_u函数表示
function resolveScopedSlots (
fns, // see flow/vnode
res,
// the following are added in 2.6
hasDynamicKeys,
contentHashKey
) {
res = res || { $stable: !hasDynamicKeys };
for (var i = 0; i < fns.length; i++) {
var slot = fns[i];
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys);
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
slot.fn.proxy = true;
}
res[slot.key] = slot.fn;
}
}
if (contentHashKey) {
(res).$key = contentHashKey;
}
return res
}
fns为[{fn: ƒ (list), key: "default"}]
,该函数返回
{
$stable: true,
default: ƒ (list)
}
app-layout生成的VNode为:
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options),
children: undefined,
listeners: undefined,
propsData: {items: Array(1)},
tag: "app-layout"
},
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {
attrs: {},
scopedSlots: {
$stable: true,
default: ƒ (list)
},
on: undefined, hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
layout子组件的render函数为:
with(this){
return _c(
'div',
[
_l((items),function(item, index){
return _t("default", null,{"data": item.text})
})
],
2)
}
_l函数为:
function renderList (
val,
render
) {
var ret, i, l, keys, key;
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length);
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i);
}
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i);
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = [];
var iterator = val[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
ret.push(render(result.value, ret.length));
result = iterator.next();
}
} else {
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
ret[i] = render(val[key], key, i);
}
}
}
if (!isDef(ret)) {
ret = [];
}
(ret)._isVList = true;
return ret
}
对数组的每一个元素,执行:
return _t("default", null,{"data": item.text})
此过程会触发getter,添加依赖,_t函数为:
// name为default,props为{data: 'text1'}
function renderSlot (
name,
fallback,
props,
bindObject
) {
// f函数
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);
}
// 获取渲染后的[VNode]
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
}
}
接着将生成的VNode生成元素节点,并将子组件内容挂载到父级,然后挂载到body上,最后再删除初始标签内容。 现在我们来对比一下普通插槽和作用域插槽的区别:// 普通插槽
slots: {
xxoo: h('div')
}
// 作用域插槽
scopedSlots: {
xxoo: (scopedData) => h('div', scopedData.a)
}
作用域插槽和普通插槽的区别在于,子组件拿到它的时候它还是一个函数,只有你执行该函数,它才会返回要渲染的内容(即vnode),所以可以统一为:
// 普通插槽
slots: {
xxoo: () => h('div')
}
这个也是Vue2.6.*中的改变之处。
v-slot原理
父子组件分别以
<div id="app"><app-layout :items="items"><template v-slot:default="list"><div>{{ list.data }}</div></template></app-layout></div>
和
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
为例。 父组件在模板解析阶段,编译闭合标签template时,closeElement函数中会执行processSlotContent,如下:
// 2.6 v-slot syntax
{
if (el.tag === 'template') {
// v-slot on <template>
// 作用在template上,slotRE = /^v-slot(:|$)|^#/
var slotBinding = getAndRemoveAttrByRegex(el, slotRE);
if (slotBinding) {
var ref = getSlotName(slotBinding);
var name = ref.name;
var dynamic = ref.dynamic;
el.slotTarget = name;
el.slotTargetDynamic = dynamic;
el.slotScope = slotBinding.value || emptySlotScopeToken; // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
var slotBinding$1 = getAndRemoveAttrByRegex(el, slotRE);
if (slotBinding$1) {
// add the component's children to its default slot
var slots = el.scopedSlots || (el.scopedSlots = {});
var ref$1 = getSlotName(slotBinding$1);
var name$1 = ref$1.name;
var dynamic$1 = ref$1.dynamic;
var slotContainer = slots[name$1] = createASTElement('template', [], el);
slotContainer.slotTarget = name$1;
slotContainer.slotTargetDynamic = dynamic$1;
slotContainer.children = el.children.filter(function (c) {
if (!c.slotScope) {
c.parent = slotContainer;
return true
}
});
slotContainer.slotScope = slotBinding$1.value || emptySlotScopeToken;
// remove children as they are returned from scopedSlots now
el.children = [];
// mark el non-plain so data gets generated
el.plain = false;
}
}
}
该函数在element元素中添加属性:
{
...
slotScope: "list",
slotTarget: ""default"",
slotTargetDynamic: false,
...
}
然后在app-layout组件的closeElement函数中增加属性:
...
scopedSlots: {"default": {
attrsList: [],
attrsMap: {v-slot:default: "list"},
children: [{
attrsList: [],
attrsMap: {},
children: [{…}],
end: 99,
parent: {type: 1, tag: "template", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: true,
rawAttrsMap: {},
start: 73,
tag: "div",
type: 1}],
end: 110,
parent: {type: 1, tag: "app-layout", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
plain: false,
rawAttrsMap: {v-slot:default: {
end: 72,
name: "v-slot:default",
start: 51,
value: "list"
}},
slotScope: "list",
slotTarget: ""default"",
slotTargetDynamic: false,
start: 41,
tag: "template",
type: 1
}}
...
接着在生成代码阶段,genElement中的genData$2函数会处理上面生成的scopedSlots属性值:
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
该函数具体为
function genScopedSlots (
el,
slots,
state
) {
var generatedSlots = Object.keys(slots)
.map(function (key) { return genScopedSlot(slots[key], state); })
.join(',');
return ("scopedSlots:_u([" + generatedSlots + "]" + (needsForceUpdate ? ",null,true" : "") + (!needsForceUpdate && needsKey ? (",null,false," + (hash(generatedSlots))) : "") + ")")
}
genScopedSlot函数的作用是生成如下代码:
"{key:"default",fn:function(list){return [_c('div',[_v(_s(list.data))])]}}"
返回了
"{
attrs:
{
"items":items
},
scopedSlots:_u([{
key:"default",
fn:function(list){
return [_c('div',[_v(_s(list.data))])]
}
}]),"
生成完整的render函数为:
with(this){
return _c('div',
{attrs:{"id":"app"}},
[_c('app-layout',{
attrs:{"items":items},
scopedSlots:_u([{
key:"default",
fn:function(list){return [_c('div',[_v(_s(list.data))])]}}])
})
],1)
}
生成VNode时,_u即resolveScopedSlots函数处理
function resolveScopedSlots (
fns, // see flow/vnode
res,
// the following are added in 2.6
hasDynamicKeys,
contentHashKey
) {
res = res || { $stable: !hasDynamicKeys };
for (var i = 0; i < fns.length; i++) {
var slot = fns[i];
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys);
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
slot.fn.proxy = true;
}
res[slot.key] = slot.fn;
}
}
if (contentHashKey) {
(res).$key = contentHashKey;
}
return res
}
其中函数参数fns=[{key: "default", fn: ƒ}]
和res={$stable: true}
。返回值为
{$stable: true, default: ƒ (list)}
生成虚拟DOM节点的函数vnode = createComponent(Ctor, data, context, children, tag)
,其中data为
{
attrs: {},
hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
on: undefined,
scopedSlots: {
$stable: true,
default: ƒ (list)
}
}
app-layout的VNode为:
{
syncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options)
children: undefined,
listeners: undefined,
propsData: {},
tag: "app-layout",
}
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
data: {attrs: {}, scopedSlots: {
$stable: true,
default: ƒ (list)
}, on: undefined, hook: {…}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-app-layout",
text: undefined,
child: undefined
}
子组件与slot-scope一致的render函数为:
with(this){return _c('div',[
_l((items),function(item,index){
return _t("default",null,{"data":item.text})
})],2)
}
同样的经过_t函数处理
function renderSlot (
name,
fallback,
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
}
}
this.$scopedSlots为:
{
default: ƒ (),
$hasNormal: false,
$key: undefined,
$stable: true
}
props为{data: "text1"}
,返回的nodes为子组件的[VNode],最后挂载子组件到父组件,并将父级组件挂载到body页面,再删除老的元素节点。此处理过程与slot-scope一致。
匿名插槽模板
<div id="app"><app-layout v-slot:default>另一个主要段落</app-layout></div>
会被编译为:
"_c('app-layout',{
scopedSlots:_u([{key:"default",fn:function(){return [_v("另一个主要段落")]
},proxy:true}])
})"
其他处理过程一致。
总结
在Vue2.5.*之前,如果是普通插槽就直接访问的是VNode,而如果是作用域插槽,由于子组件需要在父组件访问子组件的数据,所以父组件下是一个未执行的函数(slotScope) => return h('div',slotScope.msg),接受子组件的slotProps参数,在子组件渲染实例时会调用该函数传入数据。在2.6之后,两者合并,普通插槽也变成一个函数,只是不接受参数了。