slot
In Vue 2.6.0, we introduced a new uniform syntax (i.e., v-slot directives) for named slots and scoped slots. It replaces slot and slot-scope.
Anonymous and named slots Slot
The parent and child components are respectively
<app-layout> Another main paragraph </app-layout>Copy the code
and
<div class="container"><slot></slot></div>
Copy the code
As an example. The render function generated by the parent component is:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('app-layout',[_v("The other main paragraph")])],1)}
})
Copy the code
Call:
Vnode = createComponent(Ctor, data, Context, children, tag); vnode = createComponent(Ctor, data, Context, children, tag); 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
);
Copy the code
The Vnode generated by the app-Layout component is:
{ asyncFactory: undefined, asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ VueComponent(options) children: [VNode], notice:"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
}
Copy the code
The render function generated by the children of app-Layout is:
(function anonymous(
) {
with(this){return _c('div',{staticClass:"container"},[_t("default")],2)}
})
Copy the code
This points to the constructor of the app-Layout parent. InitRender during child component initialization does the following for slot:
vm.$slots = resolveSlots(options._renderChildren, renderContext);
Copy the code
Options. _renderChildren represents an array containing app-Layout child 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 nameThe $1 in slots) {
if (slots[nameThe $1].every(isWhitespace)) {
delete slots[nameThe $1]; }}return slots
}
Copy the code
$slot = {default: [VNode]} vm.$slot = {default: [VNode]} vm.$slot = emptyObject
_t("default")
Copy the code
The _render function in the child component formats the representation of the slot
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
Copy the code
generate
vm.$scopedSlots= {default: ƒ (),$hasNormal: true.$key: undefined,
$stable: false
}
Copy the code
Then look at the _t function:
// Name1 is default, others are undefinedfunction 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{/ / from this.$slotsIs the text virtual node content of the app-layout component [VNode] Nodes = this.$slots[name] || fallback;
}
var target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes)
} else {
return nodes
}
}
Copy the code
The child component content is then mounted to the parent, then to the body, and finally the initial tag content is deleted. For named slots, change the name of default to xx.
Scope slot slot-scope
The parent and child components are respectively
<div id="app"><app-layout :items="items"><template slot-scope="list"><div>{{ list.data }}</div></template></app-layout></div>
Copy the code
and
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
Copy the code
As an example. When the template template tail tag is resolved by the template template component, executing closeElement does the following:
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 " +
Replace 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'); }}Copy the code
{slotScope: “list”} The closeElement function continues:
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;Copy the code
Generation:
{
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}}}Copy the code
GenScopedSlots and genScopedSlot are used to generate the code
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 + "}")}Copy the code
generate
"{ attrs:{"items":items}, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])]}}]),"
Copy the code
The complete render function is:
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)
}
Copy the code
Where the _u function represents
function resolveScopedSlots (
fns, // see flow/vnode
res,
// the following are added in2.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
}
Copy the code
If the FNS is [{fn: ƒ (list), key: “default”}], this function returns
{
$stable: true, default: ƒ (list)}Copy the code
The VNode generated by app-Layout is:
{ asyncFactory: undefined, asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ VueComponent(options), children: misses, listeners: misses, 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
}
Copy the code
The render function for the Layout child is:
with(this){
return _c(
'div',
[
_l((items),function(item, index){
return _t("default", null,{"data": item.text})
})
],
2)
}
Copy the code
The _l function is:
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
}
Copy the code
For each element of the array, execute:
return _t("default", null,{"data": item.text})
Copy the code
This triggers the getter, adds the dependency, and the _t function says:
// Name is default, and props is {data:'text1'}
function renderSlot (
name,
fallback,
props,
bindVar 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] after rendering 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
}
}
Copy the code
The generated VNode then generates the element node, mounts the child component content to the parent, then to the body, and finally deletes the initial tag content. Now let’s compare the difference between a regular slot and a scoped slot: // Regular slot
slots: {
xxoo: h('div'} // scope slots scopedSlots: {xxoo: (scopedData) => h('div', scopedData.a)
}
Copy the code
The difference between a scoped slot and a regular slot is that it is still a function when the child gets hold of it, and only if you execute the function does it return the content to be rendered (i.e. a vNode), so it can be unified as:
{xxoo: () => h()'div')}Copy the code
This is also a change in Vue2.6.*.
V – slot principle
The parent and child components are respectively
<div id="app"><app-layout :items="items"><template v-slot:default="list"><div>{{ list.data }}</div></template></app-layout></div>
Copy the code
and
<div><slot v-for="(item,index) in items" :data="item.text"></slot></div>`
Copy the code
As an example. ProcessSlotContent is executed in the closeElement function when compiling the closed tag template in the parent component’s template parsing phase, as follows:
// 2.6 v-slot syntax
{
if (el.tag === 'template') {/ / v - slot on < template > / / role on the 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 slotBindingThe $1 = getAndRemoveAttrByRegex(el, slotRE);
if (slotBindingThe $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; }}}Copy the code
This function adds attributes to the element:
{... slotScope:"list",
slotTarget: ""default"",
slotTargetDynamic: false. }Copy the code
Then add the attribute to the closeElement function of the app-Layout component:
. scopedSlots: {"default": {
attrsList: [],
attrsMap: {v-slot:default: "list"}, children: [{attrsList: [], attrsMap: {}, children: [{...}], end: 99, the 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}}...Copy the code
Then in the code generation phase, the genData$2 function in genElement processes the scopedSlots attribute value generated above:
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
Copy the code
The function is specified as
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))) : "") + ")")}Copy the code
The genScopedSlot function generates the following code:
"{key:"default",fn:function(list){return [_c('div',[_v(_s(list.data))])]}}"
Copy the code
Returned to the
"{ attrs: { "items":items }, scopedSlots:_u([{ key:"default", fn:function(list){ return [_c('div',[_v(_s(list.data))])] } }]),"
Copy the code
The complete render function is generated as follows:
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)
}
Copy the code
When generating vNodes, _u is handled by the resolveScopedSlots function
function resolveScopedSlots (
fns, // see flow/vnode
res,
// the following are added in2.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
}
Copy the code
FNS =[{key: “default”, fn: ƒ}] and res={$stable: true}. The returned value is {$stable: true, default: ƒ (list)} Vnode = createComponent(Ctor, data, Context, children, tag), where data is
{attrs: {}, hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}, on: undefined, scopedSlots: {$stable: true, default: ƒ (list)}}Copy the code
The VNode of app-layout is:
{ syncFactory: undefined, asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ VueComponent(options) children: missing information, listeners:"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
}
Copy the code
The render function of the child component consistent with the slot-scope is:
with(this){return _c('div',[
_l((items),function(item,index){
return _t("default",null,{"data":item.text})
})],2)
}
Copy the code
Again, the _t function
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
}
}
Copy the code
This $scopedSlots as follows:
{default: ƒ (),$hasNormal: false.$key: undefined,
$stable: true
}
Copy the code
Function {data: “text1”}, return Nodes as [VNode] of the child component, mount the child component to the parent component, mount the parent component to the body page, and delete the old element node. This processing is consistent with slot-scope. Anonymous Slot template
<div id="app"><app-layout v-slot:default> Another main paragraph </app-layout></div>Copy the code
Will compile to:
"_c('app-layout',{ scopedSlots:_u([{key:"default",fn:function(){return [_v("Another major paragraph")] },proxy:true}]) })"
Copy the code
Other processes are the same.
conclusion
Prior to Vue2.5.*, a normal slot would access VNode directly, whereas a scoped slot would need to access the child’s data from the parent. So under the parent component is an unexecuted function (slotScope) => return h(‘div’,slotScope.msg), which accepts the slotProps parameter of the child component and is called to pass in data when the child component renders the instance. After 2.6, the two were merged and the normal slot became a function, but it took no arguments.