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.
The operating environment of this tutorial: windows7 system, vue3 version, DELL G3 computer.
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
props: {{test}}
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:
props: {{test}}
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: '', 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
props: {{test}}===========
BaseComponent slot
default slot
EnhancedComponent slot
default slot
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:
Base slot
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:
Base slot
default slot
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 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下
Base slot
default slot
父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到
Base slot
default slot
如果能够通过父级拿到这段模板对应的 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 是一个代理对象
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!