40张图全面剖析Vue3核心的CompositionAPI(十四)

8,340 阅读10分钟

0.Vue3升级

img
  • Vue3性能提升
  • 源码升级:
    • 源码TS重写,全新的TS支持
    • 使用Proxy代替defineProperty实现响应式
    • 重写虚拟DOM的实现
      • PatchFlag编译模板,动态节点标记成不同类型如TEXT PROPS,方便diff算法更好区分静态和不同类型的动态节点
      • hoistStatic静态节点提升父作用域缓存,多个相邻的静态节点会被合并,空间换时间的优化策略
      • cacheHandler缓存事件
    • Tree-Shaking
      • 编译时,根据不同的情况,引入不同的 API
    • SSR优化
      • 静态节点直接输出,绕过了vdom,动态节点,还是需要动态渲染
  • 新特性:
    • Composition API(组合API)
    • 新的内置组件
      • Fragment
      • Teleport
      • Suspense
  • 全局API的改变
    • 将全局的API,即:Vue.xxx调整到应用实例(app)上
2.x 全局 API(Vue3.x 实例 API (app)
Vue.config.xxxxapp.config.xxxx
Vue.config.productionTip移除
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties
  • 其他改变:

    • 异步组件需要使用defineAsyncComponent创建方法,组件v-model,自定义指令,$attrs包含class&style,v-for和v-if优先级,过渡类名改变
  • data 选项应始终被声明为一个函数,在beforeDestroy生命周期的选项已更名为beforeUnmount,在destroyed生命周期的选项已更名为unmounted

  • 移除keyCode支持作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除$children、过滤器filter、v-on.native修饰符(自定义事件在emits选项中定义,没有定义的为原生事件)、.sync 修饰符等等

    • $on$off$once 实例方法已被移除
    • .....
  • 更好的周边工具:Vite、Pinia

最新Vue3.2对ref性能提高巨大,建议总是都使用ref定义响应式,全新的script setup语法

组件状态驱动的动态 CSS 值:p{color: v-bind(color)} 其中color为定义的变量

....

1.extends

之前我抽离公共逻辑,使用的Vue2和Vue3都支持的Mixin

另外一个类似于Mixin的方式是通过extends属性

在开发中extends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API

image-20220203133012442

2.Options API的弊端

在Vue2中,我们编写组件的方式是Options API,在对应的属性中编写对应的功能模块,比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命 周期钩子

但是这样会有一个弊端,当我们实现某一个功能,实现的代码会被拆分到各个属性里面,当随着组件逻辑的增多,同一个功能的分散可能导致代码的难于阅读和理解

定义数据与使用数据被分割在组件的各个位置,导致我们需要不断地翻滚页面来查看具体的业务逻辑

伴随着组件越来越复杂,分割的情况会越来越严重,而这就是CompositionAPI所要解决的问题

最后通过hook函数把定义数据与使用数据的逻辑放在一起进行处理,以达到更加易读,更加方便扩展的目的!

3.CompositionAPI编写位置

使用CompositionAPI编写代码的位置是setup函数,相当于组件的另一个选项

但是这一个选项里可以代替之前大部分的选项,比如methods、computed、watch、data、生命周期等等

官方文档: v3.cn.vuejs.org/guide/compo…

4.setup函数的参数和返回值

setup函数中获取组件实例

CompositionAPI中没有this

可通过getCurrentInstance获取当前实例

注意使用的API都需要从vue中导入

例如:import { onMounted, getCurrentInstance } from "vue"

image-20220204151823687

setup函数参数

setup函数的参数有两个

  • 第一个参数props对象:组件外部传递过来,且组件内部声明接收了的属性

  • 第二个参数context上下文对象,其内部包含三个属性:

    • attrs对象:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs
    • slots:收到的插槽内容,相当于 this.$slots(这个在以后渲染函数返回时会有作用,后文会写)
    • emit:分发自定义事件的函数,相当于 this.$emit

    注意:setup在beforeCreate之前自动执行一次,this是undefined,所以setup函数里访问this不能找到组件实例,因为setup调用发生在data、computed、methods解析之前,自然也就不可能使用this.$emit等有关方法

image-20220204210823998

setup函数返回值

返回值:

  • 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注! 相当于替代了选项的data、methods)
  • 若返回一个渲染函数:则可以自定义渲染内容。(了解)

注意:

  • 尽量不要与Vue2.x配置混用

    • Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法

    • 但在setup中不能访问到Vue2.x配置(data、methos、computed...)

    • 如果有重名, setup优先

  • setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)

image-20220205160710772

当我们点击button的时候 counter变量改变了,但是Vue没有模板没有追踪其更新改变界面

5.Reactive API

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

const 代理对象= reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)

传入的源对象是一个对象或者数组类型,否则会报警告,所以定义基础类型要用ref函数

image-20220203151558103

当我们使用reactive函数处理数据后,就会进行依赖收集,当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)

reactive函数定义的响应式数据是“深层次的”,内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作

6.Ref API

创建一个包含响应式数据的引用对象(reference对象,简称ref对象),该对象作为响应式引用维护着它内部value属性的值

  • JS中操作数据: xxx.value
  • 模板中读取数据: 不需要.value,直接:<div>{{xxx}}</div>

备注:

  • 接收的数据可以是:基本类型和对象类型
  • 基本类型的数据:响应式依然是靠Object.defineProperty()getset完成的
  • 对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数—— reactive函数
image-20220203153711344

ref的浅层解包

image-20220203154210056

可以使用ref获取元素或组件,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可

image-20220204005203810

reactive对比ref

  • 从定义数据角度对比:
    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型数据
    • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象
  • 从原理角度对比:
    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)。
    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
  • 从使用角度对比:
    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据:均不需要.value

7.toRefs和toRef

toRef则是转换一个reactive对象中的某个属性为ref

使用场景:有一个响应式对象数据,但是模版中只需要使用其中一项数据

toRefs可以将reactive返回的对象中的属性都转成ref

使用场景:想使用响应式对象中的多个或者所有属性做为响应式数据

toRef的使用

image-20220203221055113

toRefs的使用

image-20220203220140934

8.computed

我们可以在 setup 函数中使用 computed 方法来编写一个计算属性

方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象

image-20220204003104605

方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象

image-20220204003147252

9.watch和watchEffect

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

watchEffect用于自动收集响应式数据的依赖,当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect

watch需要手动指定侦听的数据源

watchEffect基本使用

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

其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行

image-20220204003542516

watchEffect的停止侦听

我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可

比如上面age,我们希望数字达到20就停止侦听

image-20220204003847043

watchEffect清除副作用

清除副作用,比如我们侦听函数里执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数依赖数据变化再次执行了

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

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

当 副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数,在回调函数里执行一些清除工作

image-20220204004854294

watchEffect的执行时机

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

image-20220204010334211

上面会打印两次结果:

  1. 当我们setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null
  2. 而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素

当更改 title 时,将在组件更新前执行副作用。

我们可以配置watchEffect的第二个参数flush来调整执行时机:

  • 默认值是pre,DOM挂载或者更新之前执行
  • post:DOM挂载或更新之后执行,需要使用模板元素时使用
  • sync,这将强制效果始终同步触发,不建议
image-20220204010911622

watch的基本使用

watch需要侦听特定的数据源,并在回调函数中执行副作用

默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调

与watchEffect的比较,watch允许我们

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

侦听单个数据源

image-20220204014456211

侦听多个数据源

image-20220204020010406

watch的选项

  • 深度侦听,设置deep为true
  • 立即执行,设置immediate为true
image-20220204020305519

10.生命周期钩子

上面已经使用setup替代了data 、 methods 、 computed 、watch 等这些选项

我们来说如何在setup中使用生命周期函数

可以使用直接导入的 onX 函数注册生命周期钩子:

image-20220204022202693
  • 与Vue2.x中钩子对应关系如下:
    • beforeCreate===>setup()
    • created=======>setup()
    • beforeMount ===>onBeforeMount
    • mounted=======>onMounted
    • beforeUpdate===>onBeforeUpdate
    • updated =======>onUpdated
    • beforeUnmount ==>onBeforeUnmount
    • unmounted =====>onUnmounted
image-20220204023123214

11.其他CompositionAPI

1.readonly 与 shallowReadonly

readonly让一个响应式数据变为只读的(深只读)

但是readonly包裹的原来的对象是允许被修改的,尽量避免修改进而影响readonly的值

本质上就是readonly返回的原生对象只读代理的setter方法被劫持了

shallowReadonly:让一个响应式数据变为只读的(浅只读)

它们常见传入的参数

  1. 普通对象
  2. ref对象
  3. reactive对象
image-20220203153332656

2.shallowReactive 和 shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)。
  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
  • 什么时候使用?
    • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。

3.toRaw 与 markRaw

  • toRaw:
    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw:
    • 作用:标记一个对象,使其永远不会再成为响应式对象。
    • 应用场景:
      1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
      2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4.provide 与 inject

image-20220204023013398
  • 作用:实现祖与后代组件间通信
  • 父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据,提供响应式,在 provide提供值时使用 ref 和 reactive
  • provide可以传入两个参数: 1.提供的属性名称,2.提供的属性值
  • inject也可以传入两个参数:1.provide提供的属性名,2.默认值
  • 具体写法:
image-20220204114723937

如果要修改值,最好向下层组件提供一个方法,调用该方法,该数据会在提供的位置进行修改

image-20220204115757859

5.customRef

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

它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数

并且应该返回一个带有 get 和 set 的对象

对双向绑定的属性实现防抖效果:

image-20220204121241058

6.响应式数据的判断

isRef: 检查一个值是否为一个 ref 对象

isReactive: 检查一个对象是否是由 reactive 创建的响应式代理,如果该代理是 readonly 创建建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true

isReadonly: 检查一个对象是否是由 readonly 创建的只读代理

isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

7.ref其他的API

unref:获取一个ref引用中的value,如果参数是ref,返回内部值,否则返回参数本身,这是 val = isRef(val) ? val.value : val 的语法糖函数

triggerRef:手动触发和 shallowRef 相关联的副作用

image-20220204122629733

12.自定义Hook函数

hook本质是一个函数,把setup函数中使用的Composition API进行了封装

类似于vue2.x中的mixin,可以更好的抽离逻辑,复用代码

封装useCounter抽离计算器逻辑:

image-20220204124016432

封装useTitle修改title:

image-20220204124154475

封装useScrollPosition监听界面滚动位置

image-20220204124435408

封装useMousePosition监听鼠标位置

image-20220204124824552

封装useLocalStorage存储和获取数据

image-20220204125049398

为这些hook设置统一出口(不是必需)

image-20220204125731235

需要的组件使用:

<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("yunmu");
    setTimeout(() => {
      titleRef.value = "kobe";
    }, 3000);

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

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

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

    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>