第十三章-异步组件与函数式组件

82 阅读2分钟

一、异步组件

1、为什么要封装

  • 节省用户的工作量,

  • 能够提供拓展功能

封装前

  <template>
    <component :is="asyncComponent"/>
  </template>
  <script>
    import { shallowRef } from "vue"
    export  default {
       setup(props, setupContext) {
         const asyncComponent = ref(null);
         import("CompB.vue").then(CompB => asyncComponent.value = CompB)
         return {
           asyncComponent
         }
       }
    }
  </script>

封装后

<template>
  <AsyncComp/>
</template>
<script>
  export  default {
     components: {
       AsyncComp: defineAsyncComponent(() => import("CompB"))
     }
  }
</script>

注意:

  • defineAsynComponent函数本质是一个高阶函数, 返回值是一个包装组件
  • 包装组件会根据加载器的状态决定渲染什么内容, 如果加载器成功加载了组件, 则渲染组件, 否则渲染一个占位符
  • 占位内容是一个注释节点。组件没有加载成功时, 页面会渲染一个注释节点来占位, 这里使用了一个空文本节点来占位

2、封装的功能有哪些

  • 超时
  • Error组件
  • loading组件与延迟
  • 重试机制(思路值得参考)
 function  load(onError) {
  let p = fetch();
  return p.catch(err => {
    return new Promise((resolve, reject) => {
      const retry = () => resolve(load(onError))
      const fail = () => reject(err);
      onError(retry, fail);
    })
  })
}
Promise.resolve(1).catch(err => console.log("err", err)).then(res => console.log("res", res)) // 1
// Promise中resolve或者reject为空, 则会传递给下个then方法

报错之后通过onError判断是否重连还是结束

3、实现代码

3.1、defineAsyncComponent

function load(options, times = 0) {
  let p = options.loader();
  return p.catch(err => {
    if(options.onError) {
      times = times + 1;
      return new Promise((resolve, reject) => {
        const retry = () => resolve(load(options, times))
        const fail = () => reject(err);
        options.onError(retry, fail, times);
      })
    } else {
      throw err;
    }
  })
}
function defineAsyncComponent(options) {
  if(typeof options === "function") {
    options = {
      loader: options
    }
  }
  let InnerComp = null;
  return {
    name: "defineAsyncComponent",
    setup() {
      let loaded = ref(false);
      let loading = ref(false);
      let error = shallowRef("");
      let loadTimer = null;
      let timeoutTimer = null;
      
      if(options.delay) {
        loadTimer = setTimeout(() => {
          loading.value = true;
        }, options.delay)
      } else {
        loading.value = true;
      }
      
      load().then(c => {
        InnerComp = c;
        loaded.value = true;
      }).catch((err) => err.value = err
      ).finally(() => {
        loading.value = false;
        clearTimeout(loadTimer) //不管成功与否都需要清除延迟定时器
        clearInterval(timeoutTimer); //不管成功与否都需要清除超时定时器
      })
      
      if(options.timeout) {
        timeoutTimer = setTimeout(() => {
          error.value = "timeout"
        }, options.timeout)
      }
    
      const placeholder = {type: Text, children: ""}
      return () => {
        if(loaded) {
          return {type: InnerComp};
        } else if(error.value && options.errorComponent) {
          return {type: options.errorComponent , props: {error: errer.value}}
        } else if(loading.value && options.loadongComponent) {
          return {type: options.loadongComponent}
        } else {
          return placeholder;
        }
      }
    }
  }
}

3.2异步组件的加载, 直接沿用就行, return的对象本就是要求格式

const MyComponent = {
  name: "MyComponent",
  setup() {
    return () => {}, // 如果setup返回的是函数则会覆盖render
  },
  .....
}
const vnode = {
  type: MyComponent
}

3.3异步组件的卸载, 当异步组件加载成功后, 会卸载loading组件并渲染异步加载的组件, 需要更改unmount函数

function unmount(vnode) {
  if(typeof vnode.type === Fragment) {
    vnode.type.children.forEach(c => unmount(c))
    return 
  } else if( typeof vnode.type === "object") {
    unmount(vnode.component.subtree);
    return;
  }
  let parent = vnode.el.parentNode;
  if(parent) {
    parent.removeChildren(vnode.el);
  }
}

二、函数式组件

1、函数式组件

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

2、对比vue2

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

3、实现代码

函数式组件

function MyFuncComp(props) {
 reutrn {type: "h1", children: props.title}
}
MyFuncComp.props = {
  title: string,
}

更改patch支持

function patch(n1, n2, container, anchor) {
    if(n1 && n1.type !== n2.type) {
      unmount(n1);
      n1 = null;
    }
    let { type } = n2;
    if(typeof type === "string") { 
    
    } else if(typeof  type === "object" || typeof type === "function"){  
      // type 是对象  有状态组件
      // type 是函数  函数式组件
      if(!n1) {
        mountComponent(n2, container, anchor)
      } else {
        patchComponent()
      }
    } else if(type === Text){
  
    } else if(type === Fragment) {
       
    } else {
      // 省略了其他类型的vnode
    }
  }

更改mountComponent支持

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