vue2 中也能使用 HOC 和 Render Props?

1,017 阅读1分钟

概述

为何只谈 vue2 中如何抽离组件公共逻辑?因为 vue3 中已经有了 composition API 来帮助我们抽离组件公共逻辑。

方案

  1. 官方提供:mixin
  2. react 社区创意:高阶组件(HOC)
  3. react 社区创意:render props
  4. 基于 hook 是一个没有 UI 的组件这一思想

例子

以下统一使用 vue 组合式 API 征求意见稿 中记录鼠标位置的功能来演示(react hook 中也是用了这个例子)。
在这个例子中,我们需要抽离重用一组信息和两组逻辑:鼠标位置信息、组件挂载后监听鼠标移动事件、组件销毁前销毁鼠标移动事件。

mixin

定义

这边是通过一个函数返回一个 mixin,你也可以直接返回一个 mixin 对象,但不推荐。

// mouseMixin.js
export default function () {
  return {
    data() {
      return {
        mouse: {
          x: 0,
          y: 0,
        },
      }
    },
    methods: {
      update(e) {
        this.mouse.x = e.clientX
        this.mouse.y = e.clientY
      }
    },
    mounted: function() {
      window.addEventListener('mousemove', this.update)
    },
    beforeDestroy() {
      window.removeEventListener('mousemove', this.update)
    },
  }
}

使用

<template>
  <!-- 这里的 mouse 对象就是从 mixin 中注入的 -->
  <h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
</template>

<script>
import mouseMixin from './mouse.js'

export default {
  name: 'MouseMixinDemo',
  mixins: [mouseMixin()],
}
</script>

高阶组件(HOC)

定义一个函数,接收一个一个组件,返回新的组件(新组件透传 props 以及定义自己的逻辑传给原组件)就是一个高阶组件。下面看看 vue2 中如何实现?

定义

export default function mouse(WrappedComponent) {
  return {
    data() {
      return {
        mouse: {
          x: 0,
          y: 0
        }
      }
    },
    methods: {
      update(e) {
        this.mouse.x = e.clientX
        this.mouse.y = e.clientY
      }
    },
    mounted: function() {
      window.addEventListener('mousemove', this.update)
    },
    beforeDestroy() {
      window.removeEventListener('mousemove', this.update)
    },
    render(h) {
      return h(WrappedComponent, {
        props: {
          // 注入 mouse
          mouse: this.mouse,
          // 透传 prop
          ...this.$props,
        },
        // 透传属性
        attrs: {
          ...this.$attrs,
        },
        // 透传事件
        on: {
          ...this.$listeners,
        },
      })
    },  
  }
}

使用

  1. 这边使用的 demo 写得稍微复杂一些,因为 HOC 是包装了一层组件,那么中间这层组件不该对用户的使用产生任何干扰,所以需要透传 props、attrs、绑定事件。
<template>
  <div>
    <h1>鼠标高阶组件演示</h1>
    <h3>{{ this.text }}</h3>
    <button v-bind="$attrs" @click="$emit('vic')">触发自定义事件</button>
    <h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
  </div>
</template>

<script>
export default {
  name: 'MouseHOCDemo',
  props: {
    mouse: Object,
    text: String,
  },
}
</script>
  1. 使用高阶函数来包装目标组件
<template>
  <MouseHOCDemo 
    text="传入的 prop 文本" 
    :disabled="false"
    @vic="log"
  />
</template>

<script>
import MouseHOCDemo from './HOC/MouseHOCDemo.vue'
import mouse from './HOC/mouse'

export default {
  components: {
    // 主意这里注册的组件是被包装过的目标组件
    MouseHOCDemo: mouse(MouseHOCDemo),
  },
  methods: {
    // 自定义事件
    log() {
      console.log('自定义事件 vic 被触发')
    },
  },
}
</script>

render props

所谓 render props 就是:定义一个组件,这个组件做两件事:

  1. 定义自己的逻辑
  2. 调用 props 中的 render 方法,将相关信息传入
    关键在于调用 props 中的 render 方法来渲染出目标组件,而在 vue2 模版中,我们是没有办法通过调用一个方法来渲染出一段 vnode 的。但是没有关系,vue 的作用域插槽给我们在 vue2 中实践 render props 开了一道口子。下面看具体的代码演示。

定义

export default {
  name: 'RenderPropsMouse',
  data() {
    return {
      mouse: {
        x: 0,
        y: 0
      }
    }
  },
  methods: {
    update(e) {
      this.mouse.x = e.clientX
      this.mouse.y = e.clientY
    }
  },
  mounted: function() {
    window.addEventListener('mousemove', this.update)
  },
  beforeDestroy() {
    window.removeEventListener('mousemove', this.update)
  },
  render(h) {
    return h('div', this.$scopedSlots.default({ mouse: this.mouse }))
  }
}

或者使用模版

<template>
  <div>
    <slot :mouse="mouse">
  </slot>
  </div>
</template>

<script>
export default {
  name: 'RenderPropsMouse',
  data() {
    return {
      mouse: {
        x: 0,
        y: 0
      }
    }
  },
  methods: {
    update(e) {
      this.mouse.x = e.clientX
      this.mouse.y = e.clientY
    }
  },
  mounted: function() {
    window.addEventListener('mousemove', this.update)
  },
  beforeDestroy() {
    window.removeEventListener('mousemove', this.update)
  },
}
</script>

使用

<template>
  <Mouse>
    <!-- 这里是关键,数据和方法都可以这般注入 -->
    <template v-slot="{ mouse }">
      <h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
    </template>
  </Mouse>
  
</template>

<script>
import Mouse from './Mouse.vue'

export default {
  components: {
    Mouse
  }
}
</script>

hook 是一个没有 UI 的组件

定义

export default {
  props: {
    mouse: {
      type: Object,
    }
  },
  methods: {
    update(e) {
      this.$emit('update:mouse', {
        x: e.clientX,
        y: e.clientY
      })
    }
  },
  mounted() {
    window.addEventListener('mousemove', this.update)
  },
  beforeDestroy() {
    window.removeEventListener('mousemove', this.update)
  },
  render() {},
}

使用

<template>
  <div>
    <h2>x: {{ mouse.x }}, y: {{ mouse.y }}</h2>
    <Mouse :mouse.sync="mouse" />
  </div>
</template>

<script>
import Mouse from './mouse.js'

export default {
  components: {
    Mouse
  },
  name: 'MouseRenderPropsDemo',
  data() {
    return {
      mouse: {
        x: 0,
        y: 0
      }
    }
  },
}
</script>

在线示例