Home  >  Article  >  Web Front-end  >  What are vue high-order components?

What are vue high-order components?

青灯夜游
青灯夜游Original
2022-12-20 13:24:372074browse

In vue, a high-order component is actually a high-order function, that is, a function that returns a component function. Characteristics of high-order components: 1. It is a pure function with no side effects, and the original component should not be modified, that is, the original component cannot be changed; 2. It does not care about the data (props) passed, and the newly generated component does not care about the source of the data. ; 3. The received props should be passed to the packaged component, that is, the original component props are directly passed to the packaging component; 4. High-order components can completely add, delete, and modify props.

What are vue high-order components?

The operating environment of this tutorial: windows7 system, vue3 version, DELL G3 computer.

Introduction to high-order components

vue Understanding of high-order components, in React components are implemented with reused code, while in Vue they are implemented with mixins, and The official documentation also lacks some concepts of high-order components, because it is difficult to implement high-order components in Vue and is not as simple as React. In fact, mixins in Vue are also replaced by mixins. After reading part of the source code, I have a deeper understanding of Vue. Understanding

The so-called high-order component is actually a high-order function, that is, a function that returns a component function. How to implement it in Vue? Note that high-order components have the following characteristics

高阶组件(HOC)应该是无副作用的纯函数,且不应该修改原组件,即原组件不能有变动
高阶组件(HOC)不关心你传递的数据(props)是什么,并且新生成组件不关心数据来源
高阶组件(HOC)接收到的 props 应该传递给被包装组件即直接将原组件prop传给包装组件
高阶组件完全可以添加、删除、修改 props

Examples of high-order components

Base.vue

d477f9ce7bf77f53fbcf36bec1b69b7a
  dc6dce4a544fdca2df29d5ac0ea9906b
    be3b8d7b68acfe2d6133cd36dc6a63f1props: {{test}}94b3e26ee717c64999d7867364b1b4a3
  16b28748ea4df4d9c2150843fecfba68
21c97d3a051048b8e55e3c8f199a54b2
3f1c4e4b6b16bbbd69b2ee476dc4f83a
export default {
  name: 'Base',
  props: {
    test: Number
  },
  methods: {
    Click () {
      this.$emit('Base-click')
    }
  }
}
2cacc6d41bbb37262a98f745aa00fbf0

Vue components mainly have three points: props, events and slots. For the Base component component, it receives a numeric type props, namely test, and triggers a custom event. The name of the event is: Base-click, without slots. We will use the component like this:

Now we need the base-component component to print a sentence: haha ​​every time it is mounted. At the same time, this may be a requirement of many components, so according to the mixins method, we You can do this, first define mixins

export default consoleMixin {
  mounted () {
    console.log('haha')
  }
}

and then mix consoleMixin in the Base component:

d477f9ce7bf77f53fbcf36bec1b69b7a
  dc6dce4a544fdca2df29d5ac0ea9906b
    be3b8d7b68acfe2d6133cd36dc6a63f1props: {{test}}94b3e26ee717c64999d7867364b1b4a3
  16b28748ea4df4d9c2150843fecfba68
21c97d3a051048b8e55e3c8f199a54b2
3f1c4e4b6b16bbbd69b2ee476dc4f83a
export default {
  name: 'Base',
  props: {
    test: Number
  },
  mixins: [ consoleMixin ],
  methods: {
    Click () {
      this.$emit('Base-click')
    }
  }
}
2cacc6d41bbb37262a98f745aa00fbf0

When using the Base component in this way, a haha ​​message will be printed after each mounting is completed, but Now we are going to use higher-order components to achieve the same function. Recall the definition of higher-order components: receive a component as a parameter and return a new component. So what we need to think about at this time is, what is a component in Vue? Components in Vue are functions, but that is the final result. For example, our component definition in a single-file component is actually an ordinary options object, as follows:

export default {
  name: 'Base',
  props: {...},
  mixins: [...]
  methods: {...}
}

Isn’t this a pure object?

import Base from './Base.vue'
console.log(Base)

What is Base here? It is a JSON object. When you add it to the components of a component, Vu will eventually construct the constructor of the instance with this parameter, that is, option, so the component in Vue is a function. But before it is introduced, it is still just an options object, so it is easy to understand that the component in Vue is just an object at first, that is, the higher-order component is a function that accepts a pure object and returns a new pure object

export default function Console (BaseComponent) {
  return {
    template: '0040ddd46d317a97de61e65cb3442a89',
    components: {
      wrapped: BaseComponent
    },
    mounted () {
      console.log('haha')
    }
  }
}

Console here is a higher-order component. It accepts a parameter BaseComponent, which is the passed in component, returns a new component, uses BaseComponent as a subcomponent of the new component and sets a hook function in mounted to print haha. We can do the same with mixins. The thing is, we have not modified the sub-component Base. The $listeners $attrs here are actually transparently transmitting props and events. Does this really solve the problem perfectly? No, first of all, the template option can only be used in the full version of Vue, not in the runtime version, so at the very least we should use the render function (render) instead of the template (template)

Console. js

export default function Console (BaseComponent) {
  return {
    mounted () {
      console.log('haha')
    },
    render (h) {
      return h(BaseComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
      })
    }
  }
}

We rewrote the template into a rendering function. It seems that there is no problem, but in fact there is still a problem. In the above code, the BaseComponent component still cannot receive props. Why, aren't we already in the h function? Have you passed attrs among the two parameters? Why can't you receive it? Of course you can't receive it. Attrs refers to attributes that have not been declared as props, so you need to add props parameters in the rendering function:

export default function Console (BaseComponent) {
  return {
    mounted () {
      console.log('haha')
    },
    render (h) {
      return h(BaseComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

Then it still doesn't work. Props are always empty objects. Here props is the object of the higher-order component, but the higher-order component does not declare props, so we need to declare another props

export default function Console (BaseComponent) {
  return {
    mounted () {
      console.log('haha')
    },
    props: BaseComponent.props,
    render (h) {
      return h(BaseComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

ok. A similar higher-order component is completed, but if it is still possible, we only implement it. Transparently transmitting props and transparently transmitting events, emmmm only slots are left. We modify the Base component to add a named slot and a default slot Base.vue

d477f9ce7bf77f53fbcf36bec1b69b7a
  dc6dce4a544fdca2df29d5ac0ea9906b
    0817b6bf5b3183ab1a144d078c45fdbcprops: {{test}}54bdf357c58b8a65c66d7c19c8e4d114
    e8dddc28d3bfad1f72ef15bb99cfb297 ce4ce005a24c94d9b4e4a39d8a2161ed7971cf77a46923278913ee247bc958ee
    e388a4556c0f65e1904146cc1a846bee===========94b3e26ee717c64999d7867364b1b4a3
    58cb293b8600657fad49ec2c8d37b47238b537b6886351ac721d89624ba185ca 4842836e9b56128d85f8ca400a7b33ba
  16b28748ea4df4d9c2150843fecfba68
21c97d3a051048b8e55e3c8f199a54b2
 
3f1c4e4b6b16bbbd69b2ee476dc4f83a
export default {
  ...
}
2cacc6d41bbb37262a98f745aa00fbf0

d477f9ce7bf77f53fbcf36bec1b69b7a
  dc6dce4a544fdca2df29d5ac0ea9906b
    88d574120e9e0d5bf9eb3bcd60be7003
      9d8e5d4a2d5c644920def765daa3de8aBaseComponent slot2e9b454fa8428549ca2e64dfac4625cd
      e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3
    686fde5332128d14fa2aa6dd6ebb0c77
    df1b898a74c042562246884f2d946ef4
      9d8e5d4a2d5c644920def765daa3de8aEnhancedComponent slot2e9b454fa8428549ca2e64dfac4625cd
      e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3
    c29ea36a6f271e266acde995154dc3a8
  16b28748ea4df4d9c2150843fecfba68
21c97d3a051048b8e55e3c8f199a54b2
 
3f1c4e4b6b16bbbd69b2ee476dc4f83a
  import Base from './Base.vue'
  import hoc from './Console.js'
 
  const wrapBase = Console(Base)
 
  export default {
    components: {
      Base,
      wrapBase
    }
  }
2cacc6d41bbb37262a98f745aa00fbf0

The execution result here is that there are no slots in wrapBase. So we have to change the high-level components

function Console (BaseComponent) {
  return {
    mounted () {
      console.log('haha')
    },
    props: BaseComponent.props,
    render (h) {
 
      // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
 
      return h(BaseComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      }, slots) // 将 slots 作为 h 函数的第三个参数
    }
  }
}

At this time, the slot content is indeed rendered, but the order is not right, and all high-order components are rendered to the end. . In fact, Vue will consider scope factors when processing named slots. First, Vue will compile the template into a rendering function (render). For example, the following template:

dc6dce4a544fdca2df29d5ac0ea9906b
  74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3
16b28748ea4df4d9c2150843fecfba68

will be compiled into the following rendering function:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [
    _c("div", {
      attrs: { slot: "slot1" },
      slot: "slot1"
    }, [
      _vm._v("Base slot")
    ])
  ])
}

Observing the above rendering function, we find that the ordinary DOM creates the corresponding VNode through the _c function. Now we modify the template. In addition to the ordinary DOM, the template also has components, as follows:

dc6dce4a544fdca2df29d5ac0ea9906b
  88d574120e9e0d5bf9eb3bcd60be7003
    74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3
    e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3
  686fde5332128d14fa2aa6dd6ebb0c77
16b28748ea4df4d9c2150843fecfba68

its render function

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    [
      _c("Base", [
        _c("p", { attrs: { slot: "slot1" }, slot: "slot1" }, [
          _vm._v("Base slot")
        ]),
        _vm._v(" "),
        _c("p", [_vm._v("default slot")])
      ])
    ],
  )
}

我们发现无论是普通DOM还是组件,都是通过 _c 函数创建其对应的 VNode 的 其实 _c 在 Vue 内部就是 createElement 函数。createElement 函数会自动检测第一个参数是不是普通DOM标签如果不是普通DOM标签那么 createElement 会将其视为组件,并且创建组件实例,注意组件实例是这个时候才创建的 但是创建组件实例的过程中就面临一个问题:组件需要知道父级模板中是否传递了 slot 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下

dc6dce4a544fdca2df29d5ac0ea9906b
  88d574120e9e0d5bf9eb3bcd60be7003
    74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3
    e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3
  686fde5332128d14fa2aa6dd6ebb0c77
16b28748ea4df4d9c2150843fecfba68

父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到

88d574120e9e0d5bf9eb3bcd60be7003
    74997f6adba13580e4e439db54218f0dBase slot94b3e26ee717c64999d7867364b1b4a3
    e388a4556c0f65e1904146cc1a846beedefault slot94b3e26ee717c64999d7867364b1b4a3
  686fde5332128d14fa2aa6dd6ebb0c77

如果能够通过父级拿到这段模板对应的 VNode,那么子组件就知道要渲染哪些 slot 了,其实 Vue 内部就是这么干的,实际上你可以通过访问子组件的 this.$vnode 来获取这段模板对应的 VNode

this.$vnode 并没有写进 Vue 的官方文档

子组件拿到了需要渲染的 slot 之后进入到了关键的一步,这一步就是导致高阶组件中透传 slot 给 Base组件 却无法正确渲染的原因 children的VNode中的context引用父组件实例 其本身的context也会引用本身实例 其实是一个东西

console.log(this. vnode.context===this.vnode.componentOptions.children[0].context) //ture

而 Vue 内部做了一件很重要的事儿,即上面那个表达式必须成立,才能够正确处理具名 slot,否则即使 slot 具名也不会被考虑,而是被作为默认插槽。这就是高阶组件中不能正确渲染 slot 的原因

即 高阶组件中 本来时父组件和子组件之间插入了一个组件(高阶组件),而子组件的 this.$vnode其实是高阶组件的实例,但是我们将slot透传给子组件,slot里 VNode 的context实际引用的还是父组件 所以

console.log(this.vnode.context === this.vnode.componentOptions.children[0].context) // false

最终导致具名插槽被作为默认插槽,从而渲染不正确。

决办法也很简单,只需要手动设置一下 slot 中 VNode 的 context 值为高阶组件实例即可

function Console (Base) {
  return {
    mounted () {
      console.log('haha')
    },
    props: Base.props,
    render (h) {
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
        // 手动更正 context
        .map(vnode => {
          vnode.context = this._self //绑定到高阶组件上
          return vnode
        })
 
      return h(WrappedComponent, {
        on: this.$listeners,
        props: this.$props,
        attrs: this.$attrs
      }, slots)
    }
  }
}

说明白就是强制把slot的归属权给高阶组件 而不是 父组件 通过当前实例 _self 属性访问当实例本身,而不是直接使用 this,因为 this 是一个代理对象

【相关推荐:vuejs视频教程web前端开发

The above is the detailed content of What are vue high-order components?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn