【vue3】读vue-promised源码,理解流程控制组件

850 阅读3分钟

场景

一个简单的场景:前端发起请求获取users,得到数据后展示在页面。虽然简单,还是有一下几个点需要我们注意:

  1. 接口请求的这过程,页面状态为loading
  2. 数据请求成功,由loading =》 展示数据
  3. 接口请求失败,loading =》 展示报错信息

然后你就会写出包含v-if、v-else-if、v-else,这样的代码不但不那么优雅,而且每一个页面都要来重复写这一套逻辑。

<template>
  <div>
    <div v-if="isLoading">loading----</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else>
      <pre>{{users}}</pre>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'

export default defineComponent({
  setup() {
    // 数据
    let users = ref<unknown | null>(null)
    // 是否loading
    let isLoading = ref(true)
    // 错误信息
    let error = ref(null)

    // 模拟getUser接口, 2s后返回数据
    const getUser = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve([
            {name: 'jgmiu1', id: 27},
            {name: 'jgmiu2', id: 28},
            {name: 'jgmiu3', id: 29},
          ])
        }, 2000)
      })
    }

    // 调用接口,根据状态设置值
    const fetchUsers = () => {
     getUser().then(data => {
       // 
       isLoading.value = false
       error.value = null
       users.value = data
     }).catch(err => {
        error.value = err
        isLoading.value = false
        users.value = null
     }) 
    }
    onMounted(() =>{
      fetchUsers()
    })
    return {
      users,
      isLoading,
      error      
    }
  },
})
</script>

对于以上的这种重复性的动作,肯定会有大佬去抽离出来,形成一个流程性控制得组件。vue-promised就是一个专门进行异步流程控制得组件,他讲ui与数据进行分离,使用slot-scope在适当的时候回抛数据,出现异常的时候也回抛异常。

简单版vue-promised

参考vue-promised源码,使用vue3的实现一个异步流程控制组件。这个组件旨在流程控住,与视图无关,所以我们的组件不是一个template组件。视图是通过插槽传入的,该组件只关心什么状态下显示什么视图。

前置知识

ts/tsx文件中使用defineComponent创建组件

vue3出来之后,我像react一样在ts/tsx文件编写组件。

import {defineComponent} from "vue";

export const Test = defineComponent({
    setup() {
        return () => <>
            <div>JSX test</div>
        </>
    }
})
// 这里有个坑 使用export会不生效
export default Test

根据setup(props, {slots})中的slots决定渲染那个插槽

  1. 新建一个Test组件
import {defineComponent} from "vue";

export const Test = defineComponent({
    setup(props, {slots}) {
      console.log(slots)
      return () => <h1></h1>
    }
})
export default Test
  1. 调用Test组件
<Test>
    <template v-slot:pending>
      <h1>loading...</h1>
    </template>
    <template v-slot="data">
      <pre>{{data}}</pre>
    </template>
    <template v-slot="error">
      <pre>{{error}}</pre>
    </template>
</Test>

插槽:pending、error和default。我们只需要请求进行中展示pending视图,在请求成功之后展示default视图并利用slot-scope外抛数据展示到页面

  1. 在Test中输出slots如下,可以发现slots是一个对象,每一个插槽是一个函数

image.png

4.根据slots展示相应视图。

import {defineComponent} from "vue";

export const Test = defineComponent({
    setup(props, {slots}) {
      console.log('xxx',slots)
      return () => slots['default']!({name: 'xxx'})
    }
})
export default Test

image.png

具体实现

根据用户传入promise的状态,显示相应视图

Promised

// rejected default pending

import { 
  defineComponent,
  PropType,
  toRefs,
  reactive
} from 'vue-demi'

import { usePromise, UsePromiseResult } from './usePromise'

export const MyPromised = defineComponent({
  name: 'MyPromised',
  props: {
    // 接受一个用户传入的promise  
    promise: {} as PropType<Promise<unknown> | null | undefined>,
  },
  setup(props, {slots}) {
    // 将props变成ref对象
    const propsAsRefs = toRefs(props)
    // 得到promise的状态
    const promiseState = reactive<UsePromiseResult>(
      usePromise(propsAsRefs.promise)
    )
    return () => {
      // 根据promiseState展示相应的视图
      const [slotName, slotData] = promiseState.isRejected
      ? ['rejected', promiseState.error]
      : !promiseState.isPending 
      ? ['default', promiseState.data]
      : ['pending', promiseState.data]
      return slots[slotName]!(slotData)
    }
  },
})
  1. 接受传入的promised
  2. 调用usePromise得到promisedState
  3. 根据promisedState展示相应视图

usePromise

实时获取promised的状态


import { Ref, ref, computed, watch, unref, ComputedRef } from 'vue-demi'

type Refable<T> = Ref<T> | T

export function usePromise<T = unknown> (
  promise: Refable<Promise<T> | null | undefined>
) {
  // 是否有错
  const isRejected = ref(false)
  // 是否成功
  const isResolved = ref(false)
  // 是否处于调用(等待)状态
  const isPending = computed(() => !isRejected.value && !isResolved.value)
  // 异常错误
  const error = ref<Error | undefined | null>()
  // promise被解决的data
  const data = ref<T | null | undefined>()
  // 使用watch监听promise,实时向上发送状态
  watch(
    () => unref(promise),
    (newPromise) => {
      isRejected.value = false
      isResolved.value = false
      error.value = null
      newPromise?.then(newData => {
        data.value = newData
        isResolved.value = true
      }, err => {
        error.value = err
        isRejected.value = true
      })
    },
    {
      immediate: true
    }
  )
  return {
    isRejected, isResolved, isPending, error, data
  }
}

// promiseState接口定义
export interface UsePromiseResult<T = unknown> {
  isPending: ComputedRef<boolean>
  isResolved: Ref<boolean>
  isRejected: Ref<boolean>
  error: Ref<Error | undefined | null>
  data: Ref<T | undefined | null>
}

感想

都2022年了,我还在思考怎么写好一个组件。自身实力远对不起我的工作时间,2022了希望不在沉沦业务,不在错的公司浪费时间。