Vue.js 中的 when 函数实现

MobX.js 提供了一种响应式编程的方式,可以自动跟踪状态的变化并对此做出反应。在 MobX 中,when 函数是一种便利的机制,用于观察一个特定的条件,并在该条件成立时执行一些操作。

最简单的例子:

async function fetchData() {
    await when(() => that.isVisible)
    // do your staff
}

when函数接收两个参数,第一个参数是一个返回布尔值的函数,第二个参数是一个回调函数。当第一个函数返回 true 时,MobX 将运行第二个参数的回调函数,并且自动清理所有的相关依赖。

此外,when还返回一个可用于手动清理依赖的函数,如果你想在条件尚未满足时就中止观察,可以调用这个函数。

对于异步操作,when 函数还有一个更有用的特性,你可以在 when 函数前加上 await 关键字,从而等待条件成立。这可以让你用同步的方式编写异步的代码。

MobX 需要 when 函数,因为这是观察状态并在条件满足时执行操作的一种强大方式。使用 when,你可以编写出更清晰、更直观的代码,也可以避免不必要的状态检查和循环等待。

实际项目中的例子:

需求背景是获取到远端的 canUseMembers 后才进行初始化,但是 remoteStore 可能是被业务初始化好,也可能没有初始化好,或者可能被清理了数据,或者说前置有调用 getRemoteConfigs 的逻辑。那么此时,我们需要使用 watch 来保证远端数据是取好的,然后开始初始化。

<script setup lang="ts">
import { RemoteStore } from './RemoteStore';

const remoteStore = RemoteStore();
const useRemoteData = () => {
  const remoteMembers = ref([]);
  const init = async () => {
    try {
      const res = await fetchRemoteMembers();
      remoteMembers.value = res;
    } catch(err){
      console.error(err);
    }
  }

  watch(() => remoteStore.remoteConfigs.value.canUseMembers.length > 0, () => {
    init();
  })

}
</script>

RemoteStore 模块的结合实现👇🏻

import { watch, ref } from 'vue';

const RemoteStore = () => {
  const remoteConfigs = ref({});
  // 不确定什么时候调用 getRemoteConfigs
  const getRemoteConfigs = async () => {
    const result = await fetchRemoteConfigs();
    remoteConfigs.value = result;
  }
  return {
    remoteConfigs
  }
}

实现

when 函数的实现主要运用了 Vue.js 的 watch 和 onScopeDispose。我们在给定的作用t域内运行一个新的效果,当这个效果返回 true 时,我们解析 Promise。同时我们还添加了一个清理函数,当作用域被销毁时,我们清理定时器并拒绝 Promise。

这里用到了 Vue 的 EffectScope,请查阅 进阶:搞懂 Vue 的 EffectScope

import { type EffectScope, watch, onScopeDispose } from 'vue';

/**
 * @see https://mobx.js.org/reactions.html#await-when
 */
export function when(
  effect: () => boolean,
  scope: EffectScope,
  options?: {
    /** 超时时间,超出时间后调用 Promise.reject */
    timeout?: number;
  },
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    let timeoutTimer = 0;
    if (options && options.timeout) {
      timeoutTimer = window.setTimeout(() => {
        reject(new Error('Effect function timeout'));
      }, options.timeout);
    }

    scope.run(() => {
      let disposed = false;
      watch(
        effect,
        flag => {
          if (flag && !disposed) {
            disposed = true;
            resolve();
          }
        },
        { immediate: true },
      );
      onScopeDispose(() => {
        if (timeoutTimer) {
          window.clearTimeout(timeoutTimer);
        }
        reject(new Error('Scope disposed'));
      });
    });
  });
}

使用手册

when 函数接收三个参数:

effect: 一个返回布尔值的函数,当这个函数返回 true 时,when 返回的 Promise 会被解析。
scope: 一个 EffectScope 对象,when 中的效果会在这个作用域内运行。
options: 包含选项的对象。当前只支持 timeout 属性,表示在超时后应拒绝 Promise。

现在,你可以在vue组件中、hook函数中通过when来收敛你的逻辑代码了。