Vue3新特性

Vue

介绍

本文介绍一些Vue3的新特性vue3中文文档 (opens new window)

# 生命周期的变化

vue2x vue3
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
update onUpdate
beforeDestroy onBeforeUnmount
destroy onUnmounted
errorCaptured onErrorCaptured

从上面表格来看

  • setup 代替了 beforeCreate,created,如果在 options api 中书写,会在 beforeCreate 之前执行
  • mounted、beforeMount、beforeUpdate、updated ,都改成 onxxxx
  • beforeDestroy 改成 onBeforeUnmount
  • destroyed 改成 onUnmounted

# 数据响应式原理不同

  • Vue2 响应式原理是利用Object.defineProperty()对数据进行劫持结合发布订阅模式来实现的
    Object.defineProperty()语法
object.defineProperty(obj, 'name', {
  value: '张三', # 属性的值,默认为 undefined。
  # enumerable: true, # 属性是否可枚举 默认为 false
  # configurable: true, # 属性是否可配置 默认为 false
  writable: true, # 属性是否可写 默认为 false 跟可配置不能同时存在
  get: function () {
      return name
  },
  set: function ( val ) {
      name = val
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

缺点:
1、object.defineProperty()不支持数组拦截
2、对象新增、删除属性没有响应式。需要使用Vue.set(obj, 'age', 30)来添加
3、只能重定义属性的读取(get)和设置(set)行为

  • Vue3 响应式原理是利用proxy对数据进行代理,vue会为组件的data对象创建一个响应式代理对象(reactive),通过proxy监听属性的变化,从而实现对数据的监控并更新视图
    proxy()语法
   #两个参数,对象,13个配置项
   const handler = {
     get: function(obj, prop) {
         return prop in obj ? obj[prop] : '';
     },
     set: function(target, prop, val) {
         target[prop] = val;
         return true
     }
   };
   const p = new Proxy({}, handler);

1
2
3
4
5
6
7
8
9
10
11
12

Proxy可以重定义更多的行为,比如delete函数调用等更多行为。
Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

# 响应式API

  • reactive() 函数创建一个响应式对象或数组 reactive()返回的是一个原始对象的proxy,它和原始对象是不相等的
  import { reactive } from 'vue'
  export default {
    # setup是一个专门用于组合式API的特殊钩子函数
    setup() {
      const state = reactive({ count: 0 })
      function increment() {
        state.count++
      }
      # 暴露 state increment 函数 到模板
      # 要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。
      return {
        state,
        increment
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

reactive() 的局限性
1、仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
2、因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失

  let state = reactive({ count: 0 })
  # 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
  state = reactive({ count: 1 })
1
2
3

同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性

  const state = reactive({ count: 0 })
  # n 是一个局部变量,同 state.count
  # 失去响应性连接
  let n = state.count
  # 不影响原始的 state
  n++
  # count 也和 state.count 失去了响应性连接
  let { count } = state
  # 不会影响原始的 state
  count++
1
2
3
4
5
6
7
8
9
10
  • <script setup>语法糖
  #<script setup> 中的代码会在每次组件实例被创建的时候执行
  #顶层的绑定会被暴露给模板当使用 <script setup> 的时候
  #任何在<script setup>声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用
  <script setup>
    import { reactive } from 'vue'
    const state = reactive({ count: 0 })
    function increment() {
      state.count++
    }
  </script>
  <template>
    <button @click="increment">
      {{ state.count }}
    </button>
  </template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • ref() 方法来允许我们创建可以使用任何值类型的响应式ref
    接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
    和响应式对象的属性类似,ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value。
    ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
  const obj = {
    foo: ref(1),
    bar: ref(2)
  }
  # 该函数接收一个 ref
  # 需要通过 .value 取值
  # 但它会保持响应性
  callSomeFunction(obj.foo)
  # 仍然是响应式的
  const { foo, bar } = obj
1
2
3
4
5
6
7
8
9
10

当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。下面是计数器例子,用 ref() 代替:

  <script setup>
    import { ref } from 'vue'
    const count = ref(0)
    function increment() {
      count.value++
    }
  </script>
  <template>
    <button @click="increment">
      {{ count }} <!-- 无需 .value -->
    </button>
  </template>
1
2
3
4
5
6
7
8
9
10
11
12
  • toRef()和toRefs() 这两个方法共同点就是用来创建响应式的引用的,主要用来取出响应式对象里的属性,或者解构响应式对象,解构出来的属性值依然是响应式属性,如果不用这两直接解构的话是会丢失响应式效果的
<script setup>
  import { reactive, toRef, toRefs } from 'vue'
  const data = reactive({
      name: '沐华',
      age: 18
  })
  # 这样虽然能拿到 name / age,但是会变成普通变量,没有响应式效果了
  const { name, age } = data
  # 取出来一个响应式属性
  const name = toRef(data, 'name')

  # 这样解构出来的所有属性都是有响应式的
  const { name, age } = toRefs(data)
  # 不管是 toRef 还是 toRefs,这样修改是会把 data 里的 name 改掉的
  # 就是会改变源对象属性,这才是响应式该有的样子
  name.value = '沐沐华华'
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 父子组件之间的通信 defineProps、defineEmits、defineExpose

  • 子父 - 接收参数
  # 父组件
  <template>
    <Child :name="name" />
  </template>
  # 子组件
  <script setup>
    const props = defineProps({
      name: { type: String, default:'', required: true }
    })
    console.log(props.name)
  </script>
1
2
3
4
5
6
7
8
9
10
11
  • 父子 - 派发事件
  # 子组件
  <script setup>
    import { defineEmits } from 'vue'
    const emit = defineEmits(['updateName'])
    emit('updateName', name.value)
  </script>
  # 父组件
  <template>
    <Child  @updateName="handleUpdate" />
  </template>
  <script setup>
    const handleUpdate=(data)={
      console.log(data)
    }
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 父调用子组件的函数
  # 子组件
  <script setup>
    import { ref, defineExpose } from 'vue'
    const changeName = (val) => {
      console.log(val)
    }
    # 将方法、变量暴露给父组件使用,父组件才可通过ref API拿到子组件暴露的数据
    defineExpose['changeName']
  </script>
  # 父组件
  <template>
    <Child ref="childElementRef"/>
  </template>
  <script setup>
    import { ref,onMounted } from 'vue'
    import Child from './Child.vue'
    const childElementRef = ref()
    onMounted(() => {
      console.log(childElementRef.value.changeName()) # DOM 元素将在初始渲染后分配给 ref
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Provide()和Inject()依赖注入

  • Provide() 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
    Provide()要为组件后代提供数据,需要使用到 provide() 函数:
<script setup>
  import { provide } from 'vue'
  provide('message', 'hello, Vue!') # message 注入名  hello, Vue! 注入值
</script>
1
2
3
4

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(), 使用不同的注入名,注入不同的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

<script setup>
  import { ref, provide } from 'vue'
  const count = ref(0)
  provide('key', count)
</script>
1
2
3
4
5

除了在一个组件中提供依赖,我们还可以在整个应用层(App)面提供依赖:

<script setup>
  import { createApp } from 'vue'
  const app = createApp({})
  app.provide('key', 'value')
</script>
1
2
3
4
5
  • inject()注入获取上层组件提供的数据,需使用 inject() 函数:
<script setup>
  import { inject } from 'vue'
  const message = inject('message')
</script>
1
2
3
4

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:

<script setup>
  # 如果没有祖先组件提供 "message"
  # `value` 会是 "我是默认值"
  import { inject } from 'vue'
  const message = inject('message', '我是默认值')
</script>
1
2
3
4
5
6
  • 和响应式数据配合使用,有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:
  # 在供给方组件内
  <script setup>
    import { provide, ref } from 'vue'
    const location = ref('North Pole')
    function updateLocation() {
      location.value = 'South Pole'
    }
    provide('location', {
      location,
      updateLocation
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
  # 在注入方组件内
  <script setup>
    import { inject } from 'vue'
    const { location, updateLocation } = inject('location')
  </script>
  <template>
    <button @click="updateLocation">{{ location }}</button>
  </template>
1
2
3
4
5
6
7
8

最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。

  # 在注入方组件内
  <script setup>
    import { provide, ref, readonly } from 'vue'
    const count = ref(0)
    provide('read-only-count', readonly(count))
  </script>
1
2
3
4
5
6

# watch 侦听器的不同

watch 就是用来监听一个已有属性,发生变化的时候做某些操作,Vue2 中常用有如下三种写法

  watch: {
      userId: 'getData',
      userName (newName, oldName) {
          this.getData()
      },
      userInfo: {
          handler (newVal, newVal) { this.getData() },
          immediate: true,
          deep: true
      }
  }
1
2
3
4
5
6
7
8
9
10
11

Vue3 的 watch 是一个函数,能接收三个参数,参数一是监听的属性,参数二是接收新值和老值的回调函数,参数三是配置项

<script setup>
  import { watch, ref, reactive } from 'vue'
  const name = ref('张三')
  const data = reactive({
    age: 18,
    money: 100000000000000000000,
    children: []
  })

  # 监听 ref 属性
  watch(name, (newName, oldName) => { ... })

  # 监听其他属性、路由或者状态管理的都这样
  watch(
      () => data.age, 
      (newAge, oldAge) => { ... }
  )

  # 监听多个属性,数组放多个值,返回的新值和老值也是数组形式
  watch([data.age, data.money], ([newAge, newMoney], [oldAge, oldMoney]) => { ... })

  # 第三个参数是一个对象,为可配置项,有5个可配置属性
  watch(data.children, (newList, oldList) => { ... }, {
      # 这两个和 Vue2 一样
      immediate: true,
      deep: true,
      # 回调函数的执行时机,默认在组件更新之前调用。更新后调用改成post
      flush: 'pre', # 默认值是 pre,可改成 post 或 sync
      # 下面两个是调试用的
      onTrack (e) { debugger }
      onTrigger (e) { debugger }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

  const x = ref(0)
  const y = ref(0)
  # 单个 ref
  watch(x, (newX) => {
    console.log(`x is ${newX}`)
  })
  # getter 函数
  watch(
    () => x.value + y.value,
    (sum) => {
      console.log(`sum of x + y is: ${sum}`)
    }
  )
  # 多个来源组成的数组
  watch([x, () => y.value], ([newX, newY]) => {
    console.log(`x is ${newX} and y is ${newY}`)
  })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意,你不能直接侦听响应式对象的属性值,例如:

  const obj = reactive({ count: 0 })
  
  # 错误,因为 watch() 得到的参数是一个 number
  watch(obj.count, (count) => {
    console.log(`count is: ${count}`)
  })
1
2
3
4
5
6

这里需要用一个返回该属性的 getter 函数:

  #提供一个 getter 函数
  watch(
    () => obj.count,
    (count) => {
     console.log(`count is: ${count}`) 
    }
  )
1
2
3
4
5
6
7
  • watchEffect()允许我们自动跟踪回调的响应式依赖。
  watchEffect(async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    )
    data.value = await response.json()
  })
1
2
3
4
5
6

回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行
1、 watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
2、watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确

回调的触发时机: 默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post’ 选项:

  watch(source, callback, {
    flush: 'post'
  })
  
  watchEffect(callback, {
    flush: 'post'
  })
1
2
3
4
5
6
7

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():

  import { watchPostEffect } from 'vue'
  
  watchPostEffect(() => {
    #在 Vue 更新后执行
  })
1
2
3
4
5
Last Updated: 2024/8/27 17:28:54