《Vue.js 设计与实现》读书笔记 - 第13章、异步组件与函数式组件

第13章、异步组件与函数式组件

13.1 异步组件要解决的问题

用户可以简单通过 import 异步导入组件。

<template>
  <component :is="asyncComp">
</template>

<script>
export default {
  setup() {
    const asyncComp = shallowRef(null)
    import ('CompB.vue').then(CompB => asyncComp.value = CompB)
  }
}
</script>

但我们还需要考虑组件加载失败,Loading 显示,失败重试等问题。

13.2 异步组件的实现原理

卸载函数增加卸载组件的逻辑

function unmount(vnode) {
  if (vnode.type === Fragment) {
    vnode.children.forEach((c) => unmount(c))
    return
  } else if (typeof vnode.type === 'object') {
    unmount(vnode.component.subTree)
    return
  }
  const parent = vnode.el.parentNode
  if (parent) {
    parent.removeChild(vnode.el)
  }
}

在异步组件增加完善的逻辑,具体含义见代码注释。

function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = {
      loader: options,
    }
  }
  const { loader } = options
  let InnerComp = null

  let retries = 0
  function load() {
    return loader().catch((err) => {
      if (options.onError) {
        // 函数返回Promise 并让用户在retry中调用resolve
        // 这样用户retry之后 通过load().then() 可以获取组件值
        // 即使又失败 就是又返回一个Promise 对象 还是要获取Promise中resolve的值
        // 所以这样可以多次调用retry
        return new Promise((resolve, reject) => {
          const retry = () => {
            resolve(load())
            retries++
          }
          const fail = () => reject(err)
          options.onError(retry, fail, retries)
        })
      } else {
        throw err
      }
    })
  }

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

      const loadingTimer = null
      if (options.delay) {
        // 设置options.delay的话,在options.delay时间之后再把loading设置为true,
        // 防止组件加载比较快的情况下出现loading一闪而过导致用户体验差
        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))
        // 加载后设置加载成功 loading为false
        .finally(() => {
          loading.value = false
          clearTimeout(loadingTimer)
        })
      let timer = null
      if (options.timeout) {
        timer = setTimeout(() => {
          const err = new Error('异步组件加载超时')
          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) {
          // 存在错误返回error对应组件
          return { type: options.errorComponent, props: { error: error.value } }
        } else if (loading.value && options.loadingComponent) {
          // 加载中返回loading对应组件
          return { type: options.loadingComponent }
        }
        return placeholder
      }
    },
  }
}

13.3 函数式组件

函数式组件本质就是一个普通函数,该函数的返回值是虚拟 DOM。在 Vue3 中函数式组件的性能和普通组件差不多,但是比较简单。

实现也很简单,就是在 patch 新增对函数的支持,同时在组件挂载时,把 type 作为渲染函数。

function patch(n1, n2, container, anchor) {
  //...
  // 新增了对函数的判断
  if (typeof type === 'object' || typeof type === 'function') {
    // 组件
    if (!n1) {
      // 挂载
      mountComponent(n2, container, anchor)
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

function mountComponent(vnode, container, anchor) {
  const isFunctional = typeof vnode.type === 'function'
  let componentOptions = vnode.type
  if (isFunctional) {
    componentOptions = {
      render: vnode.type,
      props: vnode.type.props,
    }
  }
  // ...
}

使用方法:

function MyFuncComp(props) {
  return {
    type: 'h1',
    children: props.title
  }
}
MyFuncComp.props = {
  title: String
}
const vnode = {
  type: MyFuncComp,
  props: {
    title: '函数式组件'
  }
}
posted @ 2023-02-07 18:35  我不吃饼干呀  阅读(44)  评论(0编辑  收藏  举报