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.