Skip to content

13. 异步组件与函数式组件

在 Vue.js 3 中使用函数式组件,主要是因为它的简单性,而不是因为它的性能好

13.1  异步组件要解决的问题

  1. 允许用户指定加载出错时要渲染的组件。
  2. 允许用户指定 Loading 组件,以及展示该组件的延迟时间。
  3. 允许用户设置加载组件的超时时长。
  4. 组件加载失败时,为用户提供重试的能力。

13.2  异步组件的实现原理

1. 封装 defineAsyncComponent 函数

html
<template>
  <AsyncComp />
</template>
<script>
  export default {
    components: {
      // 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
      AsyncComp: defineAsyncComponent(() => import("CompA")),
    },
  };
</script>
js
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(loader) {
  // 一个变量,用来存储异步加载的组件
  let InnerComp = null;
  // 返回一个包装组件
  return {
    name: "AsyncComponentWrapper",
    setup() {
      // 异步组件是否加载成功
      const loaded = ref(false);
      // 执行加载器函数,返回一个 Promise 实例
      // 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
      loader().then((c) => {
        InnerComp = c;
        loaded.value = true;
      });

      return () => {
        // 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
        return loaded.value
          ? { type: InnerComp }
          : { type: Text, children: "" };
      };
    },
  };
}

2. 超时与 Error 组件

js
const AsyncComp = defineAsyncComponent({
  loader: () => import("CompA.vue"),
  timeout: 2000, // 超时时长,其单位为 ms
  errorComponent: MyErrorComp, // 指定出错时要渲染的组件
});

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

  const { loader } = options;

  let InnerComp = null;

  return {
    name: "AsyncComponentWrapper",
    setup() {
      const loaded = ref(false);
      // 定义 error,当错误发生时,用来存储错误对象
      const error = shallowRef(null);

      loader()
        .then((c) => {
          InnerComp = c;
          loaded.value = true;
        })
        // 添加 catch 语句来捕获加载过程中的错误
        .catch((err) => (error.value = err));

      let timer = null;
      if (options.timeout) {
        timer = setTimeout(() => {
          // 超时后创建一个错误对象,并复制给 error.value
          const err = new Error(
            `Async component timed out after ${options.timeout}ms.`
          );
          error.value = err;
        }, options.timeout);
      }

      // 包装组件被卸载时清除定时器
      onUmounted(() => clearTimeout(timer));

      // 占位内容
      const placeholder = { type: Text, children: "" };

      return () => {
        if (loaded.value) {
          // 如果组件异步加载成功,则渲染被加载的组件
          return { type: InnerComp };
        } else if (error.value && options.errorComponent) {
          // 只有当错误存在且用户配置了 errorComponent 时才展示 Error 组件,同时将 error 作为 props 传递
          return {
            type: options.errorComponent,
            props: { error: error.value },
          };
        } else {
          return placeholder;
        }
      };
    },
  };
}

3. 延迟与 Loading 组件

在网络良好的情况下添加 loading 组件会造成组件切换时页面闪烁的问题,因此延迟加载 loading 组件更能提升用户体验

js
defineAsyncComponent({
  loader: () =>
    new Promise((r) => {
      /* ... */
    }),
  // 延迟 200ms 展示 Loading 组件
  delay: 200,
  // Loading 组件
  loadingComponent: {
    setup() {
      return () => {
        return { type: "h2", children: "Loading..." };
      };
    },
  },
});
js
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;
        }
      };
    },
  };
}

修改 unmount 函数以支持异步组件的卸载

js
function unmount(vnode) {
  if (vnode.type === Fragment) {
    vnode.children.forEach((c) => unmount(c));
    return;
  } else if (typeof vnode.type === "object") {
    // 对于组件的卸载,本质上是要卸载组件所渲染的内容,即 subTree
    unmount(vnode.component.subTree);
    return;
  }
  const parent = vnode.el.parentNode;
  if (parent) {
    parent.removeChild(vnode.el);
  }
}

4. 重试机制

先看下接口重试

js
function fetch() {
  return new Promise((resolve, reject) => {
    // 请求会在 1 秒后失败
    setTimeout(() => {
      reject("err");
    }, 1000);
  });
}
// load 函数接收一个 onError 回调函数
function load(onError) {
  // 请求接口,得到 Promise 实例
  const p = fetch();
  // 捕获错误
  return p.catch((err) => {
    // 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
    // 同时将 retry 函数作为 onError 回调的参数
    return new Promise((resolve, reject) => {
      // retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
      const retry = () => resolve(load(onError));
      const fail = () => reject(err);
      onError(retry, fail);
    });
  });
}

// 调用 load 函数加载资源
load(
  // onError 回调
  (retry) => {
    // 失败后重试
    retry();
  }
).then((res) => {
  // 成功
  console.log(res);
});

组件重试

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

  const { loader } = options;

  let InnerComp = null;

  // 记录重试次数
  let retries = 0;
  // 封装 load 函数用来加载异步组件
  function load() {
    return (
      loader()
        // 捕获加载器的错误
        .catch((err) => {
          // 如果用户指定了 onError 回调,则将控制权交给用户
          if (options.onError) {
            // 返回一个新的 Promise 实例
            return new Promise((resolve, reject) => {
              // 重试
              const retry = () => {
                resolve(load());
                retries++;
              };
              // 失败
              const fail = () => reject(err);
              // 作为 onError 回调函数的参数,让用户来决定下一步怎么做
              options.onError(retry, fail, retries);
            });
          } else {
            throw error;
          }
        })
    );
  }

  return {
    name: "AsyncComponentWrapper",
    setup() {
      const loaded = ref(false);
      const error = shallowRef(null);
      const loading = ref(false);

      let loadingTimer = null;
      if (options.delay) {
        loadingTimer = setTimeout(() => {
          loading.value = true;
        }, options.delay);
      } else {
        loading.value = true;
      }
      // 调用 load 函数加载组件
      load()
        .then((c) => {
          InnerComp = c;
          loaded.value = true;
        })
        .catch((err) => {
          error.value = err;
        })
        .finally(() => {
          loading.value = false;
          clearTimeout(loadingTimer);
        });

      // 省略部分代码
    },
  };
}

13.3  函数式组件

一个函数式组件就是一个返回虚拟 DOM 的函数,函数式组件没有自身状态,但它仍然可以接收由外部传入的 props

js
function MyFuncComp(props) {
  return { type: "h1", children: props.title };
}
// 定义 props
MyFuncComp.props = {
  title: String,
};

patch 函数内部,通过检测 vnode.type 的类型来判断组件的类型

js
function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  const { type } = n2;

  if (typeof type === "string") {
    // 省略部分代码
  } else if (type === Text) {
    // 省略部分代码
  } else if (type === Fragment) {
    // 省略部分代码
  } else if (
    // type 是对象 --> 有状态组件
    // type 是函数 --> 函数式组件
    typeof type === "object" ||
    typeof type === "function"
  ) {
    // component
    if (!n1) {
      mountComponent(n2, container, anchor);
    } else {
      patchComponent(n1, n2, anchor);
    }
  }
}

但无论是有状态组件,还是函数式组件,都可以通过 mountComponent 函数来完成挂载,也都可以通过 patchComponent 函数来完成更新

js
function mountComponent(vnode, container, anchor) {
  // 检查是否是函数式组件
  const isFunctional = typeof vnode.type === "function";

  let componentOptions = vnode.type;
  if (isFunctional) {
    // 如果是函数式组件,则将 vnode.type 作为渲染函数,将 vnode.type.props 作为 props 选项定义即可
    componentOptions = {
      render: vnode.type,
      props: vnode.type.props,
    };
  }

  // 省略部分代码
}

上次更新于: