Please tell us the principle of slot and slot-scope in Vue (2.6.11 Deep Resolution) - Know it

Preface

Slot and slot-scope in Vue have always been an advanced concept, which is not always touched in our daily component development, but is very powerful and flexible.

In Vue 2.6

  1. Slot and slot-scope are unified into functions within components
  2. Their rendering scope is all sub-components
  3. And can be accessed through this.$slotScopes

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

For version 2.6 updated slot grammar, if you don't know much about it, you can take a look at this very large official announcement

Vue 2.6 released

For a simple example, the community has a library for asynchronous process management: vue-promised, which is used 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>

As you can see, if we just pass an asynchronous promise to the component that handles the request, it will automatically help us complete the promise and throw pending, rejected, and data after successful asynchronous execution.

This can greatly simplify our asynchronous development experience, where we would manually execute this promise, manually manage state handling errors, and so on...

All these powerful features benefit from the slot-scope functionality provided by Vue, which is even a little close to Hook in encapsulation flexibility, and even can help the parent component manage some state without concern for UI rendering at all.

Analogue React

If you have experience developing React, it's really like renderProps in React.(If you don't have React development experience, skip it)

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

// This is a render props component that provides mouse position to the outside world
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}>
        // Here children is executed as a function to provide the external state within the subcomponent
        {this.props.children(this.state)}
      </div>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <div style={{ height: '100%' }}>
        // It's like Vue's scoped slot here
        <Mouse>
         ({ x, y }) => (
           // render prop gives us the state s we need to render what we want
           <h1>The mouse position is ({x}, {y})</h1>
         )
        </Mouse>
      </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))

Principle Analysis

Initialization

For such an example

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

This template is compiled as follows:

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))])];
        },
      },
    ]),
  });
}

These two foo and bar functions can then be accessed by an instance of the test component, this.$slotScopes, after a series of initialization processes (resolveScopedSlots, normalizeScopedSlots).(If it's not named, the key will be default.)

Go inside the test component, assuming it is defined as:

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

The template is then compiled as a function of this type:

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

Now that you have a few clues, let's look at the implementation of the _t function to get closer to the truth.

_t is also the alias for renderSlot, and the simplified implementation is as follows:

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  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 function to return vnode
    nodes = scopedSlotFn(props) || fallback
  }
  return nodes
}

It's really simple,

If it is a normal slot, call the function directly to generate the vnode, if it is a scoped slot,

The function is called directly with props, which is {msg}, to generate the vnode.Slots that became functions after version 2.6 reduced the mental burden.

To update

In the test component above, after 1s we pass this.msg = "Changed"; trigger a responsive update, where the compiled render function:

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

Re-execute, the msg is now the updated Changed, and the update is naturally implemented.

A special case is that responsive attributes are also used and updated in the parent component's actions, such as:

<test>
  <template v-slot:bar>
    <span>Hello</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>

In fact, since the global component rendering context is a subcomponent when the _t function is executed, dependency collection is naturally a dependency of the collected subcomponent.So after updating msgInParent, it actually triggers the re-rendering of sub-components directly, which is an optimization compared to version 2.5.

There are additional cases, such as v-if and v-for on template s, for example:

<test>
  <template v-slot:bar v-if="show">
    <span>Hello</span>
  </template>
</test>

function render() {
  with(this) {
    return _c('test', {
      scopedSlots: _u([(show) ? {
        key: "bar",
        fn: function () {
          return [_c('span', [_v("Hello")])]
        },
        proxy: true
      } : null], null, true)
    })
  }
}

Note that here _u is a ternary expression directly inside. Reading _u occurs in the parent component's _render, so the child component can't collect the dependency of the show at this time, so updates to the show only trigger updates to the parent component. How do the child components re-execute the $scopedSlot function and re-render it in this case?

We already have some prior knowledge: Update granularity of Vue Knows that the components of the Vue are not updated recursively, but slotScopes'function execution occurs within the child components, and the parent component must have some way to inform the child components when updating.

In fact, this process occurs in the patchVnode rendered by the parent component. When the patch process of the test component enters the updateChildComponent function, it checks whether its slot is stable. Obviously, the v-if controlled slot is very unstable.

const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (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.
  const needsForceUpdate = !!hasDynamicScopedSlot
  
  if (needsForceUpdate) {
    // Here the vm corresponds to test, which is also an instance of a subcomponent, which triggers forced rendering of the subcomponent.
    vm.$forceUpdate()
  }

Here are some optimizations, not to say that any slotScope triggers a forced update of a subcomponent.

There are three situations where subcomponent updates are forced to trigger:

  1. The $stable property on scopedSlots is false

Looking all the way through this logic, you find that this $stable is determined by _u, the third parameter of the resolveScopedSlots function. Since this _u was generated by the compiler when it generated the render function, look at codegen's 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
    )
  })

Simply put, when some dynamic syntax is used, the subcomponent is notified to force an update of this scopedSlots.

  1. Also related to the $stable property, old scopedSlots are unstable

This is a good understanding that the old scopedSlots need to be forced to update, so they must be forced to update after rendering.

  1. Old $key does not equal new $key

This logic is interesting. Looking back all the way to the generation of $key, you can see that it is the fourth parameter of _u, contentHashKey, which is calculated using the hash algorithm on the string of generated code at codegen time. In other words, if the string generated by this string function changes, you need to force an update of the subcomponent.

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

summary

After Vue version 2.6, slots and slot-scope s have been unified so that they all become functions and all slots can be accessed directly on this.$slotScopes, which makes it easier for us to develop advanced components.

In terms of optimization, Vue 2.6 also tries to keep the update of slots from triggering the rendering of parent components and avoid unnecessary rendering as much as possible through a series of clever judgments and algorithms.(In version 2.5, since the scope for generating slots is in the parent component, slot updates that are clearly child components are updated with the parent component)

Previously, after listening to a very large speech, Vue3 will make more pre-compiled optimizations using the static nature of the template, and we have already felt his efforts in generating code in the text, and we look forward to the more robust performance that Vue3 will bring.

Information aboutThank you

1. If this article is helpful to you, please click on your approval. Your "approval" is the power of my creation.

2. If you pay attention to the public number "Front End from Advanced to Hospitalization", you can be my friend. I will pull you into the "Front End Advanced Communication Group" to communicate and make progress together.

Keywords: Vue React

Added by Imagine3 on Tue, 07 Apr 2020 05:10:19 +0300