yliu

时来天地皆同力,运去英雄不自由

hook下如何书写发布订阅


这篇文章的思路来源为 ahooks,因为 react 已经有相对应的实现了,所以这里主要介绍 vue 下如何实现一个 hook 的发布订阅。

假设有一个需求,当价格发生改变的时候刷新列表,在 vue 中很自然而然想到调用 watch 观察,但是这样的调用还是有点繁琐。例如下面一段伪代码

import { ref, watch } from 'vue';

const isChange = ref(false);
const refreshList = () => {
  // 省略
  isChange.value = false;
};

const onClick = () => {
  isChange.value = true;
};

watch(isChange, (val) => {
  if (!val) {
    return;
  }
  refreshList();
});

但是仔细观察一下,其实我们只是想让变化的时候通知一下,然后调用 refreshList 即可。

且我们也不希望每次都复制这样一段代码在其他组件中重复使用,基于上面的场景我们很容易想到可以写一个发布订阅的模块来实现我们需求。

EventEmitter

class EventEmitter {
  subscriptions = new Set();

  emit = (val) => {
    for (const subscription of this.subscriptions) {
      subscription(val);
    }
  };

  useSubscription = (callback) => {
    this.subscriptions.add(subscription);
  };
}

上面的发布订阅模块很简单,useSubscription 来订阅,emit 来进行通知。下面是使用形式

const event = new EventEmitter();
const refreshList = () => {};
const fn = (val) => {
  if (val !== 'refresh') {
    return;
  }
  refreshList();
};
event.useSubscription('refresh', fn);

const onClick = () => {
  event.emit('refresh');
};

onUnmounted(() => {
  event.subscriptions.delete(fn);
});

使用方式稍微简化了一下,不过也多出了 onUnmounted,这也谈不上方便,因为会有额外的心智负担,上面之所以单独写一个 fn 就是因为卸载的时候需要卸载对应的函数才行。

基于简化和不想重复管理的需求,我们再重新写一版,这里我们采用 hook 的形式来书写

useEventEmitter

class EventEmitter {
  subscriptions = new Set();

  emit = (val) => {
    for (const subscription of this.subscriptions) {
      subscription(val);
    }
  };

  useSubscription = (callback) => {
    this.subscriptions.add(callback);
    onUnmounted(() => {
      this.subscriptions.delete(callback);
    });
  };
}
const useEventEmitter = () => {
  const event = new EventEmitter();

  return event;
};

上面的逻辑改变了一下,我们在 useSubscription 中调用 onUnmounted 来完成一个自动的卸载。

之后使用方法

const event = useEventEmitter();

const refreshList = () => {};

event.useSubscription((val) => {
  if (val !== 'refresh') {
    return;
  }
  refreshList();
});

const onClick = () => {
  event.emit('refresh');
};

这里就达到我们期待的一个效果了,不过还是有需要改进的地方:

在使用发布订阅的场景基本上组件之间的层级会嵌套很多,例如上面每次调用 useEventEmitter 生成一个新的 event 所以通过单例简化为全局唯一的 event 即可,或者也可以借助 provideinject 解决这一问题

改进之后

class EventEmitter {
  subscriptions = new Set();

  emit = (val) => {
    for (const subscription of this.subscriptions) {
      subscription(val);
    }
  };

  useSubscription = (callback) => {
    this.subscriptions.add(callback);
    onUnmounted(() => {
      this.subscriptions.delete(callback);
    });
  };
}
const event = new EventEmitter();
const useEventEmitter = () => {
  return event;
};