头图

Preface

vue.js is the component, and the soul of the component is the slot. With the help of slots, we can achieve component reuse to the greatest extent. This article is mainly a detailed summary of the implementation mechanism of the slot, which is useful in some scenarios. vue.js it is, knowing why, and mastering the implementation principle of 060d063355d5b1 can not only improve one's own problem-solving ability, but also learn the programming ideas and development paradigms of the great gods.

Sample code

<!-- 子组件comA -->
<template>
  <div class='demo'>
    <slot><slot>
    <slot name='test'></slot>
    <slot name='scopedSlots' test='demo'></slot>
  </div>
</template>
<!-- 父组件 -->
<comA>
  <span>这是默认插槽</span>
  <template slot='test'>这是具名插槽</template>
  <template slot='scopedSlots' slot-scope='scope'>这是作用域插槽(老版){{scope.test}}</template>
  <template v-slot:scopedSlots='scopeProps' slot-scope='scope'>这是作用域插槽(新版){{scopeProps.test}}</template>
</comA>

Look beyond the surface

The role of the slot is achieve content distribution . To achieve content distribution, two conditions are required:

  1. Placeholder
  2. Distribute content

Components internally defined slot label, we can understand placeholder , parent component content in the slot is to content distribution . The essence of slot processing is to place the specified content in the specified location. Not much nonsense, from this article, you will be able to understand:

  1. The realization principle of the slot
  2. How to use slots in render

Realization principle

vue instantiation sequence of the 060d063355d7ce component is: parent component state initialization ( data , computed , watch ...) --> Template compilation --> Generate render method --> Instantiate rendering watcher --> Call render --> Call 060d063355d7d8 method to generate VNode > patch VNode , converted to true DOM -> instantiating subunit assembly -> ...... repeats the same process -> sub-assembly to generate real DOM mount real parent component generated DOM on the mount In the page --> remove the old node

From the above process, it can be inferred:

  1. The parent component template is resolved before the child component, so the parent component will first get the slot template content
  2. The subcomponent template is parsed later, so when the subcomponent calls the render method to generate VNode , you can use some means to get the VNode node of the slot
  3. The scope slot can get the variables in the sub-component, so the VNode scope sub-component needs to be passed in in real time

The processing stage of the entire slot is roughly divided into three steps:

  • Compile
  • Generate rendering template
  • Generate VNode

Take the following code as an example to briefly summarize the process of slot operation.

<div id='app'>
  <test>
    <template slot="hello">
      123
    </template>
  </test>
</div>
<script>
  new Vue({
    el: '#app',
    components: {
      test: {
        template: '<h1>' +
          '<slot name="hello"></slot>' +
          '</h1>'
      }
    }
  })
</script>

Parent component compilation stage

Compilation is to parse the template file into a AST syntax tree, and template into the following data structure:

{
  tag: 'test',
  scopedSlots: { // 作用域插槽
    // slotName: ASTNode,
    // ...
  }
  children: [
    {
      tag: 'template',
      // ...
      parent: parentASTNode,
      children: [ childASTNode ], // 插槽内容子节点,即文本节点123
      slotScope: undefined, // 作用域插槽绑定值
      slotTarget: "\"hello\"", // 具名插槽名称
      slotTargetDynamic: false // 是否是动态绑定插槽
      // ...
    }
  ]
}

Parent component generation rendering method

According to the AST syntax tree, the rendering method string is parsed and generated. The final result generated by the parent component is shown below. This structure render . The essence is to generate VNode , but _c or h is the abbreviation this.$createElement

with(this){
  return _c('div',{attrs:{"id":"app"}},
  [_c('test',
    [
      _c('template',{slot:"hello"},[_v("\n      123\n    ")])],2)
    ],
  1)
}

Parent component generates VNode

Call the render method to generate VNode , VNode specific format is as follows:

{
  tag: 'div',
  parent: undefined,
  data: { // 存储VNode配置项
    attrs: { id: '#app' }
  },
  context: componentContext, // 组件作用域
  elm: undefined, // 真实DOM元素
  children: [
    {
      tag: 'vue-component-1-test',
      children: undefined, // 组件为页面最小组成单元,插槽内容放放到子组件中解析
      parent: undefined,
      componentOptions: { // 组件配置项
        Ctor: VueComponentCtor, // 组件构造方法
        data: {
          hook: {
            init: fn, // 实例化组件调用方法
            insert: fn,
            prepatch: fn,
            destroy: fn
          },
          scopedSlots: { // 作用域插槽配置项,用于生成作用域插槽VNode
            slotName: slotFn
          }
        },
        children: [ // 组件插槽节点
          tag: 'template',
          propsData: undefined, // props参数
          listeners: undefined,
          data: {
            slot: 'hello'
          },
          children: [ VNode ],
          parent: undefined,
          context: componentContext // 父组件作用域
          // ...
        ] 
      }
    }
  ],
  // ...
}

In vue , the component is the basic unit of the page structure. From the above VNode , we can also see that the VNode page hierarchy ends in the test component, and the test component children processed during the sub-component initialization process. The sub-component construction method assembly and attributes are merged in vue-dev\core\vdom\create-component.js createComponent method, the component instantiation call entry is vue-dev\src\core\vdom\patch. js createComponent method.

Sub-component state initialization

When instantiating the sub-component, in the initRender -> resolveSlots method, the sub-component slot node will be mounted in the component scope vm , and the mount form is vm.$slots = {slotName: [VNode]} .

Sub-component compilation phase

In the compilation phase of the sub-components, the slot node will be compiled into the following AST structure:

{
  tag: 'h1',
  parent: undefined,
  children: [
    {
      tag: 'slot',
      slotName: "\"hello\"",
      // ...
    }
  ],
  // ...
}

Sub-component generation and rendering method

The generated rendering method is as follows, where _t is the abbreviation of the renderSlot method. From the renderSlot method, we can intuitively associate the slot content with the slot point.

// 渲染方法
with(this){
  return _c('h1',[ _t("hello") ], 2)
}
// 源码路径:vue-dev\src\core\instance\render-helpers\render-slot.js
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 || {}
    if (bindObject) {
      if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
        warn(
          'slot v-bind without argument expects an Object',
          this
        )
      }
      props = extend(extend({}, bindObject), props)
    }
    // 作用域插槽,获取插槽VNode
    nodes = scopedSlotFn(props) || fallback
  } else {
    // 获取插槽普通插槽VNode
    nodes = this.$slots[name] || fallback
  }

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

Difference between scoped slot and named slot

<!-- demo -->
<div id='app'>
  <test>
      <template slot="hello" slot-scope='scope'>
        {{scope.hello}}
      </template>
  </test>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        components: {
            test: {
                data () {
                    return {
                        hello: '123'
                    }
                },
                template: '<h1>' +
                    '<slot name="hello" :hello="hello"></slot>' +
                  '</h1>'
            }
        }
    })

</script>

The main difference between the scoped slot and the ordinary slot is that the slot can get the subcomponent scope variable . Due to the need to inject sub-component variables, compared to named slots, scoped slots are different in the following points:

  • When the scope slot is assembled in the rendering method, a method containing the injection scope is generated createElement VNode , there is an additional layer of injection scope method package, which determines that the slot VNode scope slot is in It is generated VNode , and the named slot is generated when the parent component creates VNode . _u is resolveScopedSlots , which is used to convert the node configuration item to {scopedSlots: {slotName: fn}} form.
 with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('test', {
            scopedSlots: _u([{
                key: "hello",
                fn: function(scope) {
                    return [_v("\n        " + _s(scope.hello) + "\n      ")]
                }
            }])
        })], 1)
    }
  • When the sub-component is initialized, the named slot node will be processed, and it will be mounted in the component $slots , and the scoped slot will be directly called renderSlot

Otherwise, the other processes are roughly the same. The slot mechanism is not difficult to understand. The key is to template and generate the render function The two steps are more content, and the process is longer and more difficult to understand.

skills

Through the above analysis, we can roughly understand the slot processing flow. Most of the work is to use templates to write vue code, but sometimes the template has certain limitations, you need to use the render method to amplify the component abstraction ability of vue Then in the render method, the method of using our slot is as follows:

Named slot

Slot processing is generally divided into two parts:

  • Parent component

The parent component only needs to be written as a rendering method compiled into a template, that is, specify the name of the slot

  • Subassembly

VNode generated during the initialization phase of the parent component, the child component only needs to slot VNode generated by the parent component, and the child component will mount the named slot to the component $slots property when it is initialized.

<div id='app'>
<!--  <test>-->
<!--    <template slot="hello">-->
<!--      123-->
<!--    </template>-->
<!--  </test>-->
</div>
<script>
  new Vue({
    // el: '#app',
    render (createElement) {
      return createElement('test', [
        createElement('h3', {
          slot: 'hello',
          domProps: {
            innerText: '123'
          }
        })
      ])
    },
    components: {
      test: {
        render(createElement) {
          return createElement('h1', [ this.$slots.hello ]);
        }
        // template: '<h1>' +
        //   '<slot name="hello"></slot>' +
        //   '</h1>'
      }
    }
  }).$mount('#app')
</script>

Scope slot

The scope of the slot is more flexible to use and can be injected into the sub-component state. Scope slot + render method is very useful for secondary component packaging. For example, when ElementUI table components are JSON data, the scope slot is very useful.

<div id='app'>
<!--  <test>-->
<!--    <span slot="hello" slot-scope='scope'>-->
<!--      {{scope.hello}}-->
<!--    </span>-->
<!--  </test>-->
</div>
<script>
  new Vue({
    // el: '#app',
    render (createElement) {
      return createElement('test', {
        scopedSlots:{
          hello: scope => { // 父组件生成渲染方法中,最终转换的作用域插槽方法和这种写法一致
            return createElement('span', {
              domProps: {
                innerText: scope.hello
              }
            })
          }
        }
      })
    },
    components: {
      test: {
        data () {
          return {
            hello: '123'
          }
        },
        render (createElement) {
          // 作用域插槽父组件传递过来的是function,需要手动调用生成VNode
          let slotVnode = this.$scopedSlots.hello({ hello: this.hello })
          return createElement('h1', [ slotVnode ])
        }
        // template: '<h1>' +
        //   '<slot name="hello" :hello="hello"></slot>' +
        //   '</h1>'
      }
    }
  }).$mount('#app')

</script>

summary

Knowing what it is, knowing why it is, then you can stay the same and respond to all changes.


Gerry
158 声望2 粉丝