Implementation of using HOC pattern in Vue

  • 2021-08-06 20:57:22
  • OfStack

Preface

HOC is a common mode of React, but can HOC only be played in React? Let's take a look at how the official document of React introduces HOC:

High-order component (HOC) is an advanced technique for reusing component logic in React. HOC itself is not a part of ReactAPI, but a design pattern based on the combined characteristics of React.

HOC It is a pattern, is an idea, not only in React can be used. Therefore, combined with the characteristics of Vue, one can play HOC in Vue.

HOC

Problems to be solved by HOC

It is not to say that whichever technology is novel has to be used. It depends on what pain points this technology can solve.

HOC mainly solves the problem of reusability. In Vue, this problem is generally solved by Mixin. Mixin is a way to extend the collection function, which essentially copies the properties of one object to another.

Initially, React also used Mixin, but later found that Mixin is not a good mode in React, and it has the following disadvantages:

mixin is prone to naming conflicts with components mixin is intrusive, changing the original components and greatly improving the complexity.

Therefore, React is gradually separated from mixin, so HOC is recommended. It is not that mixin is not excellent, but that mixin is not suitable for React.

What is HOC

The full name of HOC is high-order component-that is, high-order components. Specifically, a high-order component is a function whose parameter is a component and whose return value is a new component.

In React and Vue, components are functions, so high-order components are actually high-order functions, that is, functions that return 1 function.

Let's look at the use of HOC in React:


function withComponent(WrappedComponent) {
  return class extends Component {
    componentDidMount () {
      console.log(' Mount is complete ')
    }
    render() {
      return <WrappedComponent {...props} />;
    }
  }
}

withComponent is a high-order component with the following characteristics:

HOC is a pure function and should not modify the original component HOC doesn't care what the passing props is, and WrappedComponent doesn't care about the data source The props received by HOC should be transmitted through to WrapperComponent

Using HOC in Vue

How can I use HOC mode on Vue?

The Vue component we wrote in general is like this:


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

The function of withComponet is to print a sentence after each mount: Mount is completed.

Since HOC replaces mixin, let's write it once with mixin:


export default {
  mounted () {
    console.log(' Mount is complete ')
  }
}

Then import it into ChildComponent


import withComponent from './withComponent';
export default {
  ...
  mixins: ['withComponet'],
}

For this component, we call this in the parent component


<child-component :title='title' @changeTitle='changeTitle'></child-component>

<script>
import ChildComponent from './childComponent.vue';
export default {
  ...
  components: {ChildComponent}
}
</script>

Have you noticed that when we import an Vue component, we are actually importing an object?


export default {}

As for a component as a function, it is actually the result of processing. Therefore, the higher-order components in Vue can also be: receiving 1 pure object and returning 1 pure object.

So change to HOC mode, which is like this:


export default function withComponent (WrapperComponent) {
  return {
    mounted () {
      console.log(' Mount is complete ')
    },
    props: WrappedComponent.props,
    render (h) {
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      })
    }
  }
}

Attention {on: this.$listeners,attr: this.$attrs, props: this.props} This sentence is the principle of transparent transmission of props, which is equivalent to React <WrappedComponent {...props} />;

this. $props is an props attribute that has been declared, and this. $attrs is an props attribute that has not been declared. This 1 must be transmitted by two and one, and props is incomplete without which one.

For generality, the render function is used here to build, because template is only available in the full version of Vue.

This may seem like a good idea, but one important issue is that slots can be used in Vue components.

For example:


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
  <slot></slot>
 </div>
</template>

In the parent component


<child-component :title='title' @changeTitle='changeTitle'>Hello, HOC</child-component>

Slot-distributed content can be accessed using this. $solts. Each named slot has its corresponding property. For example, the contents in v-slot: foo will be found in this. $slots. foo. default property includes all nodes that are not included in the named slot, or v-slot: default.

So accessing this. $slots is most helpful when writing a component using the render function.

First convert this. $slots to an array, because the third argument of the rendering function is a child node, which is an array


export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log(' Mount is complete ')
    },
    props: WrappedComponent.props,
    render (h) {
      const keys = Object.keys(this.$slots);
      const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []);
      return h(WrapperComponent, {
        on: this.$listeners,
        attrs: this.$attrs,
        props: this.$props
      }, slotList)
    }
  }
}

At last, there is a model, but this is not over yet. You will find that all the slots that do not use names are all the same, and they are all handled by the default slot in the end.

A little wonder, go and see how the named slot is in Vue source code.
The initRender function was found in the src/core/instance/render. js file. When the render function was initialized


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

0

This 1 piece of code is used by Vue to parse and process slot.
Assign vm. $options._parentVnode to vm. $vnode, that is, $vnode is the vnode of the parent component. If the parent component exists, define renderContext = vm. $vnode. context. renderContext is the instance of the parent component to render. Then pass renderContext and $options._renderChildren as arguments into the resolveSlots () function.

Next, look at the resolveSlots () function, which is in src/core/instance/render-helper/resolve-slots.js In a file


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

1

Focus on the first if statement inside


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

2

Only when if (child. context = = = context child. fnContext = = = context) & & data & & data. slot! = null) is true, it is treated as a named slot; Otherwise, it is treated as a default slot regardless of whether it is named or not


else {
 (slots.default || (slots.default = [])).push(child)
}

Then why does the if condition on HOC not hold?

This is because due to the intervention of HOC, A component is inserted between the original parent component and the child component-that is, HOC, which leads to that this. $vode accessed in the child component is no longer vnode of the original parent component, but vnode in HOC, so this. $vnode. context refers to high-order components at this time, but we have transmitted slot thoroughly. context of VNode in slot refers to the original parent component instance, so it is not valid.

So that they are all processed as default slots.

The solution is also very simple, just manually point the context of vnode in slot to the HOC instance. Note that the current instance _ self attribute accesses the current instance itself, instead of using this directly, because this is a proxy object.


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

4

Moreover, the processing methods of scopeSlot and slot are different, so scopeSlot1 is transmitted transparently


<template>
 <div>
  <p>{{title}}</p>
  <button @click="changeTitle"></button>
 </div>
</template>

<script>
export default {
 name: 'ChildComponent',
 props: ['title'],
 methods: {
  changeTitle () {
    this.$emit('changeTitle');
  }
 }
}
</script>

5

That's it.

End

For more articles, please move to the landlord github. If you like it, please click star, which is also an encouragement to the author.


Related articles: