vue3之setup函数与响应式API

1,129 阅读10分钟

一、前言

自2020年9月18号vue3发布以来,直到今年2月份,vue3成为默认版本,意味着vue正式进入了vue3 时代,作为前端大军的一员,当然要紧跟时代的潮流,将vue3玩转起来,虽然前期也断断续续学过vue3的知识,也做过一些项目,但是都没有形成文章记录起来,因此想通过一系列的文章系统的学习一下vue3的知识,并记录一些使用过程中的经验,文章内容还是基础为主,希望能对你有帮助。

二、defineComponent

我们知道,vue3的 一个很大的变化就是他全面支持typescript了,所以在使用了解和使用setup函数之前,我们先来简单了解一下defineComponent,它是vue3新推出的API,可以帮助我们做Typescript的类型推导,这样的话就可以简化掉很多我们在编写时的类型定义。比如,在没有defineComponent之时,我们使用setup函数之前,必须给他的参数手动声明类型:

import { Slots } from 'vue'

// 声明 props 和 return 的数据类型
interface Data {
  [key: string]: unknown
}

// 声明 context 的类型
interface SetupContext {
  attrs: Data
  slots: Slots
  emit: (event: string, ...args: unknown[]) => void
}

// 使用的时候入参要加上声明, return 也要加上声明
export default {
  setup(props: Data, context: SetupContext): Data {
    // ...

    return {
      // ...
    }
  },
}

可以看出,这样的话就太繁琐了,但是有了defineComponent之后,我们使用setup函数就简单多了:

import { defineComponent } from 'vue'

export default defineComponent({
  setup(props, context) {
    // ...

    return {
      // ...
    }
  },
})

有了他之后我们就可以只需要声明自自己声明的变量的类型,专注于业务了。

三、setup函数

vue2的时候,在单文件组件中,我们都是在data,watch,computed,methods等选项中编写我们的逻辑代码,虽然也很有效,但是他也带来了一些问题,当我们的组件变得很大,逻辑很复杂时,代码逻辑就会变得很难阅读和理解,特别是对于未参与编写的人来说,简直是灾难。

但是有了compositionApi之后,我们就可以把我们的变量声明和逻辑处理函数放在一起,这样的话阅读起来就方便多了,而使用conpositionApi的地方,就是setup函数! 新的setup函数在组件被创建之前执行,一旦props被解析完成,他就将被作为组合式API的入口;

import { defineComponent } from 'vue'

export default defineComponent({
  setup(props, context) {
    // 业务代码
    // ...
    return {
      // 需要给 template 用的数据、函数放这里 return 出去...
    }
  },
})

需要注意的是,在vue2中,我们是通过this来获取实例,拿到他的数据以及执行他的方法,但是在setup函数里面,你可千万不要这么做!

setup的参数使用

参数类型含义是否必传
propsObject父组件传递下来的数据
contextObject组件执行上下文

props: 他是相应式的,前提是你不对它结构或者使用toReftoRefs进行响应式数据转换,否则只要传入新的props,他都将更新。

context:它是一个非响应式的普通对象,暴露三个组件的property

属性类型作用
attrs非响应式对象props未包含的属性都将变成attrs
slots非响应式对象插槽
emits方法触发事件

因为context是一个非响应式的普通对象,所以我们在使用的时候可以通过解构的方式单独引入某个属性,也可以通过context.xxx来使用,比如可以通过{emit}来引入,然后通过emit.xxx来使用他的属性。

但是对于attrsslot,千万不要解构这两个的属性,坚持通过slot.xxxattrs.xxx来使用他们的属性,因为虽然他们不是响应式的,但是他们也会随着组件本身的更新而更新。

四、响应式API之ref

vue3中,ref将会是我们最常用的一个API,我们可以用它来声明所以类型的数据,同时也可以获取Node节点,就是我们vue2中的this.$ref.xxx

变量类型声明

在声明一个变量的时候,我们都会给他一个类型声明,使用ref定义变量时类型声明的方法如下: 在ref中,类型声明都需要使用泛型,即使用<>包裹;

// 变量只有一个类型
const msg = ref<string>('Hello World!');

// 变量有多个类型
const city = ref<string | null>('深圳');

// 引用类型
const ids = ref<number[]>([1, 2, 3])

// 也可以自己声明一个类型
interface User {
      name: string
      age: number
      sex?: string
    }

const user = ref<User>({
  name: 'Pitter',
  age: 18,
  sex: '男',
})

以上就是使用ref声明变量的方法了,除此之外,ref还有一个作用,就是获取DOM节点和子组件,下面介绍如何获取DOM元素和子组件。

<template>
  <!-- 挂载DOM元素 -->
  <p ref="msg">
    给想要获取的节点加上ref属性
  </p>

  <!-- 挂载子组件 -->
  <Child ref="child" />
  给想要获取的子组件加上ref属性
  <!-- 挂载子组件 -->
</template>

script部分

import { defineComponent, onMounted, ref } from 'vue'
import Child from './Child.vue'

export default defineComponent({
  components: {
    Child
  },
  setup () {
    const msg = ref<HTMLElement | null>(null);
    const child = ref<typeof Child | null>(null);

    // 请保证视图渲染完毕后再执行节点操作 e.g. onMounted / nextTick
    onMounted( () => {
      // 比如获取DOM的文本
      console.log(msg.value.innerText);

      // 或者操作子组件里的数据
      child.value.isShowDialog = true;
    });

    // 必须return出去才可以给到template使用
    return {
      msg,
      child
    }
  }
})

注意点:

1、无论是使用ref定义的变量还是获取的DOM元素,都必须使用xxx.value才能正确获取到;

2、只能在视图渲染完成之后才能进行DOM或组件相关的操作,需要放到onMounted生命周期或者nextTick里进行操作;

3、变量必须return出去才能给template使用

另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,新版本的脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常运行(报错 TS2531: Object is possibly 'null'. )。

原因是:默认情况下 null 和 undefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 null 和 undefined 只能赋值给 void 和它们各自,这虽然是个更为严谨的选项,但因此也会带来一些影响赶工期的额外操作。

关于这个问题,有如下几种解决方案可供参考: 1、在对DOM节点或子组件进行操作时,增加一个判断

if ( child.value ) {
  // 读取子组件的数据
  console.log(child.value.num);

  // 执行子组件的方法
  child.value.sayHi('use if in onMounted');
}

2、通过 TS 的可选符?来将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值)

// 读取子组件的数据
console.log(child.value?.num);

// 执行子组件的方法
child.value?.sayHi('use ? in onMounted');

3、在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,由自己来决定是否需要对 null 进行判断:

{
  "compilerOptions": {
    // ...
    "strictNullChecks": false
  },
  // ...
}

4、使用any定义变量类型,但是能不要就不用。。

ref变量的使用

前面也提到了,使用ref定义的变量,必须使用xxx.value来使用

const msg = ref<string>('Hi!');
console.log(msg.value) // 'Hi!'

五、响应式API之reactive

reactive是除ref之外最常用的一个响应式API,但是它只能声明对象和数组;

类型声明及定义变量

ref不同的是,它的类型声明是写在变量名后面的,且不需要<>包裹,就和普通变量一样;

// 使用reactive声明一个对象

// 定义类型
interface User {
  name: string
  age: number
  sex?: string
}
// 定义对象
const user: User = reactive({
  name: 'Pitter',
  age: 18,
  sex: '男',
})

// 使用reactive声明一个数组

const ids: number[] = reactive([1, 2, 3])

const member: User[] = reactive([
  {
    name: 'Tom',
    age: 18,
    sex: '男',
  },
  {
    name: 'tony',
    age: '21',
  },
])

变量的使用

对于对象来说,使用reactive声明的对象和普通对象的使用方法是一样的

// 定义类型
interface User {
  name: string
  age: number
  sex?: string
}
// 定义对象
const user: User = reactive({
  name: 'Pitter',
  age: 18,
  sex: '男',
})

// 读取
console.log(user.name) // 'Pitter'
// 修改
user.name = 'Fully'
console.log(user.name) // 'Fully'

对于使用reactive声明的数组的使用就要注意了,因为如果操作不当的话,我们的数组变量是很容易失去响应性的,这样很容易导致数据更新了,但是你的template模板却没有更新;

对于普通数组,亦或者在vue2时,我们对数组的操作可能比较随意;

// 定义一个普通数组
let numbers: number[] = [ 1, 2, 3 ];

// 从另外一个对象数组里提取数据过来
numbers = data.map( item => item.id );

// 合并另外一个数组
let newNumbers: number[] = [ 4, 5, 6 ];
uids = [...uids, ...newUids];

// 重置数组
uids = [];

但是在对于reactive定义的数组,我们只能使用那些不会改变引用地址的操作,只有这样才不会失去响应性;

// 定义一个普通数组
let numbers: number[] = [ 1, 2, 3 ];


// 重置数组,此方法会失去响应性
// numbers = [];

// 重置数据,这个方法不会失去响应性
numbers.length = 0;

// 异步获取数据后,模板可以正确的展示
setTimeout( () => {
  uids.push(1);
}, 1000);

同时,不要对reactive变量进行解构,不然同样也会失去响应性;

// 定义类型
    interface User {
      name: string
      age: number
      sex?: string
    }
    // 定义对象
    const user: User = reactive({
      name: 'Pitter',
      age: 18,
      sex: '男',
    })

    setTimeout(() => {
      user.age = 28
    },3000)

    // 以下操作都会失去响应性
    // 3s后age都不会更新
    const { age } = user
    const newUser = { ...user }
    return {
      ...user,
    }

六、响应式API之toRef与toRefs

到这里,我们应该对refreactive有了一定的了解了,其实他们两非常相似,作用也一样,但是都各有不方便之处,对于ref声明的变量,我们每次都必须通过xxx.value来使用它们,着实有点烦人,而使用reactive声明的变量,由于不能解构,使用在template中使用的时候,也必须使用类似user.xxx的方式,也是有点烦人,那有没有什么办法来解决这个问题呢?

当然有的,vue3给我们提供了两个用来转换refreactive的API,toReftoRefs。接下来我们就来学习下这两个api的使用;

1、toRef: 创建一个新的变量,转换reactive的某一个属性为ref变量;

2、toRefs:创建一个新的对象,它的每个字段都是 reactive 对象各个字段的ref变量;

使用方法

toRef

它接受两个参数,第一个是reactive对象,第二个是要转换为ref变量的key

// 定义类型
interface User {
  name: string
  age: number
  sex?: string
}
// 定义对象
const user: User = reactive({
  name: 'Pitter',
  age: 18,
  sex: '男',
})

const userName = toRef(user, 'name')

这样就将一个reactive对象的属性转换为了一个ref变量,接下来就可以通过.value的方式去对他进行操作了。

需要注意的是,如果我们转换的是一个reactive对象上不存在的key,那么这个ref变量的值就是undefined,当我们给这个ref变量赋值之后,原先的reactive上也会增加这个key属性和值

toRefs

只接收一个参数,即reactive对象

// 定义类型
interface User {
  name: string
  age: number
  sex?: string
}
// 定义对象
const user: User = reactive({
  name: 'Pitter',
  age: 18,
  sex: '男',
})

const newUser = toRefs(user)

这个新的对象newUser本身是一个普通对象,但是他的属性都是ref变量。

这样我们就完成了reactiveref的转换,在使用的时候,我们在script中声明reactive对象,并进行操作,然后在最后returntemplate使用之前,使用toRefs进行转换,这样在模板中使用的时候就方便啦!

import { defineComponent, reactive, toRefs } from 'vue'

interface User {
  name: string,
  age: number,
  sex: string
};

export default defineComponent({
  setup () {
    // 定义一个reactive对象
    const userInfo = reactive({
      name: 'Tom',
      age: 18,
      sex: '男'
    })

    // 定义一个新的对象,它本身不具备响应性,但是它的字段全部是ref变量
    const userInfoRefs = toRefs(userInfo);

    // 2s后更新userInfo
    setTimeout( () => {
      userInfo.name = 'Tony';
      userInfo.age = 20;
    }, 2000);

    // 在这里解构toRefs对象才能继续保持响应式
    return {
      ...userInfoRefs
    }
  }
})

需要注意的是,虽然我们可以return解构后的toRefs对象,但是一定要确保没有同名属性,否则将会以后面的为准,如下例子,return的数据中,userInfoRefs中含有name属性,然后又声明了一个name变量,将会以变量的name值作为模板的值

return {
      ...userInfoRefs,
      name // 模板中的name的值以这个为准
    }

七、总结

这一篇文章主要记录的是vue3setup函数以及响应式API的使用,以及如何搭配TypeScript进行类型声明和一些注意事项,内容比较基础,在接触了vue3之后给我的感受就是,特别灵活方便,不仅给我们提供了两个响应式的API,同时还能进行转换,极大的满足了我们开发的需要,这里不得不给尤大大点个赞!同时也感觉自己还有学的东西很多,前端之路漫漫,加油!