preface

Slot and slot-scope in Vue has always been an advanced concept that we don’t often touch in our everyday component development, but is very powerful and flexible.

In the Vue 2.6

  1. slotslot-scopeIs unified within the componentfunction
  2. Their render scope isChild components
  3. And they all passthis.$scopedSlotsTo visit the

This makes the development experience of this pattern more uniform, and this article explores how it works based on the latest 2.6.11 code.

If you’re not familiar with the new slot syntax in 2.6, check out this great official announcement

Vue 2.6 has been released

For a simple example, the community has an asynchronous process management library called Vue-promised, which works like this:

<Promised :promise="usersPromise">
  <template v-slot:pending>
    <p>Loading...</p>
  </template>
  <template v-slot="data">
    <ul>
      <li v-for="user in data">{{ user.name }}</li>
    </ul>
  </template>
  <template v-slot:rejected="error">
    <p>Error: {{ error.message }}</p>
  </template>
</Promised>

Copy the code

As you can see, if we pass an asynchronous promise to the component, it will automatically complete the promise for us and throw pending, rejected, and successfully executed data in response.

This greatly simplifies our asynchronous development experience, where we had to manually execute the promise, manually manage state handling errors, and so on…

All of this power comes from the slot-scope functionality provided by Vue, which is a little closer to Hook in terms of flexibility of encapsulation, and components can even be completely unconcerned with UI rendering, only helping their parent manage some state.

Analogy to the React

If you have Experience with React, this is actually a good analogy to understand with renderProps in React. (If you don’t have React development experience, please skip this.)

import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'

// This is the render props component that provides the mouse position
class Mouse extends React.Component {
  state = { x: 0.y: 0 }

  handleMouseMove = (event) = > {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>{this.props. Children (this.props. Children) {this.props.</div>)}}class App extends React.Component {
  render() {
    return (
      <div style={{ height: '100'}} % >// This is similar to the scope slot of Vue<Mouse>({x, y}) => (// render prop gives us the state we need to render what we want<h1>The mouse position is ({x}, {y})</h1>
         )
        </Mouse>
      </div>
    )
  }
})

ReactDOM.render(<App/>.document.getElementById('app'))
Copy the code

The principle of analytic

Initialize the

For an example like this

<test>
  <template v-slot:bar>
    <span>Hello</span>
  </template>
  <template v-slot:foo="prop">
    <span>{{prop.msg}}</span>
  </template>
</test>
Copy the code

The template will compile like this:

with (this) {
  return _c("test", {
    scopedSlots: _u([
      {
        key: "bar".fn: function () {
          return [_c("span", [_v("Hello")])]; }, {},key: "foo".fn: function (prop) {
          return [_c("span", [_v(_s(prop.msg))])]; }},])}); }Copy the code

Then, after a series of initialization processing (resolveScopedSlots, normalizeScopedSlots), the test component instance this.$scopedSlots can access the two foo and bar functions. (If not named, the key will be default.)

Go inside the Test component and assume it is defined like this:

<div>
  <slot name="bar"></slot>
  <slot name="foo" v-bind="{ msg }"></slot>
</div>
<script>
  new Vue({
    name: "test",
    data() {
      return {
        msg: "World"}; }, mounted() {// Update after one second
      setTimeout((a)= > {
        this.msg = "Changed";
      }, 1000); }});</script>

Copy the code

The template will compile to a function like this:

with (this) {
  return _c("div", [_t("bar"), _t("foo".null.null, { msg })], 2);
}
Copy the code

With that in mind, let’s look at the implementation of the _t function to get closer to the truth.

_t is an alias for renderSlot. The simplified implementation looks like this:

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

Copy the code

It’s very simple,

If it is a normal slot, call the function directly to generate vNode. If it is a scoped slot,

Call the function with props ({MSG}) to generate vNode. The unification of slots for functions after 2.6 has reduced a lot of mental burden.

update

In the test component above, after 1s we pass this.msg = “Changed”; Trigger a responsive update when the compiled render function:

with (this) {
  return _c("div", [_t("bar"), _t("foo".null.null, { msg })], 2);
}
Copy the code

Re-execute, this time the MSG has Changed after the update, naturally also implemented the update.

A special case is when the parent component’s scope also uses a responsive property and updates it, such as this:

<test>
  <template v-slot:bar>
    <span>Hello {{msgInParent}}</span>
  </template>
  <template v-slot:foo="prop">
    <span>{{prop.msg}}</span>
  </template>
</test>
<script>
  new Vue({
    name: "App",
    el: "#app",
    mounted() {
      setTimeout(() => {
        this.msgInParent = "Changed";
      }, 1000);
    },
    data() {
      return {
        msgInParent: "msgInParent"}; }, components: { test: { name:"test",
        data() {
          return {
            msg: "World"}; }, template: ` <div> <slot name="bar"></slot>
            <slot name="foo" v-bind="{ msg }"></slot>
          </div>
        `,
      },
    },
  });
</script>
Copy the code

In fact, when executing the _t function, the global component rendering context is a child component, so the dependency collection is the dependency collection of the child component. So after the msgInParent update, it actually triggers the rerendering of the child components directly, which is an optimization compared to the 2.5 release.

There are additional cases where the template has v-if and V-for. Here’s an example:

<test>
  <template v-slot:bar v-if="show">
    <span>Hello</span>
  </template>
</test>
Copy the code
function render() {
  with(this) {
    return _c('test', {
      scopedSlots: _u([(show) ? {
        key: "bar".fn: function () {
          return [_c('span', [_v("Hello")])]},proxy: true
      } : null].null.true)}}}Copy the code

Note here _u internal is a ternary expression directly, read and _render _u is occurred in the parent component, then the child components at this time is can’t collect the dependence on the show, so show updates will only trigger a parent component updates, How does the sub-component re-execute the $scopedSlot function and re-render in this case?

We already have some knowledge of Vue’s update granularity, and we know that Vue’s components are not recursively updated, but that the slotScopes function execution takes place in the child component, and that the parent must have some way of notifies the child component when it is updated.

In fact, this process occurs in the rerendered patchVnode of the parent component. In the patch process of the test component, after the updateChildComponent function is entered, it will check whether its slot is stable. Obviously v-IF controlled slots are very unstable.

  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  consthasDynamicScopedSlot = !! ( (newScopedSlots && ! newScopedSlots.$stable) || (oldScopedSlots ! == emptyObject && ! oldScopedSlots.$stable) || (newScopedSlots && vm.$scopedSlots.$key ! == newScopedSlots.$key) )// Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  constneedsForceUpdate = !! hasDynamicScopedSlotif (needsForceUpdate) {
    // The vm corresponds to test, which is an instance of the child component, triggering the child component to force rendering.
    vm.$forceUpdate()
  }
Copy the code

There are some optimizations here, and it’s not that every slotScope will trigger a child component to force an update.

There are three ways to force a child component update:

  1. scopedSlotsOn the$stableProperties forfalse

The $stable is _u which is the third argument of the resolveScopedSlots function. Since _u is generated by the compiler when it generates the render function, let’s look at the codeGen logic:

  let needsForceUpdate = el.for || Object.keys(slots).some(key= > {
    const slot = slots[key]
    return (
      slot.slotTargetDynamic ||
      slot.if ||
      slot.for ||
      containsSlotChild(slot) // is passing down slot from parent which may be dynamic)})Copy the code

In a nutshell, when some dynamic syntax is used, the child component is notified to force an update to scopedSlots.

  1. Is also$stableAttribute related, oldscopedSlotsunstable

This makes sense. The old scopedSlots required a forced update, so make sure to force an update after rendering.

  1. The old$keyDoes not equal new$key

The contentHashKey is the fourth argument in _u. The contentHashKey is calculated by codeGen’s hash algorithm on the string generated by the code. In other words, When the generated string of the function changes, the child component needs to be forcibly updated.

function hash(str) {
  let hash = 5381
  let i = str.length
  while(i) {
    hash = (hash * 33) ^ str.charCodeAt(--i)
  }
  return hash >>> 0
}
Copy the code

conclusion

After Vue 2.6, there was a unified integration of slots and slot-scope, making them all functions. All slots are accessible directly from this.$scopedSlots, which makes it easier to develop advanced components.

In terms of optimization, Vue 2.6 also tries to keep slot updates from triggering renderings of the parent component, using a series of clever judgments and algorithms to avoid unnecessary renderings as much as possible. (in version 2.5, slot generation scope is in the parent component, so child slots are updated with the parent component)

Before listening to The speech of You, Vue3 will make more use of the static characteristics of templates to do more precompilation optimization, in the process of code generation in the article, we have felt his efforts to this end, very much looking forward to Vue3 to bring more powerful performance.

❤️ Thank you all

1. If this article is helpful to you, give it a like. Your likes are my motivation.

2. Follow the public number “front-end from advanced to hospital” can add my friends, I pull you into the “front-end advanced exchange group”, we communicate and progress together.