vuejs设计与实现-异步组件与函数式组件

240 阅读2分钟

异步组件要解决的问题

异步组件完全可以由用户自行实现:

// 异步渲染
import loader = () => import('App.vue')
loader().then(App => {
    createApp(App).mount('#app')
})


// 异步渲染部分页面
<script>
import CompA from 'CompA'

const asyncComp = shallowRef(null)
import('CompB.vue').then(CompB => asyncComp.value = CompB)
</script>
<template>
    <CompA />
    <component :is="asyncComp" />
</template>

但为了更好地解决问题, 需要在框架层面提供更好的支持.

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

异步组件的实现原理

封装defineAsyncComponent函数, 用来定义异步组件. 本质上是一个高阶组件, 返回值是一个包装组件. 以下是要支持的几项:

  • 超时与Error组件
  • 延迟与Loading组件
  • 重试机制
// 设计接口, 然后实现
const AsyncComp = defineAsyncComponent({
    loader: () => import('CompA.vue'),
    // 超时时长
    timeout: 2000,
    // 出错时加载的组件
    errorComponent: ErrorComp,
    // 延迟展示Loading组件的时长 
    delay: 200,
    // 可能会存在加载速度很快, 没有必要再展示Loading组件的情况
    loadingComponent: {
        setup(){
            return { type: 'h2', children: 'Loading ...' }
        }
    }
})

// 实现异步组件方法
function defineAsyncComponent(options){
    if(typeof options === 'function') {
        options = { loader: options }            
    } 
    const { loader, errorComponent, timeout,  onError, delay } = options
    let InnerComp = null
    // 记录重试次数
    let retries = 0
    // 重试机制  
    // 将 retry 和 fail 作为onError回调的参数, 让用户控制重试或抛出错误
    function load(){
        return loader().catch(err => {
            if(onError) {
                return new Promise((resolve, reject) => {
                    const retry = () => {
                        resolve(load())
                        retries++
                    }
                    const fail = () => reject(err)
                    onError(retry, fail, retries)
                })
            } else {
                throw err
            }
        })
    }
    
    return {
        name: 'AsyncComponentWrapper',
        setup(){
            const loaded = ref(false), error = shallowRef(false), loading = ref(false)
            
            let loadingTimer = null
            
            // 延迟控制  到达指定时间才显示Loading组件
            if(delay) {
                loadingTimer = setTimeout(() => {
                    loading.value = true
                }, delay)
            } else {
                loading.value = true
            }
            // 调用 load 加载组件
            load().then(c => {
                InnerComp = c
                loaded.value = true
            }).catch(err => {
                error.value = err
            }).finally(() => {
                loading.value = false
                clearTimout(loadingTimer)
            })
            
            // 超时控制
            let timer = null
            if(timeout){
                timer = setTimeout(() => {
                    const err = new Error('Async Component timed out after ${timeout}ms.')
                    error.value = err
                }, timeout)
            }
            onUnmounted(() => clearTimout(timer))
            
            const placeholder = { type: Text, children: '' }
            
            return () => {
                if(loaded.value) {
                    return { type: InnerComp }
                } else if(error.value && errorComponent){
                    return { type: errorComponent, props: error.value }
                } else if(loading.value && loadingComponent){
                    return { type: loadingComponent }
                } 
                return placeholder
            }
        }
    }
}

函数式组件

函数式组件的本质是一个函数, 其返回值是虚拟dom. 且没有自身状态, 但可以接收由外部传入的props. patch函数内部通过vnode.type的类型判断组件的类型:

  • 如果vnode.type是一个对象, 则它是一个有状态组件, 并且vnode.type是组件选项对象.
  • 如果vnode.type是一个函数, 则它是一个函数式组件.
// 函数式组件
function FuncComp(props){
    return { type: 'h1', children: props.title }
}

FuncComp.props = {
    title: String
}

// 增加对函数式组件的支持
function mountComponent(){
    const isFunctional = typeof vnode.type === 'function'
    
    let componentOptions = vnode.type
    // 对函数式组件的处理
    if(isFunctional) {
        componentOptions = {
            render: vnode.type,
            props: vnode.type.props
        }
    }
    // ...
}

isFunctional变量实现选择性地执行初始化逻辑, 因为函数式组件无须初始化data以及生命周期钩子.

总结

  • 异步组件在页面性能、拆包以及服务端下发组件等场景尤为重要.