异步组件要解决的问题

本章要讲的是Vue3对于异步组件的支持(如何封装defineAsyncComponent函数), 以及兼容函数式组件的渲染(这两个点牛马不相及不知道为什么能放在同一章)

<template>
  <CompA />
  <component :is="asyncComp" />
</template>
<script>
import { shallowRef } from "vue";
import CompA from "CompA.vue";

export default {
  components: { CompA },
  setup() {
    const asyncComp = shallowRef(null);

    // 异步加载 CompB 组件
    import("CompB.vue").then((CompB) => (asyncComp.value = CompB));

    return {
      asyncComp,
    };
  },
};
</script>

上面代码是一个简单的实现异步组件的示例。但是这是非常简单的一个示例,它缺少很多功能:

  1. 加载失败或加载超时的处理
  2. 加载状态的处理
  3. 加载失败重试的处理

异步组件的实现原理

虽然每个人都能设计代码解决上面的几个异步组件的问题,但是Vue就是比较贴心的提供了API,已经帮忙处理了这部分逻辑(有时候我觉得Vue管的太宽了)

<template>
  <AsyncComp />
</template>
<script>
export default {
  components: {
    // 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
    AsyncComp: defineAsyncComponent(() => import("CompA")),
  },
};
</script>

其调用方法如上所示例,当然,还有很多配置项并没有放出来。

function defineAsyncComponent(options) {
  if (typeof options === "function") {
    options = {
      loader: options,
    };
  }

  const { loader } = options;

  let InnerComp = null;

  return {
    name: "AsyncComponentWrapper",
    setup() {
      const loaded = ref(false);
      const error = shallowRef(null);
      // 一个标志,代表是否正在加载,默认为 false
      const loading = ref(false);

      let loadingTimer = null;
      // 如果配置项中存在 delay,则开启一个定时器计时,当延迟到时后将loading.value 设置为 true
      if (options.delay) {
        loadingTimer = setTimeout(() => {
          loading.value = true;
        }, options.delay);
      } else {
        // 如果配置项中没有 delay,则直接标记为加载中
        loading.value = true;
      }
      loader()
        .then((c) => {
          InnerComp = c;
          loaded.value = true;
        })
        .catch((err) => (error.value = err))
        .finally(() => {
          loading.value = false;
          // 加载完毕后,无论成功与否都要清除延迟定时器
          clearTimeout(loadingTimer);
        });

      let timer = null;
      if (options.timeout) {
        timer = setTimeout(() => {
          const err = new Error(`Async component timed out after ${options.timeout}ms.`);
          error.value = err;
        }, options.timeout);
      }

      const placeholder = { type: Text, children: "" };

      return () => {
        if (loaded.value) {
          return { type: InnerComp };
        } else if (error.value && options.errorComponent) {
          return {
            type: options.errorComponent,
            props: { error: error.value },
          };
        } else if (loading.value && options.loadingComponent) {
          // 如果异步组件正在加载,并且用户指定了 Loading 组件,则渲染Loading 组件
          return { type: options.loadingComponent };
        } else {
          return placeholder;
        }
      };
    },
  };
}

以上代码是比较全面的defineAsyncComponent代码,但是还没有增加重试机制。因为后面的代码都忽略了部分代码,没有那么全。我也懒得再去梳理了,因为这个逻辑并不复杂,只是很麻烦,而且没有必要了解的那么清楚。

函数组件

这里引出函数组件的原因在于上面的异步组件的封装是一个高阶函数,也就是函数组件的形式。但是我们之前实现的渲染器中,并没有对函数组件的实现。所以需要在此进行说明补充。

首先,我们需要在渲染器的patch 函数中增加对函数组件类型的判断,并让其走函数渲染的逻辑。

其次,函数组件的渲染函数即是它自己本身。由于其state是直接通过函数作用域保存,所以不需要处理,只需要处理props的逻辑即可。

代码就不放了,没有很复杂。