用实际开发的视角去学vue3(一)
1. 前言
vue3出来很长时间了,一直没系统的看过和实际去写过,只是当时看了vue3的文档。现在正好有时间,系统的学习了一下vue3。写惯了选项式的vue2的代码,刚开始接触vue3的组合式代码,乍一看有些眼高手低。但是写过几天代码后,vue3确实要比vue2用起来舒服很多。vue3给我的感觉就是我有些数据不需要响应式,完全可以定义成非响应式数据,在页面依然能够展示,我觉得在这一点上就有那么一点点的性能优化。话不多说,下面记录一下我学习的过程。
2. vue3的setup
经过几个版本的迭代,从一开始需要些setup函数到现在直接在script标签里写一个setup属性就可以了,但是还有个小小的麻烦的地方,就是我想要写组件name的时候,就必须写两个script标签,一个要用来定义名字。类似下面:
<script lang="ts" setup>
</script>
<script lang="ts">
export default {
name: '',
};
这里就有个很好用的插件:vite-plugin-vue-setup-extend,安转完成以后就可以像下面一样,name和setup写在同一个script标签中:
<script setup lang="ts" name="home">
</script>
我个人觉得这样写最舒服。
3. 响应式
在学习的过程中学到有两个API来定义响应式数据,一个是ref,一个是reactive。
ref
在组合式 API 中,推荐使用 ref()
函数来声明响应式状态:
import { ref } from 'vue'
const count = ref(0)
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
看官网的文档推荐使用ref。
ref 主要在基本类型使用,也可以在对象类型使用。
reactive
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
import { reactive } from 'vue'
const games = reactive([
{ id: "game1", name: "Dota2" },
{ id: "game2", name: "王者荣耀" },
{ id: "game3", name: "LoL" }
])
在模板中使用:
<div v-for="g in games" :key="g.id">
{{ g.name }}
</div>
响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive()
API 可以选择退出深层响应性。
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的。
reactive() 的局限性
reactive()
API 有一些局限性:
-
有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。 -
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
-
对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
let car = reactive( { brand: "benz", price: 1200 } ) // 当解构时,price 已经与 car.price 断开连接 let { price } = car // car = { brand: "雅迪", price: 12 } // car = reactive({ brand: "雅迪", price: 12 }) // 以上都不可以用 // 可以使用Object.assgin可以更新 Object.assign(car,{ brand: "雅迪", price: 12 })
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
总结一下就是reactive 只能是对象类型数据。并且你不能简单的去修改数据,想要修改数据可以使用Object.assgin可以更新。
在实际的开发中,个人觉得应该遵循如下的规则:
区别:
- ref创建的变量必须使用.value
- reactive重新分配一个对象,会失去响应式, 可以使用object.assign去整体替换
使用规则:
- 若需要一个基本类型的响应数据使用ref
- 若需要一个响应式对象,层级不深,ref和reactive都可以
- 若需一个响应式对象层级比较深,推荐使用reactive
4. 计算属性和侦听器(computed,watch,watchEffect)
这里的计算属性和侦听器和vue2的差不多,计算属性应该是新增加了可读可写的操作。
computed
姓:<input type="text" v-model="firstName"></input>
<br>
名:<input type="text" v-model="lastName"></input>
<br>
全名:<span>{{ fullName }}</span>
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
let firstName = ref("zhang")
let lastName = ref("san")
// 这样是一个只读
let fullName = computed(()=>{
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + lastName.value
})
// 可读可写
let fullName = computed({
get() {
return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + lastName.value
},
set(val) {
const [str1, str2] = val.split("-")
firstName.value = str1
lastName.value = str2
}
})
watch
这里的watch和vue2的使用方式还是有些区别。
// 监视ref的基本类型数据
let sum = ref(0)
const stop = watch(sum, (newVal, oldVal) => {
console.log("sum", newVal, oldVal)
if (newVal > 10) {
stop() // 取消监视
}
})
// 监听reactive数据
let person = reactive({
name: "张三",
age: 18,
gender: "女",
car: {
c1: "Benz",
c2: "Audi"
}
})
watch(
() => ({ ...person }),
(newVal, oldVal) => {
console.log("person发生变化了", newVal, oldVal)
})
// 减少监听范围,提高性能
watch(
() => [person.name, person.car.c1],
(newVal, oldVal) => {
console.log("person car发生变化了", newVal, oldVal)
},
{ deep: true }
)
// 监视ref定义的对象类型的数据
let person1 = ref({
name: "Tom",
age: 18,
gender: "女"
})
watch(person1.value, (newVal, oldVal) => {
console.log("person1发生变化了", newVal, oldVal)
})
// 监视ref定义的对象类型的数据,监视属性值的变化,开启deep
watch(person1, (newVal, oldVal) => {
console.log("person1发生变化了", newVal, oldVal)
}, { deep: true})
// 会出现一种情况,newVal和oldVal的值相同 为什么?因为是同一个对象,对象的地址没有变,所以读到的都是修改后的值
// 如何避免出这种情况?
watch(
() => ({ ...person1.value }),
(newVal, oldVal) => {
console.log('person1 changed', newVal, oldVal)
},
{ deep: true}
)
watchEffect
watchEffect会立即执行,用来监听响应式数据。
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
特别是注意侦听器是如何两次使用 todoId
的,一次是作为源,另一次是在回调中。
我们可以用 watchEffect
函数 来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
你可以参考一下这个例子的 watchEffect
和响应式的数据请求的操作。
watch和watchEffect的区别
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
转载自:https://juejin.cn/post/7397014008053088310