Named slots and scoped slots are implemented in Vue. Named slots can be specified by slot=”header” or v-slot:header in the parent component. Start with named slots (slot attributes).

The creation of named slots (slot attributes)

The parent component

Take a look at the parent component demo

<div>
  <child>
    <h1 slot="header">{{title}}</h1>
    <p>{{message}}</p>
    <p slot="footer">{{desc}}</p>
  </child>
</div>
Copy the code

The compiled

_c(
  "div",
  [
    _c("child", [
      _c(
        "h1",
        {
          attrs: {  / / here
            slot: "header",},slot: "header"./ / here
        },
        [_v(_s(title))]
      ),
      _v(""),
      _c("p", [_v(_s(message))]), // Here (default)
      _v(""),
      _c(
        "p",
        {
          attrs: { / / here
            slot: "footer",},slot: "footer"./ / here
        },
        [_v(_s(desc))]
      ),
    ]),
  ],
  1
);
Copy the code

The compiled component code has child nodes that mount a slot property with the slot name; There is also a slot attribute in the attrs attribute. The default slot does not add any attributes

To review the entire mount process, first execute the parent component’s _render method to create a VNode. During the creation of a VNode, collect dependencies for reactive properties. When you meet the component to VNode components to create components, if the component has child nodes, a VNode to child nodes, and add child nodes VNode to componentOptions. The children, the child node is slot.

The patch procedure is then performed to create the DOM element, and when a component VNode is encountered, the component VNode’s init hook function is called to create the component instance. The initRender method is executed during component instance initialization, which has the following logic

export function initRender (vm: Component) {
  const parentVnode = vm.$vnode = options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  / / options. _renderChildren is component VNode componentOptions. Children
  / / will merge options in _init, if it is a component instance, will componentOptions. Children assigned to the options. _renderChildren
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
}
Copy the code

Two attributes $slots and $scopedSlots are mounted to the current Vue instance.

The value of vm.$slots is the return value of the resolveSlots method, which takes slot contents (VNode arrays) and parent Vue instances.

export function resolveSlots (
  children: ?Array<VNode>, context: ? Component// point to the parent Vue instance
){
  if(! children || ! children.length) {return{}}const slots = {}
  for (let i = 0, l = children.length; i < l; i++) {
    const child = children[i]
    const data = child.data
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
    }
    // Because slot's VNode is generated in the scope of the parent component instance, child.context refers to the parent component
    if((child.context === context || child.fnContext === context) && data && data.slot ! =null
    ) {
      const name = data.slot
      const 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)
    }
  }
  for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
      delete slots[name]
    }
  }
  return slots
}
Copy the code

Iterates through the VNode array, removing the property if data.attrs.slot exists; Select * from VNode where the Vue instance is stored and the Vue instance is the same as the parent Vue instance. This is true because the slot VNode is created in the parent component instance.

  • If yes, it indicates a named slot. If the current VNode label name istemplate, it willAll children of the current VNodeAdded to theslots[name]; Whereas theThe current VNodeAdded to theslots[name]In the
  • If not, it is the default slot. Adds the current VNode toslots.defaultIn the

Finally, we iterate through slots to remove the comment VNode or the empty string text VNode; And return to slots. The vm.$slots attribute values are as follows

vm.$slots = {
  header: [VNode],
  footer: [VNode],
  default: [VNode]
}
Copy the code

The resolveSlots method generates and returns an object slots with the attribute name of the slot and the value of the VNode array

Child components

After the child component instance is created, the child component is mounted. Take a look at the compiled code for the demo and child components

<div class="container">
  <header><slot name="header"></slot></header>
  <main><slot>The default content</slot></main>
  <footer><slot name="footer"></slot></footer>
</div>
Copy the code

Compiled code

_c("div", { staticClass: "container" }, [
  _c("header", [_t("header")].2),
  _v(""),
  _c("main", [_t("default", [_v("Default content")])], 2),
  _v(""),
  _c("footer", [_t("footer")].2),]);Copy the code

In the compiled code, the

tag is compiled into the _t function with the slot name as the first argument and the function that creates the backup content VNode as the second argument

RenderSlot is defined in SRC /core/instance/render-heplpers/render-slot.js: renderSlot = renderSlot; renderSlot = renderSlot; renderSlot = renderSlot;

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // Scope slot
    } else {
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}
Copy the code

For named slots, renderSlot returns the VNode of the slot name from vm.$slots based on the slot name passed in. If no slot VNode is found, fallback is called to create the VNode of the backup content in the child component instance. But slot vNodes are created in the parent component instance

At this point, the creation process is complete and the slot content is in place.

Update process for named slots (slot attributes)

Notifies the parent’s Render Watcher update when the parent modifies a reactive property. Call the parent component render method to create VNode, in the process, also can create component VNode and slot VNode, add componentOptions slot VNode. Children. The patch process is then entered, and for component updates, the updateChildComponent function is called to update the attributes of the passed child component

export function updateChildComponent (
  vm: Component, // Subcomponent instance
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, / / component vnode
  renderChildren: ?Array<VNode> // The latest slot VNode array
) {
  constneedsForceUpdate = !! ( renderChildren ||// has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}
Copy the code

For named slots with slot contents, vm.$options._renderChildren has a value, so needsForceUpdate is true. Call resolveSlots to get the latest vm. Call vm.$forceUpdate() to update the component view.

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}
Copy the code

Prototype.$forceUpdate calls the Render Watcher update method to update the view

Summary of named slots (slot attributes

The creation process

Using slot attribute said named slot, slot content in the parent component will compile and rendering phase directly generate vnodes and VNode componentOptions. Slot VNode on components in children, in create a component instance, Mount slot VNode to vm.$slots; When creating a child VNode, get the VNode from vm.$slots based on the slot name, or create a backup VNode if it does not exist.

The update process

Trigger the parent Render Watcher update when the parent updates a reactive property. Generate a slot VNode. Reset vm.$slots and call vm.$forceUpdate() to trigger the child component to update the view

Scope slot creation process

The parent component

Take a look at the demo and the compiled code

<div>
  <child>
    <template v-slot:hello="props">
      <p>hello from parent {{props.text + props.msg}}</p>
    </template>
  </child>
</div>
Copy the code

The compiled parent component code adds a scopedSlots attribute to the child component code with the value _u function

with (this) {
    return _c(
        'div',
        [
            _c('child', {
                scopedSlots: _u([ / / here
                    {
                        key: 'hello'./ / slot
                        fn: function (props) { // Create a VNode for the slot contents
                            return [
                                _c('p', [
                                    _v(
                                        'hello from parent ' +
                                            _s(props.text + props.msg) // Get the value from the props passed in),]),]},},]),],1)}Copy the code

When the parent creates a VNode, the _u method in the scopedSlots attribute is executed. The _u method corresponds to the resolveScopedSlots method. SRC /core/instance/render-helpers/resolve-scoped-slots.js

export function resolveScopedSlots (fns: ScopedSlotsData, res? :Object, hasDynamicKeys? : boolean, contentHashKey? : number) :{ [key: string]: Function.$stable: boolean } {
  // If no res is passed, an object is created;
  // The object has a $stable property, which is true if it is not a dynamic property name, there is no V-for on the slot, and there is no V-if
  res = res || { $stable: !hasDynamicKeys }
  for (let i = 0; i < fns.length; i++) {
    const slot = fns[i]
    if (Array.isArray(slot)) {
      resolveScopedSlots(slot, res, hasDynamicKeys)
    } else if (slot) {
      if (slot.proxy) { // When v-slot:header (named slot added in 2.6) is used, proxy is true
        slot.fn.proxy = true
      }
      res[slot.key] = slot.fn
    }
  }
  if (contentHashKey) {
    (res: any).$key = contentHashKey
  }
  return res
}
Copy the code

Where, FNS is an array, each array element has a key and a FN, key corresponds to the name of the slot, fn corresponds to a function. The whole logic is to iterate over the FNS array and generate an object whose key is the slot name and value is the render function. This render function generates a VNode;

Child components

Take a look at the demo and the compiled code

<div class="child">
  <slot text="123" name="hello" :msg="msg"></slot>
</div>
Copy the code

Compiled subcomponents

_c(
  "div",
  { staticClass: "child" },
  [_t("hello".null, { text: "123".msg: msg })], / / here
  2
);
Copy the code

The

tag in the generated code is also converted to the _t function. The _t function of the scoped slot takes one more parameter than the named slot, which is an object composed of attributes in the child component

When the child component instance is created, the initRender method is called, which creates the vm.$scopedSlots = emptyObject; The VNode is then created by executing the render function of the child component, which has this logic in the _render function

const { render, _parentVnode } = vm.$options
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}
Copy the code

If the component VNode is not empty, it indicates that the rendering VNode of the child component is being created. NormalizeScopedSlots method We pass in the component VNode’s scopedSlots attribute, vm.$slots(which is an empty object), and vm.$scopedSlots(which is also an empty object) and assign the return value to VM. Above said that, when creating the parent component rendering VNode will call _u method, returns an object assigned to _parentVnode. Data. ScopedSlots, the property name is the name of the slot, the attribute value is to create a slot VNode rendering function.

export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> }, prevSlots? : { [key: string]:Function } | void
) :any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  $stable is defined in _u
  constisStable = slots ? !!!!! slots.$stable : ! hasNormalSlotsconst key = slots && slots.$key
  if(! slots) { res = {} }else if (slots._normalized) {} else if() {}else {
    // Start here
    // Create process
    res = {}
    // Iterate over the incoming slots, calling the normalizeScopedSlot method for each attribute value
    for (const key in slots) {
      if (slots[key] && key[0]! = ='$') {
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
      }
    }
  }
  // ...
  
  / / cache
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  // Add $stable, $key, $hasNormal to res and cannot be enumerated
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots) // vm.$slots is true if it has attributes, and false if it doesn't
  return res
}
Copy the code

The creation process generates an object whose key is the slot name and value is the return value of normalizeScopedSlot

function normalizeScopedSlot(normalSlots, key, fn) {
  const normalized = function () {}
  if (fn.proxy) {}
  return normalized
}
Copy the code

NormalizeScopedSlot creates a normalized function and returns a normalized function. For scoped slots, fn.proxy is false.

Back to normalizeScopedSlots, cache the generated objects to slots._normalized, and then add $stable, $key, and $hasNormal to res to return res. Vm.$scopedSlots is an object. The attribute name is the slot name, and the attribute value is a normalized function. Vm.$scopedSlots also has $stable, $key, and $hasNormal attributes.

After the assignment of vm.$scopedSlots, the render function of the child component is executed, during which the _t function, the renderSlot function, is executed

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
  } else {
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}
Copy the code

Normalizes functions from $scopedSlots, and calls normalized functions to pass in props, which are the properties that the child component passes to its parent through the slot.

const normalized = function () {
  // Call the render function of the slot to create a slot VNode
  let res = arguments.length ? fn.apply(null.arguments) : fn({})
  res = res && typeof res === 'object'&&!Array.isArray(res)
    ? [res] // single vnode
  : normalizeChildren(res)
  return res && (
    res.length === 0 ||
    (res.length === 1 && res[0].isComment) / / # 9658)?undefined
  : res
}
Copy the code

Normalized implements the rendering functions of the slot and passes in props to create vNodes and dependencies on used attributes. The process of creating a VNode from this scope slot ends. In fact, the creation of the VNode of the scoped slot is created in the child component, so the dependency collected during the creation of the slot VNode is the Render Watcher of the component

Scope slot update process

The child component is updated, the parent component is not updated

When the child component modifies the responsive property, notify the child component Watcher of the update, and create the child component’s render VNode; During creation, normalizeScopedSlots is called to get the object of the rendering function whose key is the slot name and value is the slot name from vm.$scopedSlots.

// initRender
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}
Copy the code
export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> }, prevSlots? : { [key: string]:Function } | void
) :any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  constisStable = slots ? !!!!! slots.$stable : ! hasNormalSlotsconst key = slots && slots.$key
  if(! slots) { res = {} }else if (slots._normalized) {
    // Fast path 1: only the child component is updated, the parent component is not updated, returns the object created last time
    // Fast path 1 = 1
    return slots._normalized
  } else if( isStable && prevSlots && prevSlots ! == emptyObject && key === prevSlots.$key && ! hasNormalSlots && ! prevSlots.$hasNormal ) {// Fast Path 2: The parent component is updated, but the scope slot is unchanged, returns the object created last time
    return prevSlots
  } else{}if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  return res
}
Copy the code

Parent component update

When the parent modifies a reactive property, notify the parent of the Render Watcher update. Call the _u function to retrieve the scopedSlots attribute during the parent component’s VNode creation phase. During patch, the updateChildComponent method is called

export function updateChildComponent (
  vm: Component, // Subcomponent instance
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, / / component vnode
  renderChildren: ?Array<VNode>
) {

  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  consthasDynamicScopedSlot = !! ( (newScopedSlots && ! newScopedSlots.$stable) || (oldScopedSlots ! == emptyObject && ! oldScopedSlots.$stable) || (newScopedSlots && vm.$scopedSlots.$key ! == newScopedSlots.$key) )constneedsForceUpdate = !! ( renderChildren ||// has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}
Copy the code

In the updateChildComponent method, we first get the old and new scopedSlots objects and determine whether needsForceUpdate is true. If so, we call vm.$forceUpdate() to trigger the update

NeedsForceUpdate is true only if

  • Have a child node
  • There are slots for child nodes
  • hasDynamicScopedSlotfortrue
    • newscopedSlotsObject is not empty and has dynamic slots or slots on itv-fororv-if
    • The oldscopedSlotsObject is not empty and has dynamic slots or slots on itv-fororv-if
    • newscopedSlotsThe object is not empty and is old and newscopedSlotsThe object’s$keydifferent

That is, for a named slot whose slot attributes specify the contents of the slot, when the parent component modifies the reactive attributes, the child component must update, regardless of whether the parent component’s reactive attributes are used in the slot. Unless there is no slot content.

In the case of a scoped slot, when the parent modifies a reactive property, child component updates are triggered only if the slot name is dynamic.

Summary of scope slots

The creation process

ScopedSlots do not generate vnodes directly during compilation and rendering of the parent component. Instead, a scopedSlots object is kept in the parent vnode’s data, storing slots with different names and their corresponding rendering functions. When creating child component instances, Mount this attribute to vm.$scopedSlots; When a child VNode is created, the child attributes are passed in and the render function is executed to create the VNode; The Render Watcher of the child component is added to the dep.subs of the responsive property during creation

The update process

The child component is updated, the parent component is not updated

The Watcher update is triggered when a reactive property is modified by a child component, whether or not the property is applied to the scope slot. $scopedSlots; During the creation of the render VNode, the slot function is executed to create the slot VNode and pass in the child component properties

Parent component update

When the parent modifies a reactive property, notify the parent of the Render Watcher update. The render function creates a new slot object, and calls vm.$forceUpdate() to trigger child component updates if there are dynamic slots in the old and new slot objects, but not otherwise

Fast Path 1

In normalizeScopedSlots has a fast path 1, if the parent component triggers the child components will update the incoming through _u slots in the newly created object _parentVnode. Data. ScopedSlots, there is no mount _normalized properties, So this logic is used only if the child component is updated and the parent component is not.

Procedure for creating named slots in v-slot form

After 2.6, a new uniform syntax (the V-slot directive) was introduced for named slots and scoped slots. It replaces the slot and slot-scope properties

The parent component

<div>
  <child>
    <template v-slot:hello>
      <p>hello from parent {{title}}</p>  <! -- using the parent component's properties -->
    </template>
  </child>
</div>
Copy the code

When compiled, the difference from the scope slot is that the title property used is fetched from the parent component and proxy is true

with (this) {
    return _c(
        'div',
        [
            _c('child', {
                scopedSlots: _u([
                    {
                        key: 'hello'./ / slot
                        fn: function () {  // Slot VNode render function
                            return [
                                _c('p', [_v('hello from parent ' + _s(title))]), // Get the value from this]},proxy: true.// this is true},]),})],1)}Copy the code

In much the same way as the scoped slot process, _u is executed to create a slot object with the property name being the slot name and the property value being a rendering function used to create a VNode. Then in the _render method of the child component, normalizeScopedSlots method is implemented

export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> }, prevSlots? : { [key: string]:Function } | void
) :any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  constisStable = slots ? !!!!! slots.$stable : ! hasNormalSlotsconst key = slots && slots.$key
  if(! slots) {}else if (slots._normalized) {} else if() {}else {
    res = {}
    for (const key in slots) {
      if (slots[key] && key[0]! = ='$') {
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
      }
    }
  }
  for (const key in normalSlots) {
    if(! (keyin res)) {
      res[key] = proxyNormalSlot(normalSlots, key)
    }
  }
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots)
  return res
}
Copy the code

Create an RES object with the property name of the slot and the property value of the render function. Property values are returned via normalizeScopedSlot

function normalizeScopedSlot(normalSlots, key, fn) {
  const normalized = function () {}
  if (fn.proxy) {
    // If v-slot:header is used, add attributes to vm.$slot. The attributes are normalized
    Object.defineProperty(normalSlots, key, {
      get: normalized,
      enumerable: true.configurable: true})}return normalized
}
Copy the code

In contrast to a scope slot, after a normalized function is created, the slot name is added to vm.$slots and the attribute value is normalized.

Child components

The child component is the same as the demo of the scope slot

Continue execution until the render function of the child component is executed, calling the _t function (renderSlot); Run the normalized function to create a VNode if there is a slot. Instead, execute the function that creates the backup content VNode. During execution, if the parent component’s property is used, the dependency collection is done on the property and the Render Watcher of the child component is added to the dep.subs of the property. The collection is the Render Watcher of the child component

Update process of named slot in v-slot form

Reactive properties modified by the parent component are used in slot content

Create slots vNodes are created in the child component, so the collected Watcher is the Render Watcher of the child component, so trigger the Watcher update of the child component, and execute the normalizeScopedSlots method when executing the _render function of the child component. Since the vm.$scopedSlots object is first generated and the _normalized attribute is added to cache the slot object, the previous cache is returned directly and the corresponding slot function is executed via the _t function. The latest parent component attribute values are retrieved during execution.

Reactive properties modified by the parent component are not used in the slot content

The parent component recreates the data.scopedSlots attribute of the child VNode while creating the VNode. When updating the attributes of the incoming child component, the child component view is updated if the new and old scopedSlots have dynamic slot names, and not otherwise

conclusion

The difference between v-slot and slot attributes

Slot: The parent component generates vNodes during compilation and rendering and collects the parent component Watcher; When the parent component property value is modified, the parent component update is triggered and the slot VNode is recreated. It then invokes the child component’s $forceUpdate method to trigger the child component update. That is, when a modified responsive property is not used in the slot, it triggers child component updates

V-slot: the parent component does not generate vNodes directly during compilation and rendering. Instead, it keeps a scopedSlots object in the parent vNode’s data, storing slots with different names and their corresponding rendering functions. This render function is executed to generate vNodes only during the compilation and rendering phases of the child components, at which point the collected Watcher is the Watcher of the child component. When a parent component modifs a responsive property, the child component is not updated if the modified property is not used in the slot. The child component’s Watcher update is triggered only when the used property is updated, and the slot function is re-executed to get the latest property value

Named slot and scoped slot

V-slot :header=”props” is the same as v-slot, except for the compiled code. In this case, the variable in the render function is props. Test