4

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

 2 years ago
source link: https://juejin.cn/post/7060752562332368932
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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(Vue)3.x 实例 API (app)Vue.config.xxxxapp.config.xxxxVue.config.productionTip移除Vue.componentapp.componentVue.directiveapp.directiveVue.mixinapp.mixinVue.useapp.useVue.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中监听属性改变,也包括生命 周期钩子

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

f84e4e2c02424d9a99862ade0a2e4114~tplv-k3u1fbpfcp-watermark.awebp

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

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

bc0be8211fc54b6c941c036791ba4efe~tplv-k3u1fbpfcp-watermark.awebp

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

6cc55165c0e34069a75fe36f8712eb80~tplv-k3u1fbpfcp-watermark.awebp

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等有关方法

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和异步组件的配合)

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

5.Reactive API

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

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

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

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

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

6.Ref API

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

  • JS中操作数据: xxx.value
  • 模板中读取数据: 不需要.value,直接:<div>{{xxx}}</div>
  • 接收的数据可以是:基本类型和对象类型
  • 基本类型的数据:响应式依然是靠Object.defineProperty()getset完成的
  • 对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数—— reactive函数

ref的浅层解包

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

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的使用

toRefs的使用

8.computed

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

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

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

9.watch和watchEffect

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

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

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

watchEffect基本使用

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

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

watchEffect的停止侦听

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

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

watchEffect清除副作用

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

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

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

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

watchEffect的执行时机

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

上面会打印两次结果:

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

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

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

watch的基本使用

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

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

与watchEffect的比较,watch允许我们

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

侦听单个数据源

侦听多个数据源

watch的选项

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

10.生命周期钩子

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

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

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

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

11.其他CompositionAPI

1.readonly 与 shallowReadonly

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

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

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

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

它们常见传入的参数

  1. ref对象
  2. reactive对象

2.shallowReactive 和 shallowRef

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

3.toRaw 与 markRaw

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

4.provide 与 inject

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

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

5.customRef

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

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

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

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

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 相关联的副作用

12.自定义Hook函数

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

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

封装useCounter抽离计算器逻辑:

封装useTitle修改title:

封装useScrollPosition监听界面滚动位置

封装useMousePosition监听鼠标位置

封装useLocalStorage存储和获取数据

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

需要的组件使用:

<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>
复制代码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK