前言

之前在学习 React Hooks 的过程中,看到一篇外网文章,通过 Hooks 来申请数据,并将这段逻辑形象成一个新的 Hooks 给其余组件复用,我也在我的博客里翻译了一下:《在 React Hooks 中如何申请数据?》,感兴趣能够看看。尽管是去年的文章,在浏览之后一下子就把握了 Hooks 的应用形式,而且数据申请是在业务代码中很罕用的逻辑。

Vue 3 曾经公布一段时间了,其组合 API 多少有点 React Hooks 的影子在外面,明天我也打算通过这种形式来学习下组合 API。

我的项目初始化

为了疾速启动一个 Vue 3 我的项目,咱们间接应用当下最热门的工具 Vite 来初始化我的项目。整个过程零打碎敲,行云流水。

npm init vite-app vue3-app
# 关上生成的我的项目文件夹cd vue3-app# 装置依赖npm install# 启动我的项目npm run dev

咱们关上 App.vue 将生成的代码先删掉。

组合 API 的入口

接下来咱们将通过 Hacker News API 来获取一些热门文章,Hacker News API返回的数据结构如下:

{  "hits": [    {      "objectID": "24518295",      "title": "Vue.js 3",      "url": "https://github.com/vuejs/vue-next/releases/tag/v3.0.0",    },    {...},    {...},  ]}

咱们通过 ui > li 将新闻列表展现到界面上,新闻数据从 hits 遍历中获取。

<template>  <ul>    <li      v-for="item of hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template><script>import { reactive } from 'vue'export default {  setup() {    const state = reactive({      hits: []    })    return state  }}</script>

在解说数据申请前,我看先看看 setup() 办法,组合 API 须要通过 setup() 办法来启动,setup() 返回的数据能够在模板内应用,能够简略了解为 Vue 2 外面 data() 办法返回的数据,不同的是,返回的数据须要先通过 reactive() 办法进行包裹,将数据变成响应式。

组合 API 中申请数据

在 Vue 2 中,咱们申请数据时,通常须要将发动申请的代码放到某个生命周期中(createdmounted)。在 setup() 办法内,咱们能够应用 Vue 3 提供的生命周期钩子将申请放到特定生命周期内,对于生命周期钩子办法与之前生命周期的比照如下:

能够看到,基本上就是在之前的办法名前加上了一个 on,且并没有提供 onCreated 的钩子,因为在 setup() 内执行就相当于在 created 阶段执行。上面咱们在 mounted 阶段来申请数据:

import { reactive, onMounted } from 'vue'export default {  setup() {    const state = reactive({      hits: []    })    onMounted(async () => {      const data = await fetch(        'https://hn.algolia.com/api/v1/search?query=vue'      ).then(rsp => rsp.json())      state.hits = data.hits    })    return state  }}

最初成果如下:

监听数据变动

Hacker News 的查问接口有一个 query 参数,后面的案例中,咱们将这个参数固定了,当初咱们通过响应式的数据来定义这个变量。

<template>  <input type="text" v-model="query" />  <ul>    <li      v-for="item of hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template><script>import { reactive, onMounted } from 'vue'export default {  setup() {    const state = reactive({      query: 'vue',      hits: []    })    onMounted((async () => {      const data = await fetch(        `https://hn.algolia.com/api/v1/search?query=${state.query}`      ).then(rsp => rsp.json())      state.hits = data.hits    })    return state  }}</script>

当初咱们在输入框批改,就能触发 state.query 同步更新,然而并不会触发 fetch 从新调用,所以咱们须要通过 watchEffect() 来监听响应数据的变动。

import { reactive, onMounted, watchEffect } from 'vue'export default {  setup() {    const state = reactive({      query: 'vue',      hits: []    })    const fetchData = async (query) => {      const data = await fetch(        `https://hn.algolia.com/api/v1/search?query=${query}`      ).then(rsp => rsp.json())      state.hits = data.hits    }    onMounted(() => {      fetchData(state.query)      watchEffect(() => {        fetchData(state.query)      })    })    return state  }}

因为 watchEffect() 首次调用的时候,其回调就会执行一次,造成初始化时会申请两次接口,所以咱们须要把 onMounted 中的 fetchData 删掉。

onMounted(() => {- fetchData(state.query)  watchEffect(() => {    fetchData(state.query)  })})

watchEffect() 会监听传入函数内所有的响应式数据,一旦其中的某个数据发生变化,函数就会从新执行。如果要勾销监听,能够调用 watchEffect() 的返回值,它的返回值为一个函数。上面举个例子:

const stop = watchEffect(() => {  if (state.query === 'vue3') {    // 当 query 为 vue3 时,进行监听    stop()  }  fetchData(state.query)})

当咱们在输入框输出 "vue3" 后,就不会再发动申请了。

返回事件办法

当初有个问题就是 input 内的值每次批改都会触发一次申请,咱们能够减少一个按钮,点击按钮后再触发 state.query 的更新。

<template>  <input type="text" v-model="input" />  <button @click="setQuery">搜寻</button>  <ul>    <li      v-for="item of hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template><script>import { reactive, onMounted, watchEffect } from 'vue'export default {  setup() {    const state = reactive({      input: 'vue',      query: 'vue',      hits: []    })    const fetchData = async (query) => {      const data = await fetch(        `https://hn.algolia.com/api/v1/search?query=${query}`      ).then(rsp => rsp.json())      state.hits = data.hits    }    onMounted(() => {      watchEffect(() => {        fetchData(state.query)      })    })        const setQuery = () => {      state.query = state.input    }    return { setQuery, state }  }}</script>

能够留神到 button 绑定的 click 事件的办法,也是通过 setup() 办法返回的,咱们能够将 setup() 办法返回值了解为 Vue2 中 data() 办法和 methods 对象的合并。

原先的返回值 state 变成了当初返回值的一个属性,所以咱们在模板层取数据的时候,须要进行一些批改,在后面加上 state.

<template>  <input type="text" v-model="state.input" />  <button @click="setQuery">搜寻</button>  <ul>    <li      v-for="item of state.hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template>

返回数据批改

作为强迫症患者,在模板层通过 state.xxx 的形式获取数据切实是好受,那咱们是不是能够通过对象解构的形式将 state 的数据返回呢?

<template>  <input type="text" v-model="input" />  <button class="search-btn" @click="setQuery">搜寻</button>  <ul class="results">    <li      v-for="item of hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template><script>import { reactive, onMounted, watchEffect } from 'vue'export default {  setup(props, ctx) {    const state = reactive({      input: 'vue',      query: 'vue',      hits: []    })    // 省略局部代码...    return {      ...state,      setQuery,    }  }}</script>

答案是『不能够』。批改代码后,能够看到页面尽管发动了申请,然而页面并没有展现数据。

state 在解构后,数据就变成了静态数据,不能再被跟踪,返回值相似于:

export default {  setup(props, ctx) {    // 省略局部代码...    return {      input: 'vue',      query: 'vue',      hits: [],      setQuery,    }  }}

为了跟踪根底类型的数据(即非对象数据),Vue3 也提出了解决方案:ref()

import { ref } from 'vue'const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1

下面为 Vue 3 的官网案例,ref() 办法返回的是一个对象,无论是批改还是获取,都须要取返回对象的 value 属性。

咱们将 state 从响应对象改为一个一般对象,而后所有属性都应用 ref 包裹,这样批改后,后续的解构才做能力失效。这样的弊病就是,state 的每个属性在批改时,都必须取其 value 属性。然而在模板中不须要追加 .value,Vue 3 外部有对其进行解决。

import { ref, onMounted, watchEffect } from 'vue'export default {  setup() {    const state = {      input: ref('vue'),      query: ref('vue'),      hits: ref([])    }    const fetchData = async (query) => {      const data = await fetch(        `https://hn.algolia.com/api/v1/search?query=${query}`      ).then(rsp => rsp.json())      state.hits.value = data.hits    }    onMounted(() => {      watchEffect(() => {        fetchData(state.query.value)      })    })    const setQuery = () => {      state.query.value = state.input.value    }    return {      ...state,      setQuery,    }  }}

有没有方法放弃 state 为响应对象,同时又反对其对象解构的呢?当然是有的,Vue 3 也提供了解决方案:toRefs()toRefs() 办法能够将一个响应对象变为一般对象,并且给每个属性加上 ref()

import { toRefs, reactive, onMounted, watchEffect } from 'vue'export default {  setup() {    const state = reactive({      input: 'vue',      query: 'vue',      hits: []    })    const fetchData = async (query) => {      const data = await fetch(        `https://hn.algolia.com/api/v1/search?query=${query}`      ).then(rsp => rsp.json())      state.hits = data.hits    }    onMounted(() => {      watchEffect(() => {        fetchData(state.query)      })    })    const setQuery = () => {      state.query = state.input    }    return {      ...toRefs(state),      setQuery,    }  }}

Loading 与 Error 状态

通常,咱们发动申请的时候,须要为申请增加 Loading 和 Error 状态,咱们只须要在 state 中增加两个变量来管制这两种状态即可。

export default {  setup() {    const state = reactive({      input: 'vue',      query: 'vue',      hits: [],      error: false,      loading: false,    })    const fetchData = async (query) => {      state.error = false      state.loading = true      try {        const data = await fetch(          `https://hn.algolia.com/api/v1/search?query=${query}`        ).then(rsp => rsp.json())        state.hits = data.hits      } catch {        state.error = true      }      state.loading = false    }    onMounted(() => {      watchEffect(() => {        fetchData(state.query)      })    })    const setQuery = () => {      state.query = state.input    }    return {      ...toRefs(state),      setQuery,    }  }}

同时在模板应用这两个变量:

<template>  <input type="text" v-model="input" />  <button @click="setQuery">搜寻</button>  <div v-if="loading">Loading ...</div>  <div v-else-if="error">Something went wrong ...</div>  <ul v-else>    <li      v-for="item of hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template>

展现 Loading、Error 状态:

将数据申请逻辑形象

用过 umi 的同学必定晓得 umi 提供了一个叫做 useRequest 的 Hooks,用于申请数据十分的不便,那么咱们通过 Vue 的组合 API 也能够形象出一个相似于 useRequest 的公共办法。

接下来咱们新建一个文件 useRequest.js

import {  toRefs,  reactive,} from 'vue'export default (options) => {  const { url } = options  const state = reactive({    data: {},    error: false,    loading: false,  })  const run = async () => {    state.error = false    state.loading = true    try {      const result = await fetch(url).then(res => res.json())      state.data = result    } catch(e) {      state.error = true    }    state.loading = false  }  return {    run,    ...toRefs(state)  }}

而后在 App.vue 中引入:

<template>  <input type="text" v-model="query" />  <button @click="search">搜寻</button>  <div v-if="loading">Loading ...</div>  <div v-else-if="error">Something went wrong ...</div>  <ul v-else>    <li      v-for="item of data.hits"      :key="item.objectID"    >      <a :href="item.url">{{item.title}}</a>    </li>  </ul></template><script>import { ref, onMounted } from 'vue'import useRequest from './useRequest'export default {  setup() {    const query = ref('vue')    const { data, loading, error, run } = useRequest({      url: 'https://hn.algolia.com/api/v1/search'    })    onMounted(() => {      run()    })    return {      data,      query,      error,      loading,      search: run,    }  }}</script>

以后的 useRequest 还有两个缺点:

  1. 传入的 url 是固定的,query 批改后,不能及时的反馈到 url 上;
  2. 不能主动申请,须要手动调用一下 run 办法;
import {  isRef,  toRefs,  reactive,  onMounted,} from 'vue'export default (options) => {  const { url, manual = false, params = {} } = options  const state = reactive({    data: {},    error: false,    loading: false,  })  const run = async () => {    // 拼接查问参数    let query = ''    Object.keys(params).forEach(key => {      const val = params[key]      // 如果去 ref 对象,须要取 .value 属性      const value = isRef(val) ? val.value : val      query += `${key}=${value}&`    })    state.error = false    state.loading = true    try {      const result = await fetch(`${url}?${query}`)          .then(res => res.json())      state.data = result    } catch(e) {      state.error = true    }    state.loading = false  }  onMounted(() => {    // 第一次是否须要手动调用    !manual && run()  })  return {    run,    ...toRefs(state)  }}

通过批改后,咱们的逻辑就变得异样简略了。

import useRequest from './useRequest'export default {  setup() {    const query = ref('vue')    const { data, loading, error, run } = useRequest(      {        url: 'https://hn.algolia.com/api/v1/search',        params: {          query        }      }    )    return {      data,      query,      error,      loading,      search: run,    }  }}

当然,这个 useRequest 还有很多能够欠缺的中央,例如:不反对 http 办法批改、不反对节流防抖、不反对超时工夫等等。最初,心愿大家看完文章后能有所播种。