Commit 825072ba authored by liyan's avatar liyan

2023年7月文章分享

parent 9335d67c
Pipeline #18344 canceled with stage
---
title: Vue2转Vue3,项目中这些常用的Vue3新特性你需要清楚!
date: 2022/3/29 10:42:20
tags:
- 前端
- vue3新特性
- vue
author: 李燕
---
最近进入第二个Vue3项目的开发,因与第一个Vue3项目开发的间隔时间比较长,所以此次开发,还是明显感觉到对Vue3一些常用的新特性不是很清晰,使用的过程中需要查文档、翻代码。故此处,将项目中用到的Vue3新特性进行汇总总结一下,以期明确用法,加深记忆。
<!-- more -->
#### 响应式API
`vue2`中,我们只要定义在`data()`方法中的数据就是响应式数据。但在`vue3`中主要是使用`ref``reactive`来定义响应式数据。由于`vue3`使用的是`proxy`进行响应式监听,所以新增、删除属性也都是响应式的。
##### 一、ref、reactive
`ref`
一般用来定义基本类型的响应式数据。接受一个内部值并返回一个响应式且可变的 `ref` 对象。`ref` 对象仅有一个 `.value` 属性,指向该内部值。
使用`ref`定义的响应式数据在`setup`函数中使用需要加上`.value`,但在模板中可以直接使用。
```
<template>
<div>{{ num }}</div>
</template>
<script setup>
import { ref } from "vue";
const num = ref(1);
const addNum = () => {
   num.value++;
}
</script>
```
`reactive`
用来定义引用类型的响应式数据,不能用来定义基本类型的响应式数据。
定义的对象是不能直接使用`es6`语法解构的,不然就会失去它的响应式。
```
<template>
<div>{{ state.num }}</div>
</template>
<script setup>
import { reactive } from "vue";
const state = reactive({
   num: 0,
});
const addNum = () => {
   state.num++;
}
</script>
```
##### 二、isRef、unref、toRef、toRefs
`isRef`
检查值是否为一个 `ref` 对象。
`unref`
如果参数是一个ref,则返回内部值,否则返回参数本身。这是 `val = isRef(val) ? val.value : val` 的语法糖函数。
`toRef`
可以用来为源响应式对象上的某个 `property` 新创建一个 `ref`。然后,`ref` 可以被传递,它会保持对其源 `property` 的响应式连接。(复制 reactive 里的单个属性并转成 ref。)
`toRefs`
复制 reactive 里的所有属性并转成 ref。
`reactive`定义的对象不能直接使用`es6`语法解构的,不然就会失去它的响应式,如果要保持响应性,解构时需要使用`toRefs()`方法。
#### 组合式API
为了让相关代码更紧凑`vue3`提出了组合式`api`,组合式`api`能将同一个逻辑关注点相关代码收集在一起。 组合式`api`的入口就是`setup`方法。
`setup`
`setup` 选项是一个接收 `props``context` 的函数,它在`beforeCreate`之前执行。
```
// 写法一
export default {
 setup(){}
}
// 写法二
export default defineComponent({
 setup(){}
})
```
##### 一、参数
- `props`
`props`就是我们父组件给子组件传递的参数。
`setup` 函数中的 `props` 是响应式的,不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构需使用`toRefs`方法。
如果`props`中的某个属性是可选的,则传入的`props`中可能没有该属性。在这种情况下,`toRefs` 将不会为 该属性创建一个 `ref` ,需要使用 `toRef` 替代它。
- `context`
`context` 是一个普通的 `JavaScript` 对象,也就是说,它不是响应式的,可以对 `context` 使用 `ES6` 解构。
```
 setup(props, context) {
   // Attribute
   console.log(context.attrs)
   // 插槽
   console.log(context.slots)
   // 触发事件 (方法)
   console.log(context.emit)
   // 暴露公共 property (函数)
   console.log(context.expose)
}
})
```
##### 二、返回值
我们在模板,或者`vue2`选项式写法的计算属性、方法、生命周期钩子等等中使用的数据都需要在`setup`方法中通过`return`返回出来。
如果 `setup` 返回一个对象,那么该对象的 `property` 以及传递给 `setup``props` 参数中的 `property` 就都可以在模板中访问到。
`setup` 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态。
##### 三、属性/方法暴露
假如我们想在父组件中直接调用子组件的方法,我们可以在子组件`setup`中使用return或`expose`把属性或方法暴露出去,然后在父组件我们就可以通过子组件的`ref`直接调用了。
当组件的`setup`中没定义`expose`暴露内容的时候,通过`ref`获取到的就是组件自身的内容,也就是`setup`函数`return`的内容和`props`属性的内容。
当定义了`expose`暴露内容的时候,通过ref获取到的就只是组件`expose`暴露内容,并且`setup`函数`return`的内容会失效,也就是会被覆盖。
##### 四、单文件setup
要使用这个语法,需要将 `setup` 添加到 `<script>` 代码块上。
```
<script setup>
</script>
```
当使用 `<script setup>` 的时候,任何在 `<script setup>` 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用。
- `defineProps``defineEmits`
`<script setup>` 中必须使用 `defineProps``defineEmits` API 来声明 `props``emits`
```
<template>
<div>{{ props.title }}</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
// 定义属性
const props = defineProps({
 title: String
})
// 定义事件
const emits = defineEmits(['change'])
//触发事件 类似 context.emit()
emits('change')
</script>
```
- ` defineExpose`
使用 `<script setup>` 的组件是默认关闭的,也即通过模板 ref 或者 `$parent` 链获取到的组件的公开实例,不会暴露任何在 `<script setup>` 中声明的绑定。
如果 `<script setup>` 组件中明确要暴露出去的属性,必须使用 `defineExpose`
也就是说,`setup函数`组件默认会暴露props和return里面的内容,而`<script setup>`语法糖不会暴露任何内容出去。
#### computed 和watch
##### 一、computed
`vue2`中computed 很简单,是一个对象,只需要简单定义就可以使用。
`vue3`中,computed 是函数式的,需要先引入再使用。
computed 可以接受一个函数,并根据 函数的返回值返回一个不可变的响应式 ref 对象。
也接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
```
import { reactive, computed } from "vue"
const user = reactive({ name: "张三", age: 24 });
// 接受一个函数,并根据返回值返回一个不可变的响应式ref对象
// 这里的fullName1是不能修改的
const fullName1 = computed(() => {
  return `${user1.name}今年${user1.age}岁啦`;
});
// 接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
// 这里的fullName2是可以修改的
let fullName2 = computed({
   get() {
     return `${user2.name}今年${user2.age}岁啦`;
  },
   set(val) {
       user2.name = val;
    },
});
const updateUser2Name = () => {
 // 需要使用value访问
 fullName2.value = "新的name";
};
```
##### 二、watch
```
const user = reactive({ name: "张三", age: 24 });
// source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量    
// callback: 执行的回调函数    
// options:支持 deep、immediate 和 flush 选项。
// 监听单一源:基本类型
watch(()=> user.name, (newVal, oldVal)=> {})
// 监听单一源:引用类型
watch( user1, (newVal, oldVal)=> {})
watch(()=> user1, (newVal, oldVal)=> {}, {deep: true})
//监听多个源
watch(
  [() => user.name, () => user.age],
  ([newVal1, newVal2], [oldVal1, oldVal2]) => {
       console.log(newVal1, newVal2);
       console.log(oldVal1, oldVal2);
  }
);
```
监听基本数据类型需要使用箭头函数方式,否则监听不到。
监听引用数据类型可以直接监听,但是新老值是一样的,如果需要对比新老值需要使用箭头函数并搭配深拷贝或浅拷贝的方式。
使用箭头函数也能监听引用数据类型,我们如果不需要对比新老值,可以直接使用第三个参数`deep:true`开启深度监听即可。如果需要对比新老值就没必要使用这种方式了,需要看情况使用深拷贝和浅拷贝。
对于监听引用数据类型里面的引用数据类型需要格外注意,需要判断引用数据类型是属性值改变还是地址改变,属性值改变可以直接监听,地址改变需要使用箭头函数的方式或者看情况使用深拷贝和浅拷贝。
##### 三、watchEffect
```
import { watchEffect,reactive } from "vue";
const user = reactive({ name: "张三", age: 27 });
const updateUser2Age = () => {
  user.age++;
};
watchEffect(() => {
     console.log("watchEffect", user.age);
});
```
watchEffect自动收集依赖,不需要手动传入依赖。当里面用到的数据发生变化时就会自动触发`watchEffect`。并且`watchEffect` 会先执行一次用来自动收集依赖。而且`watchEffect` 无法获取到变化前的值,只能获取变化后的值。
#### 生命周期
`vue3`虽然提倡把生命周期函数都放到`setup`中,但是`vue2`那种选项式写法还是支持的。
![QQ截图20230703094040.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f37b25f6264b402081e499411c601e74~tplv-k3u1fbpfcp-watermark.image?)
`vue2`相较于`vue3`少了`renderTracked``renderTriggered`两个生命周期方法。
销毁生命周期方法名也发生了变化,由`beforeDestroy``destroyed`变为`beforeUnmount``unmounted`,这样是为了更好的与`beforeMount``mounted` 相对应。
`vue3`写在`setup`函数中生命周期方法名就是前面多加了`on`
#### 模板指令
##### 一、v-model
`vue2`中,如果在自定义组件上使用`v-model`需要在组件内通过`model`参数指明`v-model`的属性和事件。
```
// 父组件
<Child v-model="value" />
// 子组件
<template>
 <div>
   <input type="text" :value="value" @input="handleInput" />
</button>
 </div>
</template>
<script>
export default {
 // 定义v-model传过来的值名字是value1 修改值的事件是change事件
 model: {
   prop: "value",
   event: "change",
},
 props: {
   value: String,
},
 methods: {
   handleInput(e) {
     this.$emit("change", e.target.value);
  },
},
};
</script>
```
`vue3`中自定义组件也可以使用`v-model`,但不用去指定`model`或者使用`.sync`参数了。
默认情况下,组件上的 `v-model` 使用 `modelValue` 作为 prop 和 `update:modelValue` 作为事件。
```
// 父组件
<Child v-model="name" />
// 子组件
<template>
 <div>
   <button @click="changeName">改变值</button>
 </div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
 props: {
   modelValue: String,
},
 setup() {
   const changeName = () => {
     context.emit("update:modelValue", "李四");
  };
   return {
     changeName,
  };
},
});
</script>
```
也可以自定义参数名。
```
// 父组件
<Child v-model:name="name1" />
// 子组件
<template>
 <div>
   <button @click="changeName">改变值</button>
 </div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
 props: {
   name: String,
},
 setup() {
   const changeName = () => {
     context.emit("update:name", "李四");
  };
   return {
     changeName,
  };
},
});
</script>
```
还可以通过向 `v-model` 传递多个参数,这在`vue2`中是不可以的。
```
<Child v-model:name1="name1" v-model:name2="name2" />
```
##### 二、key支持在template使用
`vue2`中,`key`是不能定义在`template`节点上的。但是在`vue3`中支持了。
##### 三、v-if和v-for优先级
`vue2``v-for`的优先级是比`v-if`高的,在一个元素上同时使用 `v-if``v-for` 时,`v-for` 会优先作用。
但在`vue3`中,`v-if`的优先级比`v-for`更高。
#### 组件
##### 一、异步组件
`vue2`中异步组件是通过将组件定义为返回 `Promise` 的函数来创建的。
```
const asyncModal = () => import('./Modal.vue')
```
`vue3`中异步组件通过`defineAsyncComponent`定义。
```
import { defineAsyncComponent } from 'vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
```
##### 二、emits
组件里面新增了`emits`选项,可以通过 `emits` 选项在组件上定义发出的事件。
```
export default defineComponent({
 // 与props类似,支持数组和对象
 // emits: ["submit"],
  emits: {
    submit: null
  },
 setup(props, { emit }) {
   const handleClick = () => {
     emit("submit", { name: "randy" });
  };
   return {
     handleClick,
  };
},
});
```
#### 其他
##### 一、插槽
在vue2中,插槽的写法如下
```
// 子组件
<slot name="content" :user="user"></slot>
// 父组件
<template slot="content" slot-scope="scoped">
 <div>name: {{scoped.user.name}}</div>
 <div>age: {{scoped.user.age}}</div>
<template>
```
在 vue3 中将`slot``slot-scope`进行了合并使用,使用`v-slot`代替。
```
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
 <div>name: {{scoped.user.name}}</div>
 <div>age: {{scoped.user.age}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{user}">
 <div>name: {{user.name}}</div>
 <div>age: {{user.age}}</div>
</template>
```
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment