Today we will focus on the design and implementation of the Slots feature commonly used in vue.js.

This article is divided into three parts: normal slot, scope slot and v-slot syntax of vue.js 2.6.x.

This is an advanced article. For those of you who are not familiar with the use of Slots, we recommend you go to vue.js first.

1 Common slot

Let’s start with a simple example of the use of Slots.

<! -- SlotDemo.vue -->
<template>
  <div class="slot-demo">
    <slot>this is slot default content text.</slot>
  </div>
</template>
Copy the code

Render this component directly on the page as shown below:

Next, we overwrite the content of Slots:

<slot-demo>this is slot custom content.</slot-demo>

After re-rendering, it should look like this:

The use of Slots is obvious to all of us. So what is the logic behind vue.js? Let’s take a look at vue.js’s low-level implementation of Slots.

1.1 vm.$slots

The $slots attribute is defined in the Vue.js Component interface:

$slots: { [key: string]: Array<VNode> };

Print $slots from the above example on the console:

Next we’ll show you how the Slots content is rendered and converted to the object shown above.

1.2 renderSlot

$Slots. Let’s parse the logic of renderSlot. First, let’s look at the parameters of renderSlot.

export function renderSlot (
  name: string, // slotName slotName
  fallback: ?Array<VNode>, // Slot default content to generate vNode array
  props: ?Object./ / props object
  bindObject: ?Object // v-bind the binding object
): ?Array<VNode> {}
Copy the code

Instead of looking at the logic of scoped-slot, we’ll look at the logic of plain Slots:

const slotNodes = this.$slots[name]
nodes = slotNodes || fallback
return nodes
Copy the code

$slots[name] = this.$slots[name]; this.$slots[name] = this.

1.3 resolveSlots

Now, for those of you who don’t know where this.$slots is defined, we need to look at another method, resolveSlots

export function resolveSlots (
  children: ?Array<VNode>, // Children of the parent nodecontext: ? Component// The parent node's context, that is, the vm instance of the parent component
) :{ [key: string]: Array<VNode> } {}
Copy the code

Having looked at the definition of resolveSlots, let’s move on to the logic behind it.

We define an empty object from slots. If the children parameter does not exist, we return:

const slots = {}
if(! children) {return slots
}
Copy the code

If present, children are traversed:

for (let i = 0, l = children.length; i < l; i++) {
  const child = children[i]
  const data = child.data
  
  // If data.slot exists, add the slot name as the key and child as the value directly to the slots
  if((child.context === context || child.fnContext === context) && data && data.slot ! =null
  ) {
    const name = data.slot
    const slot = (slots[name] || (slots[name] = []))
    // The child tag is the template tag
    if (child.tag === 'template') {
      slot.push.apply(slot, child.children || [])
    } else {
      slot.push(child)
    }
    
  // If data.slot does not exist, throw the child directly into slot.default
  } else {
    (slots.default || (slots.default = [])).push(child)
  }
}
Copy the code

Slots fetches the value, filters out attributes that contain only whitespace characters, and returns:

// ignore slots that contains only whitespace
for (const name in slots) {
  if (slots[name].every(isWhitespace)) {
    delete slots[name]
  }
}
return slots
Copy the code
// isWhitespace logic
function isWhitespace (node: VNode) :boolean {
  return(node.isComment && ! node.asyncFactory) || node.text ===' '
}
Copy the code

1.4 initRender

The initialization and assignment of the slots variable was explained above. The following initRender method initializes vm.$slots.

// src/core/instance/render.js
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
genSlot()
Copy the code

After reading the above code, some people are sure to ask: how do you parse the contents of an object?

1.5 genSlot

Don’t worry, we’ll go over the logic of Slots parsing. Without further ado, let’s go to the code:

function genSlot (el: ASTElement, state: CodegenState) :string {
  const slotName = el.slotName || '"default"' // Set slotName to 'default'.
  const children = genChildren(el, state) // Generate for children
  let res = `_t(${slotName}${children ? `,${children}` : ' '}`
  const attrs = el.attrs && ` {${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(', ')}} ` // Convert attrs to object form
  const bind = el.attrsMap['v-bind'] // Get the V-bind attribute on slot
  
  // If attrs or bind exists but children are not available, set the second argument to null
  if((attrs || bind) && ! children) { res +=`,null`
  }
  
  // Use attrs as the third argument to '_t()' if attrs exists.
  if (attrs) {
    res += `,${attrs}`
  }
  
  // if attrs is present, bind takes the third argument, otherwise bind takes the fourth argument (scoped-slot logic)
  if (bind) {
    res += `${attrs ? ' ' : ',null'}.${bind}`
  }
  return res + ') '
}
Copy the code

The slotName above is assigned in the processSlot function, and the slotTarget used by the parent component during compilation is also processed here:

// src/compiler/parser/index.js
function processSlot (el) {
  if (el.tag === 'slot') {
    // Get name directly from attr
    el.slotName = getBindingAttr(el, 'name')
    // ...
  }
  // ...
  const slotTarget = getBindingAttr(el, 'slot')
  if (slotTarget) {
    // If slotTarget exists, select the slot value of the named slot, otherwise select 'default'.
    el.slotTarget = slotTarget === '" "' ? '"default"' : slotTarget
    if(el.tag ! = ='template' && !el.slotScope) {
      addAttr(el, 'slot', slotTarget)
    }
  }
}
Copy the code

SlotTarget is then used in genData function for data splicing:

if(el.slotTarget && ! el.slotScope) { data +=`slot:${el.slotTarget}, `
}
Copy the code

The parent component generates the following code

with(this) {
  return _c('div', [
    _c('slot-demo'),
    {
      attrs: { slot: 'default' },
      slot: 'default'
    },
    [ _v('this is slot custom content.')]])}Copy the code

Then when el.tag is slot, execute genSlot directly:

else if (el.tag === 'slot') {
  return genSlot(el, state)
}
Copy the code

Following our example, the child component will eventually generate the following code:

with(this) {
  // _c => createElement ; _t => renderSlot ; _v => createTextVNode
  return _c(
    'div',
    {
      staticClass: 'slot-demo'
    },
    [ _t('default', [ _v('this is slot default content text.')]])}Copy the code

2 Scope slot

We have seen above how vue.js handles and transforms ordinary slot tags. Let’s take a look at the implementation logic of scoped slots.

2.1 vm.$scopedSlots

The $scopedSlots attribute is defined in the vue. js Component interface:

$scopedSlots: { [key: string] :() = > VNodeChildren };
Copy the code

Where VNodeChildren is defined as follows:

declare type VNodeChildren = Array<? VNode |string | VNodeChildren> | string;
Copy the code

Here’s a related example:

<template>
  <div class="slot-demo">
    <slot text="this is a slot demo , " :msg="msg"></slot>
  </div>
</template>

<script>
export default {
  name: 'SlotDemo',
  data () {
    return {
      msg: 'this is scoped slot content.'}}}</script>
Copy the code

Then use:

<template>
  <div class="parent-slot">
    <slot-demo>
      <template slot-scope="scope">
        <p>{{ scope.text }}</p>
        <p>{{ scope.msg }}</p>
      </template>
    </slot-demo>
  </div>
</template>
Copy the code

The effect is as follows:

We can see the usage from the example, binding the text and: MSG attributes to the slot tag of the child component. The parent component then uses the slot to read the value of the slot property using the slot-scope property.

2.2 processSlot

To mention the processSlot function’s processing logic for slot-scope:

let slotScopeif (el.tag === 'template') {  
  slotScope = getAndRemoveAttr(el, 'scope')  // Compatible with slot scope usage prior to 2.5 (there is a warning here, which I ignored)
  el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')}else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {  
  el.slotScope = slotScope 
}
Copy the code

As we can see from the above code, vue.js reads the slot-scope property directly and assigns it to the slotScope property of the AST abstract syntax tree. A node with a slotScope attribute is mounted directly on the parent node’s scopedSlots attribute as an object with a slot name of key and value of its own.

else if (element.slotScope) {   
  currentParent.plain = false  
  const name = element.slotTarget || '"default"'  
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
Copy the code

We then assign vm.$scopedSlots to renderMixin as follows:

// src/core/instance/render.js
if (_parentVnode) {  vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject}
Copy the code

The genData function then performs the following logic:

if (el.scopedSlots) {  data += `${genScopedSlots(el, el.scopedSlots, state)}, `}
Copy the code

2.3 genScopedSlots & genScopedSlot

Next we’ll look at the logic in the genScopedSlots function:

function genScopedSlots (slots: { [key: string]: ASTElement }, state: CodegenState) :string {
  // Iterate over the el.scopedSlots object, execute genScopedSlot, and concatenate the results with commas
  // _u => resolveScopedSlots
  return `scopedSlots:_u([The ${Object.keys(slots).map(key => {
      return genScopedSlot(key, slots[key], state)
    }).join(', ')}]) `
}
Copy the code

Then let’s look at how the genScopedSlot function generates the render function string:

function genScopedSlot (key: string, el: ASTElement, state: CodegenState) :string {
  if(el.for && ! el.forProcessed) {return genForScopedSlot(key, el, state)
  }
  // The function takes the value of the slot-scope attribute on the tag (getAndRemoveAttr(el, 'slot-scope'))
  const fn = `function(The ${String(el.slotScope)}) {` +
    `return ${el.tag === 'template'
      ? el.if
        ? `${el.if}?${genChildren(el, state) || 'undefined'}:undefined`
        : genChildren(el, state) || 'undefined'
      : genElement(el, state)
    }} `
  // Key is the slot name, and fn is the generated function code
  return `{key:${key},fn:${fn}} `
}
Copy the code

$scopedSlots = $scopedSlots = $scopedSlots = $scopedSlots

The parent component in the example above will eventually generate the following code:

with(this){
  // _c => createElement ; _u => resolveScopedSlots
  // _v => createTextVNode ; _s => toString
  return _c('div',
    { staticClass: 'parent-slot' },
    [_c('slot-demo',
      { scopedSlots: _u([
        {
          key: 'default',
          fn: function(scope) {
            return [
              _c('p', [ _v(_s(scope.text)) ]),
              _c('p', [ _v(_s(scope.msg)) ])
            ]
          }
        }])
      }
    )]
  )
}
Copy the code

2.4 renderSlot(slot-scope) & renderSlot

The slot-scope logic is ignored when the slot rendering logic is omitted.

export function renderSlot ( name: string, fallback: ? Array<VNode>, props: ? Object, bindObject: ? Object ): ? Array<VNode> { const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { // scoped slot props = props | | {} / /... nodes = scopedSlotFn(props) || fallback } // ... return nodes } resolveScopedSlots()Copy the code

The _u in renderHelps, resolveScopedSlots, has the following logic:

export function resolveScopedSlots ( fns: ScopedSlotsData, // Array<{ key: string, fn: Function } | ScopedSlotsData> res? : Object) : {[key: string] : Function} {res = res | | {} / / traverse FNS arrays, generating a ` key slot name, the value for Function ` Object for (let 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 }Copy the code

I explained the genSlot function above, so scroll up to see it. With our example, the child component generates the following code:

with(this) {
  return _c(
    'div',
    {
      staticClass: 'slot-demo'
    },
    [
      _t('default', null, { text: 'this is a slot demo , ', msg: msg })
    ]
  )
}
Copy the code

So far, we’ve covered normal and scoped slots pretty much. Next, we’ll take a look at the V-slot syntax for vue.js version 2.6.x.

3 v-slot

3.1 Basic Usage

Vue. Js 2.6.x has been available for some time, and the slot-scope slot is replaced by the v-slot instruction. (This is just grammar candy, of course)

Before looking at the implementation logic, let’s take a look at the basic usage through an example.

<template>
  <div class="slot-demo">
    <slot name="demo">this is demo slot.</slot>
    <slot text="this is a slot demo , " :msg="msg"></slot>
  </div>
</template>

<script>
export default {
  name: 'SlotDemo',
  data () {
    return {
      msg: 'this is scoped slot content.'
    }
  }
}
</script>
Copy the code

then

<template>
  <slot-demo>
    <template v-slot:demo>this is custom slot.</template>
    <template v-slot="scope">
      <p>{{ scope.text }}{{ scope.msg }}</p>
    </template>
  </slot-demo>
</template>
Copy the code

Look easy.

3.2 Similarities and differences

Next, let’s take a look at this new feature.

3.2.1 $slots & $scopedSlots

The $slots logic is the same as before:

// $slots const options = vm.$options const parentVnode = vm.$vnode = options._parentVnode const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(options._renderChildren, RenderContext $scopedSlots was modified, If (_parentVnode) {vm.$scopedSlots = normalizeScopedSlots(); normalizeScopedSlots(); _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) }Copy the code

Then, let’s take a look at normalizeScopedSlots. First, let’s look at its definition:

export function normalizeScopedSlots ( slots: { [key: string]: Function} | void, / / a node data attributes scopedSlots normalSlots: {[key: string] : Array < VNode >}, / / the current node under the ordinary slot prevSlots? : {[key: string] : Function} | void / / the current node under the special slot) : any {}Copy the code

First, if slots does not exist, an empty object {} is returned.

if (! slots) { res = {} }Copy the code

PrevSlots is returned directly if it exists and the series criteria are met.

Const hasNormalSlots = object.keys (normalSlots). Length > 0 // Do you have regular slots const isStable = slots? !!!!! slots.$stable : ! HasNormalSlots // slots $stable const key = slots && slots.$key // slots $key else if (isStable && prevSlots && prevSlots ! == emptyObject &&key === prevSlots.$key && // slots $key = prevSlots $key! HasNormalSlots && // Slots does not have normal slots! PrevSlots.$hasNormal // prevSlots does not have normal slots) {return prevSlots}Copy the code

Key, hasNormal, and $stable are directly assigned internally to def methods wrapped in Object.defineProperty using Vue.

Def (res, '$stable', isStable) def(res, '$key', key) def(res, '$hasNormal', hasNormalSlots) NormalSlots, assign key to key, Res let res else {res = {} for (const key in slots) {if (slots[key] &&key [0]! == '$') { res[key] = normalizeScopedSlot(normalSlots, key, slots[key]) } } }Copy the code

NormalSlots is then traversed again. If the key in normalSlots cannot be found in res, the proxyNormalSlot proxy is directly used. Mount slots from normalSlots to res objects.

for (const key in normalSlots) { if (! (key in res)) { res[key] = proxyNormalSlot(normalSlots, key) } } function proxyNormalSlot(slots, key) { return () => slots[key] }Copy the code

Next, let’s look at what the normalizeScopedSlot function does. The method takes three parameters, the first parameter being normalSlots, the second parameter being key, and the third parameter being fn.

Function normalizeScopedSlot(normalSlots, key, fn) {const normalized = function () { Let res = arguments.length? fn.apply(null, arguments) : // fn({}) returns an array of res, which is a single vNode; otherwise, normalizeChildren is executed. Res = res && typeof res === 'object' &&! Array.isArray(res) ? [res] // single vnode : NormalizeChildren (res) return res && (res. Length = = = 0 | | (res) length = = = 1 && res [0]. IsComment) / / slot on v - if relevant processing) ? If (fn.proxy) {object.defineProperty (normalSlots, key, {get: normalized, enumerable: true, configurable: true }) } return normalized }Copy the code

3.2.2 renderSlot

The logic is the same as before, but the warning code has been removed. I won’t elaborate on that here.

3.2.3 processSlot

First, the name of the method used to resolve slot is changed from processSlot to processSlotContent, but the logic is the same as before. I’m just adding some logic to v-slot, so we’re going to stroke this right here. Before we dive into the logic, let’s look at some of the related regex and methods.

  1. Related regulars & functions
Const dynamicArgRE = /^\[.*\]$ V - such as' [items] slotRE matching slot grammar related regular const slotRE = / ^ v - slot (: | $) | # ^ / / / match to the 'v - slot' or 'v - slot:' it is true Export function getAndRemoveAttrByRegex (el: ASTElement, name: RegExp //) {const list = el.attrslist // attrsList is Array<ASTAttr> When we have meet the RegExp directly returns the current corresponding attr / / if the parameter name is incoming slotRE = / ^ v - slot (: | $) | # ^ / / / the match to a 'v - slot' or 'v - slot: XXX' will be returned to their corresponding attr for (let i = 0, l = list.length; i < l; i++) { const attr = list[i] if (name.test(attr.name)) { list.splice(i, 1) Return attr}}} ASTAttr Interface defines Declare Type ASTAttr = {name: string; value: any; dynamic? : boolean; start? : number; end? : number }; CreateASTElement export function createASTElement (tag: string, // attrs: Array < ASTAttr >, the parent: / / attrs Array ASTElement | void / / parent node) : ASTElement {return {type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: Function getSlotName (binding) {// 'v-slot:item' let name = binding.name.replace(slotRE, '') if (! name) { if (binding.name[0] ! == '#') { name = 'default' } else if (process.env.NODE_ENV ! == 'production') {WARN (' V-slot dictate syntax requires a slot name. ', binding)}} // Return a key containing name, Dynamicargre.test (name) = '[item]'; // 'v-slot:[item]'; // Replace: name = '[item]'; //  dynamicArgRE.test(name) ? {name: name.slice(1, -1), dynamic: true} // Intercept variables such as '[item]' into 'item' : {name: '${name}"', dynamic: false}}Copy the code
  1. processSlotContent

So let’s first look at how Slots handles template.

If (el.tag === 'template') {// Match the v-slot instruction bound to the template, Const slotBinding = getAndRemoveAttrByRegex(el, slotRE) // Assign the matched name to slotTarget, Dynamic assigns slotTargetDynamic // slotScope assigns slotBinding. Value or '_empty_' if (slotBinding) {const {name, dynamic } = getSlotName(slotBinding) el.slotTarget = name el.slotTargetDynamic = dynamic el.slotScope = slotBinding.value || emptySlotScopeToken } }Copy the code

If you bind to component instead of template, the v-slot directive and slotName match the same, except that you add the children of the component to its default slot.

Else {// v-slot on component indicates default slot const slotBinding = getAndRemoveAttrByRegex(el, SlotRE) / / add components of children to the slots in the default to the if (slotBinding) {/ / get the current component scopedSlots const slots. = el scopedSlots | | (el.scopedslots = {}) // match name to slotBinding, Dynamic const {name, Dynamic} = getSlotName(slotBinding) // Get the slot name from the slots key and create a template ASTElement below it. Attrs is an empty array, Const slotContainer = slots[name] = createASTElement('template', [], Name and dynamic are assigned to slotTarget and slotTargetDynamic of slotContainer. Rather than el slotContainer. SlotTarget = name slotContainer. SlotTargetDynamic = dynamic / / add the children of the current node to the slotContainer Slotcontainer.children = el.children.filter((c: any) => {if (! c.slotScope) { c.parent = slotContainer return true } }) slotContainer.slotScope = slotBinding.value || EmptySlotScopeToken // Empty children el.children = [] el.plain = false}}Copy the code

After this processing, we can use the V-slot directive directly on the parent component to get the value of the slot binding.

Here’s an official example:

Default slot with text <! -- old --> <foo> <template slot-scope="{ msg }"> {{ msg }} </template> </foo> <! -- new --> <foo v-slot="{ msg }"> {{ msg }} </foo> Default slot with element <! -- old --> <foo> <div slot-scope="{ msg }"> {{ msg }} </div> </foo> <! -- new --> <foo v-slot="{ msg }"> <div> {{ msg }} </div> </foo>Copy the code

3.2.4 genSlot

The only change is to support the v-slot dynamic parameters as follows:

// old const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}` // new // Attrs, dynamicAttrs concat operation, And perform genProps convert them to corresponding generate string const attrs = el. Attrs | | el. DynamicAttrs? GenProps ((el. Attrs | | []).concat(el.dynamicAttrs || []).map(attr => ({ // slot props are camelized name: camelize(attr.name), value: attr.value, dynamic: attr.dynamic })) ) : nullCopy the code