vue3的Composition API详解,包含hook的思想

vue3的Composition API详解,包含hook的思想

  • 一、Options API的弊端
  • 二、认识Composition API
  • 三、setup
    • 1、setup函数的参数
    • 2、setup函数的返回值
    • 3、setup不可以使用this
    • 4、Reactive API
    • 5、Ref API
      • 1、基本使用
      • 2、Ref自动解包
      • 3、ref其他的API
    • 6、readonly
      • 1、认识readonly
      • 2、readonly的使用
      • 3、readonly的应用
    • 7、toRefs
    • 8、toRef
    • 9、customRef
    • 10、computed
    • 11、侦听数据的变化
      • 1、watchEffect
      • 2、Watch的使用
    • 12、生命周期钩子
    • 13、Provide函数与Inject函数
    • 14、vue3的hooks思想

一、Options API的弊端

Vue2中,我们编写组件的方式是Options API

  • Options API的一大特点就是在对应的属性中编写对应的功能模块
  • 比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子

但是这种代码有一个很大的弊端

  • 当我们实现某一个功能时,这个功能对应的代码逻辑被拆分到各个属性中
  • 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
  • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解

如果我们能将同一个逻辑关注点相关的代码收集在一起会更好

  • 这就是Composition API想要做的事情,以及可以帮助我们完成的事情
  • 也有人把Vue CompositionAPI简称为VCA

二、认识Composition API

那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?

  • 为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方
  • 在Vue组件中,这个位置就是 setup 函数

setup其实就是组件的另外一个选项

  • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项
  • 比如methods、computed、watch、data、生命周期等等

三、setup

1、setup函数的参数

我们先来研究一个setup函数的参数,它主要有两个参数

  • 第一个参数:props
  • 第二个参数:context

props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取

  • 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义
  • 并且在template中依然是可以正常去使用props中的属性,比如message
  • 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取(后面我会讲到为什么)
  • 因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性

  • attrs:所有的非prop的attribute
  • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到)
  • emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件)

下面通过一个例子来说明怎样使用这两个参数
假设我们现在有一个home组件,里面接收一个props
vue3的Composition API详解,包含hook的思想_第1张图片
假设app.vue为父组件,传一个message的prop属性,和id、class两个非prop属性
vue3的Composition API详解,包含hook的思想_第2张图片
vue3的Composition API详解,包含hook的思想_第3张图片

2、setup函数的返回值

setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?

  • setup的返回值可以在模板template中被使用
  • 也就是说我们可以通过setup的返回值来替代data选项

还是上面的那个例子
vue3的Composition API详解,包含hook的思想_第4张图片
甚至是我们可以返回一个执行函数代替在methods中定义的方法
vue3的Composition API详解,包含hook的思想_第5张图片
但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?

  • 答案是不可以
  • 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作

上面的示例,通过控制台可以发现,counter的值确实发生了改变,但在页面上并不会看到跟着一起改变

3、setup不可以使用this

vue3的Composition API详解,包含hook的思想_第6张图片

4、Reactive API

如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数

还是上面的例子,按照下面这样写就可以响应式了,counter会在页面随着点击发生改变
vue3的Composition API详解,包含hook的思想_第7张图片
那么这是什么原因呢?为什么就可以变成响应式的呢?

  • 这是因为当我们使用reactive函数处理我们的数据之后数据再次被使用时就会进行依赖收集
  • 数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)
  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的

Reactive判断的API

  • isProxy
    检查对象是否是由 reactive 或 readonly创建的 proxy
  • isReactive
    检查对象是否是由 reactive创建的响应式代理
    如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
  • isReadonly
    检查对象是否是由 readonly 创建的只读代理
  • toRaw
    返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
  • shallowReactive
    创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
  • shallowReadonly
    创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深层只读转换(深层还是可读、可写的)。

5、Ref API

1、基本使用

reactive API对传入的类型有限制的,它要求我们必须传入的是一个对象或者数组类型

  • 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告
    在这里插入图片描述
    这个时候Vue3给我们提供了另外一个API:ref API
  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源
  • 它内部的值是在ref的 value 属性中被维护
    vue3的Composition API详解,包含hook的思想_第8张图片
    这里有两个注意事项:
  • 模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用
  • 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式

2、Ref自动解包

模板中的解包是浅层的解包
vue3的Composition API详解,包含hook的思想_第9张图片

3、ref其他的API

  • unref
  • 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法
  • 如果参数是一个 ref,则返回内部值,否则返回参数本身
  • 这是val = isRef(val) ? val.value : val的语法糖函数
  • isRef
  • 判断值是否是一个ref对象
  • shallowRef
  • 创建一个浅层的ref对象
  • triggerRef
  • 手动触发和 shallowRef 相关联的副作用
    vue3的Composition API详解,包含hook的思想_第10张图片

6、readonly

1、认识readonly

我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用但是不能被修改,这个时候如何防止这种情况的出现呢?

  • Vue3为我们提供了readonly的方法
  • readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改)
    vue3的Composition API详解,包含hook的思想_第11张图片
    在开发中常见的readonly方法会传入三个类型的参数
  • 类型一:普通对象
  • 类型二:reactive返回的对象
  • 类型三:ref的对象

2、readonly的使用

在readonly的使用过程中,有如下规则

  • readonly返回的对象都是不允许修改的
  • 但是经过readonly处理的原来的对象是允许被修改
  • 比如 const info = readonly(obj),info对象是不允许被修改的
  • obj被修改时,readonly返回的info对象也会被修改
  • 但是我们不能去修改readonly返回的对象info
  • 其实本质上就是readonly返回的对象的setter方法被劫持了而已

3、readonly的应用

那么这个readonly有什么用呢?

在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用readonly了

vue3的Composition API详解,包含hook的思想_第12张图片
vue3的Composition API详解,包含hook的思想_第13张图片

7、toRefs

如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的

vue3的Composition API详解,包含hook的思想_第14张图片
那么有没有办法让我们解构出来的属性是响应式的呢?

  • Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref
  • 那么我们再次进行解构出来的 name 和 age 本身都是 ref的
    在这里插入图片描述
  • 这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化

8、toRef

如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法

vue3的Composition API详解,包含hook的思想_第15张图片

9、customRef

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制

  • 它需要一个工厂函数,该函数接受 tracktrigger 函数作为参数
  • 并且应该返回一个带有 get 和 set 的对象

这里我们使用一个案例:

  • 对双向绑定的属性进行debounce(节流)的操作
    vue3的Composition API详解,包含hook的思想_第16张图片
    vue3的Composition API详解,包含hook的思想_第17张图片

10、computed

在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理

  • 在前面的Options API中,我们是使用computed选项来完成的
  • 在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性

如何使用computed呢?

  • 方式一接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象
  • 方式二接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象

computed的返回值是一个ref对象
vue3的Composition API详解,包含hook的思想_第18张图片

11、侦听数据的变化

前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作

Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听

  • watchEffect用于自动收集响应式数据的依赖
  • watch需要手动指定侦听的数据源

1、watchEffect

当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect

我们来看一个案例

  • 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖

  • 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行
    vue3的Composition API详解,包含hook的思想_第19张图片
    watchEffect的停止侦听

  • 如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可

  • 比如在上面的案例中,我们age达到25的时候就停止侦听
    vue3的Composition API详解,包含hook的思想_第20张图片
    watchEffect清除副作用

  • 什么是清除副作用呢?

  • 比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了

  • 那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用

  • 在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数onInvalidate

  • 副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数

  • 我们可以在传入的回调函数中执行一些清除工作
    vue3的Composition API详解,包含hook的思想_第21张图片
    setup中使用ref获取标签元素或者组件
    vue3的Composition API详解,包含hook的思想_第22张图片
    vue3的Composition API详解,包含hook的思想_第23张图片
    watchEffect的执行时机

  • 默认情况下,组件的更新会在副作用函数执行之前

  • 如果我们希望在副作用函数中获取到元素,是否可行呢?

  • 上面的案例的结果描述

  • 我们会发现打印结果打印了两次:

  • 这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null

  • 当DOM挂载时会给title的ref对象赋值新的值副作用函数会再次执行打印出来对应的元素

    调整watchEffect的执行时机

  • 如果我们希望在第一次的时候就打印出来对应的元素呢?

  • 这个时候我们需要改变副作用函数的执行时机

  • 它的默认值是pre,它会在元素 挂载 或者 更新执行

  • 所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素

  • 我们可以设置副作用函数的执行时机
    vue3的Composition API详解,包含hook的思想_第24张图片

  • flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要

2、Watch的使用

watch的API完全等同于组件watch选项的Property

  • watch需要侦听特定的数据源,并在回调函数中执行副作用
  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调

与watchEffect的比较,watch允许我们

  • 懒执行副作用(第一次不会直接执行
  • 更具体的说明当前哪些状态发生变化时,触发侦听器的执行
  • 访问侦听状态变化前后的值

侦听单个数据源

  • watch侦听函数的数据源有两种类型
  • 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)
  • 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref)
    vue3的Composition API详解,包含hook的思想_第25张图片
    侦听多个数据源
  • 侦听器还可以使用数组同时侦听多个源
    vue3的Composition API详解,包含hook的思想_第26张图片
    侦听响应式对象
  • 如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构
    vue3的Composition API详解,包含hook的思想_第27张图片
    watch的选项
  • 如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true
  • 也可以传入 immediate 立即执行
    vue3的Composition API详解,包含hook的思想_第28张图片

12、生命周期钩子

我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代生命周期钩子
  • 那么setup中如何使用生命周期函数呢?
  • 可以使用直接导入的 onX 函数注册生命周期钩子
  • 下表包含如何在 setup () 内部调用生命周期钩子
    vue3的Composition API详解,包含hook的思想_第29张图片
    vue3的Composition API详解,包含hook的思想_第30张图片
    vue3的Composition API详解,包含hook的思想_第31张图片

13、Provide函数与Inject函数

事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。

Provide函数

  • 我们可以通过 provide来提供数据
  • 可以通过 provide 方法来定义每个 Property
  • provide可以传入两个参数
  • name:提供的属性名称
  • value:提供的属性值

Inject函数

  • 在 后代组件 中可以通过 inject 来注入需要的属性和对应的值
  • 可以通过 inject 来注入需要的内容
  • inject可以传入两个参数
  • 要 inject 的 property 的 name
  • 默认值
    vue3的Composition API详解,包含hook的思想_第32张图片
    vue3的Composition API详解,包含hook的思想_第33张图片

14、vue3的hooks思想

其实vue中的代码抽取很像react的hooks,下面来体验一下hooks怎么使用
目录结构
vue3的Composition API详解,包含hook的思想_第34张图片
App.vue

<template>
  <div>
    <h2>当前计数: {{counter}}</h2>
    <h2>计数*2: {{doubleCounter}}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>

    <h2>{{data}}</h2>
    <button @click="changeData">修改data</button>

    <p class="content"></p>

    <div class="scroll">
      <div class="scroll-x">scrollX: {{scrollX}}</div>
      <div class="scroll-y">scrollY: {{scrollY}}</div>
    </div>
    <div class="mouse">
      <div class="mouse-x">mouseX: {{mouseX}}</div>
      <div class="mouse-y">mouseY: {{mouseY}}</div>
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue';

import {
  useCounter,
  useLocalStorage,
  useMousePosition,
  useScrollPosition,
  useTitle
} from './hooks';

export default {
  setup () {
    // counter
    const { counter, doubleCounter, increment, decrement } = useCounter();

    // title
    const titleRef = useTitle("coderwhy");
    setTimeout(() => {
      titleRef.value = "kobe"
    }, 3000);

    // 滚动位置
    const { scrollX, scrollY } = useScrollPosition();

    // 鼠标位置
    const { mouseX, mouseY } = useMousePosition();

    // localStorage
    const data = useLocalStorage("info");
    const changeData = () => data.value = "哈哈哈哈"

    return {
      counter,
      doubleCounter,
      increment,
      decrement,

      scrollX,
      scrollY,

      mouseX,
      mouseY,

      data,
      changeData
    }
  }
}
</script>

<style scoped>
.content {
  width: 3000px;
  height: 5000px;
}

.scroll {
  position: fixed;
  right: 30px;
  bottom: 30px;
}
.mouse {
  position: fixed;
  right: 30px;
  bottom: 80px;
}
</style>

useCounter.js

import { ref, computed } from 'vue';

export default function() {
  const counter = ref(0);
  const doubleCounter = computed(() => counter.value * 2);

  const increment = () => counter.value++;
  const decrement = () => counter.value--;

  return {
    counter, 
    doubleCounter, 
    increment, 
    decrement
  }
}

useTitle.js

import { ref, watch } from 'vue';

export default function(title = "默认的title") {
  const titleRef = ref(title);

  watch(titleRef, (newValue) => {
    document.title = newValue
  }, {
    immediate: true
  })

  return titleRef
}

useScrollPosition.js

import { ref } from 'vue';

export default function() {
  const scrollX = ref(0);
  const scrollY = ref(0);

  document.addEventListener("scroll", () => {
    scrollX.value = window.scrollX;
    scrollY.value = window.scrollY;
  });

  return {
    scrollX,
    scrollY
  }
}

useMousePosition.js

import { ref } from 'vue';

export default function() {
  const mouseX = ref(0);
  const mouseY = ref(0);

  window.addEventListener("mousemove", (event) => {
    mouseX.value = event.pageX;
    mouseY.value = event.pageY;
  });

  return {
    mouseX,
    mouseY
  }
}

useLocalStorage.js

import { ref, watch } from 'vue';

export default function(key, value) {
  const data = ref(value);

  if (value) {
    window.localStorage.setItem(key, JSON.stringify(value));
  } else {
    data.value = JSON.parse(window.localStorage.getItem(key));
  }

  watch(data, (newValue) => {
    window.localStorage.setItem(key, JSON.stringify(newValue));
  })

  return data;
}

// 一个参数: 取值
// const data = useLocalStorage("name");

// // 二个参数: 保存值
// const data = useLocalStorage("name", "coderwhy");

// data.value = "kobe";

index.js

import useCounter from './useCounter';
import useTitle from './useTitle';
import useScrollPosition from './useScrollPosition';
import useMousePosition from './useMousePosition';
import useLocalStorage from './useLocalStorage';

export {
  useCounter,
  useTitle,
  useScrollPosition,
  useMousePosition,
  useLocalStorage
}

这样的代码结构一目了然,便于他人理解。

本篇文章主体内容来自coderwhy老师的讲解,很喜欢这个老师,干货满满

你可能感兴趣的:(Vue日常总结,前端,vue.js,javascript)