# Vue高阶组件HOC

[toc]

HOC在React中比较流行,高阶组件在Vue中可以认为是一个接收组件为参数的函数,同时抽象出通用逻辑,再返回一个新的组件。

在 React 中

在 React 里,组件是 Class,所以高阶组件有时候会用 装饰器 语法来实现,因为 装饰器 的本质也是接受一个 Class 返回一个新的 Class

在 React 的世界里,高阶组件就是 f(Class) -> 新的Class

在 Vue 中

在 Vue 的世界里,组件是一个对象,所以高阶组件就是一个函数接受一个对象,返回一个新的包装好的对象。

类比到 Vue 的世界里,高阶组件就是 f(object) -> 新的object

# 智能组件和木偶组件

木偶 组件: 就像一个牵线木偶一样,只根据外部传入的 props 去渲染相应的视图,而不管这个数据是从哪里来的。

智能 组件: 一般包在 木偶 组件的外部,通过请求等方式获取到数据,传入给 木偶 组件,控制它的渲染。

<智能组件>
  <木偶组件 />
</智能组件>

它们还有另一个别名,就是 容器组件ui组件

# 查看一个异步请求数据的案例

<template>
  <div>
    <p v-if="error">Error: {{error.message}}</p>
    <p v-else-if="isLoading && isDelayElapsed">Loading...</p>
    <ul v-else-if="!isLoading">
      <li v-for="user in data"
          :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';

// 模拟后端接口
const getUsers = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ name: 'A' }, { name: 'B' }]);
    }, 1000);
  });
};

export default defineComponent({
  name: 'Users',
  // props: {},
  // emits: {},
  // components: {},
  setup(props, context) {
    console.log('setup');
    const error = ref(null);
    const isLoading = ref(false);
    const isDelayElapsed = ref(false);
    const data = ref([]);

    const fetchUser = () => {
      error.value = null;
      isLoading.value = true;
      isDelayElapsed.value = false;

      getUsers()
        .then((res) => {
          data.value = res;
        })
        .catch((error) => {
          error.value = error;
        })
        .finally(() => {
          isLoading.value = false;
        });
      setTimeout(() => {
        // 200 毫秒之后才出现loading状态, 防止接口返回过快, loading闪烁
        isDelayElapsed.value = true;
      }, 200);
    };
    onMounted(() => {
      fetchUser();
    });

    return { error, isLoading, isDelayElapsed, data };
  }
});
</script>
<style scoped>
</style>

一般我们都这样写,平常也没感觉有啥问题,但是其实我们每次在写异步请求的时候都要有 loadingerror 状态,都需要有 取数据 的逻辑,并且要管理这些状态。

那么想个办法抽象它?好像特别好的办法也不多,React 社区在 Hook 流行之前,经常用 HOC(high order component) 也就是高阶组件来处理这样的抽象。

# 实现一个HOC的思路

下面是一个智能组件的实现思路

usePromise.js

import { defineComponent, ref, h, toRaw, onMounted } from 'vue';

// 智能组件
const usePromise = (Component, PromiseFn) => {
  return defineComponent({
    name: 'UsePromise',
    inheritAttrs: false,
    setup(props, context) {
      const error = ref(null);
      const isLoading = ref(false);
      const isDelayElapsed = ref(false);
      const data = ref([]);
      const ComponentRef = ref(null);

      // onMounted(() => {});

      const handleGetData = params => {
        error.value = null;
        isLoading.value = true;
        isDelayElapsed.value = false;

        PromiseFn(params)
          .then(res => {
            data.value = res;
          })
          .catch(error => {
            error.value = error;
          })
          .finally(() => {
            isLoading.value = false;
          });

        setTimeout(() => {
          // 200 毫秒之后才出现loading状态, 防止接口返回过快, loading闪烁
          isDelayElapsed.value = true;
        }, 200);
      };

      return () =>
        h(
          'div',
          {
            class: ['use-promise__wrapper']
          },
          [
            error.value ? h('p', {}, 'Error: ' + error.message) : null,
            isLoading.value && isDelayElapsed.value ? h('p', {}, 'Loading...') : null,
            h(Component, {
              ref: ComponentRef,

              ...toRaw(props),
              ...toRaw(context.attrs),
              data: data.value,
              onGetData: handleGetData
            })
          ]
        );
    }
  });
};

export default usePromise;

就是把异步请求和一些loading状态抽象出来。

实现属性和事件透传,木偶组件通过触发智能组件绑定的函数传递参数给api调用。

组件的使用

<script lang="ts">
import { defineComponent, h, onMounted } from 'vue';
// import Users from './Users.vue';

import usePromise from './usePromise.js';

// 模拟后端接口
const getUsers = (params = []) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ name: 'A' }, { name: 'B' }, ...params]);
    }, 1000);
  });
};

// 木偶组件,只用于显示内容
const ListCompnent = defineComponent({
  name: 'ListCompnent',
  // inheritAttrs: false,
  emits: ['getData', 'itemClick'],
  props: {
    data: {
      type: Array,
      default: () => []
    }
  },
  setup(props, context) {
    const { slots, emit, attrs } = context;
    onMounted(() => {
      emit('getData', [{ name: 'C' }]);
    });
    return () =>
      h(
        'ul',
        {
          class: ['ul-wrapper']
        },
        props.data.map((item) => {
          return h(
            'li',
            {
              key: item.name,
              'data-name': item.name,
              onClick: () => {
                emit('itemClick', item.name);
              }
            },
            item.name
          );
        })
      );
  }
});

const ListCompnentHoc = usePromise(ListCompnent, getUsers);

export default defineComponent({
  name: 'HOC',
  // props: {},
  // emits: {},
  // components: {},
  setup(props, context) {
    return () =>
      h('div', {}, [
        h('h1', 'hoc'),
        h(ListCompnentHoc, {
          onItemClick: (a) => {
            console.debug('onItemClick', a);
          }
        })
      ]);
  }
});
</script>
<style scoped>
</style>

这只是一个思路,遇到一些常用的业务场景可以这样子抽象, 但是也不是说抽象就一定是ok的。

# 避免不必要的组件抽象

Vue文档 (opens new window)

有些时候我们会去创建无渲染组件 (opens new window)或高阶组件 (用来渲染具有额外 props 的其他组件) 来实现更好的抽象或代码组织。虽然这并没有什么问题,但请记住,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。

需要提醒的是,只减少几个组件实例对于性能不会有明显的改善,所以如果一个用于抽象的组件在应用中只会渲染几次,就不用操心去优化它了。考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。

vue-promised (opens new window)

上次更新: 1/22/2025, 9:39:13 AM