likes
comments
collection
share

用实际开发的视角去学vue3(一)

作者站长头像
站长
· 阅读数 54

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" }
])

参考:reactive() 标注类型

在模板中使用:

<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 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumberboolean 这样的原始类型

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    let state = reactive({ count: 0 })
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
    
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

    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可以更新。

在实际的开发中,个人觉得应该遵循如下的规则:

区别:

  1. ref创建的变量必须使用.value
  2. reactive重新分配一个对象,会失去响应式, 可以使用object.assign去整体替换

使用规则:

  1. 若需要一个基本类型的响应数据使用ref
  2. 若需要一个响应式对象,层级不深,ref和reactive都可以
  3. 若需一个响应式对象层级比较深,推荐使用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() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

watchwatchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
转载自:https://juejin.cn/post/7397014008053088310
评论
请登录