Skip to content

异步组件与函数式组件

异步组件

js
// 同步渲染
import App from "App.vue"
createApp(App).mount('#app')

// 异步渲染 (动态导入语句import()来加载组件,会返回一个Promise实例,组件加载成功后,会调用createApp函数完成挂载)
const loader = () => import('App.vue')
loader().then(App => {
  createApp(App).mount('#app')
})

异步渲染部分页面

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

export default {
  components: {CompA},
  setup() {
    const asyncComp = shallowRef(null)
    
    // 异步加载CompB组件
    import('CompB.vue').then(CompB => asyncComp.value = CompB)

    return {
      asyncComp
    }
  }
}
</script>

为异步组件提供更好的封装与支持,需要如下的能力

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

异步组件的实现原理

封装defineAsyncComponent函数

vue
<template>
  <AsyncComp />
</template>

<script>
  export default {
    components: {
      AsyncComp: defineAsyncComponent(() => import('CompA'))
    }
  }
</script>

使用defineAsyncComponet来定义异步组件,并直接用于components组件选项来注册它。

js
 // defineAsyncComponet 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
  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: '' }
        }
      }
    }
  }
  • defineAsyncComponent函数本质上是一个高阶组件,它的返回值是一个包装组件
  • 包装组件会根据加载器的状态来决定渲染什么内容。如果加载器成功地加载了组件,则渲染被加载的组件,否则会渲染一个占位内容
  • 通常占位内容是一个注释节点。组件没有被加载成功时,页面中会渲染一个注释节点来占位。

超时与Error组件

js
// defineAsyncComponet 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
  function defineAsyncComponent(options) {
    // 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)

        // 执行加载器函数,返回一个Promise实例
        // 加载成功后,将加载成功的组件赋值给InnerComp,并将loaded标记为true,代表加载成功
        loader().then(c => {
          InnerComp = c
          loaded.value = true
        }).catch((err) => error.value = err)
        let timer = null

        if (options.timeout) {
          timer = setTimeout(() => {
            const err = new Error('Async componet 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) {
            return { type: options.errorComponent, props: { error: error.value } }
          }
          return placeholder
        }
      }
    }
  }

延迟与Loading组件

用户接口设计

js
defineAsyncComponent({
  loader: () => new Promise(r => {}),
  delay: 200,
  tineout: 2000,
  errorComponent: MyErrorComp,
  loadingComponent: {
    setup() {
      return () => {
        return {
          type: 'div',
          children: 'loading'
        }
      }
    }
  }
})
js
 function defineAsyncComponent(options) {
    // 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)

        // 是否正在加载 默认为false
        const loading = ref(false)
        const loadingTimer = null
        
        if(options.delay) {
          loadingTimer = setTimeout(() => {
            loading.value = true
          }, options.delay)
        } else {
          loading.value = true
        }

        // 执行加载器函数,返回一个Promise实例
        // 加载成功后,将加载成功的组件赋值给InnerComp,并将loaded标记为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 componet 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) {
            return { type: options.errorComponent, props: { error: error.value } }
          } else if (loading.value && options.loadingComponent) {
            // 如果异步组件正在加载,并且用户指定了Loading组件,则渲染Loading组件
            return { type: options.loadingComponent}
          }
          return placeholder
        }
      }
    }
  }

卸载Loading组件并渲染异步加载的组件,兼容Loading组件的卸载

js
  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)
    }
  }

重试机制

重试指的是当加载出错时,有能力重新发起加载组件的请求。

模拟

js
function fetch() {
  return new Promise ((resolve, reject) => {
    setTimout(() => {
      resolve('err')
    }, 2000)
  })
}

function load(onError) {
  const p = fetch()
  return p.catch(err => {
    return new Promise((resolve, reject) => {
      const retry = () => resolve(load(onError))
      const fail = () => reject(err)
      onError(retry, fail)
    })
  })
}

load((retry) => {
  retry()
}).then(res => {
  console.log(res)
})
js
function defineAsyncComponent(options) {
    // options可以是配置项,也可以是加载器
    if (typeof options === 'function') {
      options = {
        loader: options
      }
    }
    const { loader } = options
    let InnerComp = null

    // 记录重试次数
    let retries = 0
    // 封装load函数用来加载异步组件
    function load() { 
      return loader() 
        // 捕获加载器的错误
        .catch((err) => { 
          if (options.onError) { 
            return new Promise((resolve, reject) => { 
              const retry = () => { 
                resolve(load()) 
                retries++
              } 
              const fail = () => reject(err) 

              options.onError(retry, fail, retries) 
            }) 
          } else { 
            throw error 
          } 
        }) 
    } 

    return {
      name: 'AsyncComponentWrapper',
      setup() {
        // 异步组件是否加载成功
        const loaded = ref(false)
        // 定义error,当错误发生时,用来存储错误对象
        const error = shallowRef(null)

        // 是否正在加载 默认为false
        const loading = ref(false)
        const loadingTimer = null

        if (options.delay) {
          loadingTimer = setTimeout(() => {
            loading.value = true
          }, options.delay)
        } else {
          loading.value = true
        }

        // 执行加载器函数,返回一个Promise实例
        // 加载成功后,将加载成功的组件赋值给InnerComp,并将loaded标记为true,代表加载成功
        load().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 componet 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) {
            return { type: options.errorComponent, props: { error: error.value } }
          } else if (loading.value && options.loadingComponent) {
            // 如果异步组件正在加载,并且用户指定了Loading组件,则渲染Loading组件
            return { type: options.loadingComponent }
          }
          return placeholder
        }
      }
    }
  }

函数式组件

函数式组件本质上就是一个普通函数,该函数的返回值是虚拟DOM。

Vuejs3中使用函数式组件,主要是因为它的简单性,而不是因为它的性能好。这是因为在Vuejs3中,即使是有状态组件,其初始化性能消耗非常小。

在用户接口层面,一个函数式组件就是一个返回虚拟DOM的函数

js
function MyFuncComp(props) {
  return { type: 'h1', children: props.title}
}

函数式组件没有自身状态,但他仍然可以接收由外部传入的props。为了给函数式组件定义props,需要在组件函数上添加静态的props属性。

js
function MyFuncComp(props) {
  return { type: 'h1', children: props.title}
}
MyFuncComp.props = {
  title: String
}

在有状态的组件基础上,实现函数式组件将变得非常简单,因为挂载组件的逻辑可以复用mountComponent函数。

js
function patch(n1, n2, container, anchor) {
    // 如果n1存在,则对比n1和n2的类型
    if (n1 && n1.type !== n2.type) {
      // 如果新旧vnode的类型不同,则直接将旧的vnode卸载
      unmount(n1)
      n1 = null
    }
    const { type } = n2
    if (typeof type === 'string') {
      // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
      // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
      // 这时只需要挂载
      if (!n1) {
        mountElement(n2, container, anchor)
      } else {
        console.log(n1, n2)
        // n1 存在,意味着打补丁,
        patchElement(n1, n2)
      }
    } else if (type === Text) {
      // 如果新的vnode的类型是Text,则说明该vnode描述的是文本节点

      // 如果没有旧节点,则进行挂载
      if (!n1) {
        // 使用createTextNode 创建文本节点
        const el = n2.el = createText(n2.children)
        // 将文本节点插入到容器中
        insert(el, container)
      } else {
        // 如果旧vnode存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = n2.el = n1.el
        if (n2.children !== n1.children) {
          setText(el, n2.children)
        }
      }
    } else if (type === Comment) {
      if (!n1) {
        const el = n2.el = createComment(n2.children)
        insert(el, container)
      } else {
        const el = n2.el = n1.el
        if (n2.children !== n1.children) {
          setComment(el, n2.children)
        }
      }
    } else if (type === Fragment) {
      if (!n1) {
        // 如果旧vnode不存在,则只需要将Fragment的children逐个挂载即可
        n2.children.forEach(c => patch(null, c, container))
      } else {
        // 如果旧vnode存在,则只需要更新Fragment的children即可
        patchChildren(n1, n2, container)
      }
    }
    else if (typeof type === 'object') { 
    else if (typeof type === 'object' || typeof type === 'function') { 
      // 组件
      if (!n1) {
        mountComponent(n2, container, anchor)
      } else {
        patchComponent(n1, n2, anchor)
      }
    } else if (type === 'xxx') {
      // 其他类型的vnode
    }

  }

  function mountComponent(vnode, container, anchor) {

    const isFunctional = typeof vnode.type === 'function'

    const componentOptions = vnode.type

    if(isFunctional) { 
      componentOptions = { 
        render: vnode.type, 
        props: vnode.type.props 
      } 
    } 

    const { render, data, beforCreate, create, props: propOptions, beforeMount, mounted, beforeUpdate, updated } = componentOptions

    beforCreate && beforCreate()

    // 解析出最终的props数据 attrs数据
    const [props, attrs] = resolveProps(propOptions, vnode.props)

    const state = data ? reactive(data()) : null

    const instance = {
      state,
      //将解析出的props 数据包装为shalloReactive并定义到组件实例上
      props: shallowReactive(props),
      isMounted: false,
      subTree: null,
      // 将插槽添加到组件实例上
      slots,
      // 在组件实例中添加mounted数组,用来储存通过onMounted函数注册的生命周期钩子函数
      mounted: []
    }
    // 定义emit函数,它接收两个参数
    // event:事件名称
    // payload: 传递给事件处理函数的参数

    function emit(event, ...payload) {
      // 根据约定对事件名称进行处理
      const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
      // 根据处理后的事件名称去props中寻找对应的事件处理函数
      const handler = instance.props[eventName]
      if (handler) {
        // 调用事件处理函数并传递参数
        handler(...payload)
      } else {
        console.log('事件不存在')
      }
    }

    // 直接使用编译好的vnode.children对象作为slots对象即可
    const slots = vnode.children || {}


    // setupContext
    const setupContext = { attrs, emit, slots }

    // 在调用setup函数之前,设置当前组件实例
    setCurrentInstance(instance)

    // 调用setup函数,将只读版本的props作为第一个参数传递,避免用户意外地修改props值
    // 将setupContext作为第二个参数传递
    const setupResult = setup(shollowReadonly(instance.props), setupContext)

    // 在setup函数执行完毕之后,重置当前组件实例
    setCurrentInstance(null)

    // setupState 用来储存由setup放回的数据
    let setupState = null
    // 如果setup函数的返回值是函数,则将其作为渲染函数
    if (typeof setupResult === "function") {
      if (render) console.log('setup 函数返回渲染函数,render 选项将被忽略')
      // 将setupResult 作为渲染函数
      render = setupResult
    } else {
      // 如果setup 的返回值不是函数,则作为数据状态赋值给setupState
      setupState = setupContext
    }

    vnode.component = instance

    /** 由于props数据与组件自身的状态数据都需要暴露到渲染函数中,
     * 并使得渲染函数能够通过this访问它们,因此需要分装一个渲染上下午对象
     * */
    const renderContext = new Proxy(instance, {
      get(t, k, r) {
        const { state, props } = t
        // 当k当值为$slots时,直接返回组件实例上的slots
        if (k === '$slots') return slots
        if (state && k in state) {
          return state[k]
        } else if (k in props) {
          return props[k]
        } else if (setupState && k in setupState) {
          // 渲染上下文需要增加对setupState的支持
          return setupState[k]
        } else {
          console.log('不存在')
        }
      },
      set(t, k, v, r) {
        const { state, props } = t
        if (state && k in state) {
          state[k] = v
        } else if (k in props) {
          props[k] = v
        } else if (setupState && k in setupState) {
          setupState[k] = v
        } else {
          console.log('不存在')
        }
      }
    })


    create && create.call(renderContext)
    effect(() => {
      const subTree = render.call(renderContext, renderContext)
      if (!instance.isMounted) {
        beforeMount && beforeMount.call(renderContext)
        patch(null, subTree, container, anchor)
        instance.isMounted = true
        // 遍历instance.mounted数组并逐个执行即可
        instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))
      } else {
        beforeUpdate && beforeUpdate.call(renderContext)
        patch(instance.subTree, subTree, container, anchor)

        // 在这里调用updated钩子
        updated && updated.call(renderContext)
      }
      instance.subTree = subTree
    }, { scheduler: queueJob })
  }

如果是函数式组件,mountComponent函数直接将组件函数作为组件选项对象的render选项,并将组件函数的静态props属性作为组件的props选项。 函数式组件它无须初始化data以及生命周期钩子。