Bootstrap

vue3 setup 里的计算属性(computed)和侦听器(watch/watchEffect)


前言

关于 vue2 的 computed 和 watch 请戳这里

在 vue3 的 setup 里使用 computed、watch 和 watchEffect,一定记得先引入 computed、watch 和 watchEffect。


一、setup 里的计算属性(computed)

在 vue3 中,computed 可以在 setup 函数里实现。除了写法不一样,功能上与 vue2 中的 computed 是一致的。

  • 只有 getter 时,传入一个回调函数。
  • 有 getter 和 setter时,传入一个对象,有 get 和 set 两个属性方法。
  • 需要将处理后的值返回作为该计算属性的值。

1、在 setup 里使用 computed 的三种方式

  • 直接在 setup 里使用 computed 函数;
  • 通过 defineComponent 函数在 setup 里使用 computed 函数;
  • 在 <script setup> 里使用 computed 函数。

其中,defineComponent 以及 <script setup> 都是 setup 的语法糖。

(1)、直接在 setup 里使用 computed 函数

例如:

<template>
  <div>{{ fullName }}</div>
</template>
<script>
  import { ref, computed } from 'vue'

  export default {
    setup() {
      const firstName = ref('hello')
      const lastName = ref('world')
      const fullName = computed(() => {
        return firstName.value + '-·-' + lastName.value
      })
      return {
        firstName,
        lastName,
        fullName
      }
    }
  }
</script>

(2)、通过 defineComponent 函数在 setup 里使用 computed 函数

defineComponent 函数是 vue3 的语法糖:

  • defineComponent 函数支持 TS 的 “参数类型推断”(如果你使用的是 vue3 + TS,那么使用 defineComponent 将会更友好。)

例如:

<template>
  <div>{{ fullName }}</div>
</template>
<script>
  import { defineComponent, ref, computed } from 'vue'

  export default defineComponent({
    setup() {
      const firstName = ref('hello')
      const lastName = ref('world')
      const fullName = computed(() => {
        return firstName.value + '-·-' + lastName.value
      })
      return {
        firstName,
        lastName,
        fullName
      }
    }
  })
</script>

(3)、在 <script setup> 里使用 computed 函数

<script setup> 是 vue3 的新的语法糖,之前的组合 API 相比:

  • 之前的组合 API 必须返回 return,使用 <script setup> 后就不必了。
  • 有更好的运行时性能。

例如:

<template>
  <div>{{ fullName }}</div>
</template>
<script setup>
  import { ref, computed } from 'vue'

  const firstName = ref('hello')
  const lastName = ref('world')
  const fullName = computed(() => {
    return firstName.value + '-·-' + lastName.value
  })
</script>

2、在 setup 里的 computed 的 getter 和 setter

当 computed 有 getter 和 setter 时,需要传入一个对象而不是一个函数作为 computed 的参数,然后在 computed 中实现 get 和 set 两个属性方法。

例如:

<template>
  <div> firstName: {{ firstName }} </div>
  <div> lastName: {{ lastName }} </div>
  <div> fullName: {{ fullName }} </div>
</template>
<script>
  import { reactive, toRefs, computed } from 'vue'

  export default {
    setup() {
      const user = reactive({
        firstName: 'hello',
        lastName: 'world'
      })
      const fullName = computed({
        get() {
          return user.firstName + '-·-' + user.lastName
        },
        set(val) {
          const nameList = val.split('-·-')
          user.firstName = nameList[0]
          user.lastName = nameList[1]
        }
      })
      return {
        ...toRefs(user),
        fullName
      }
    }
  }
</script>

二、setup 里的侦听器(watch、watchEffect、watchPostEffect 和 watchSyncEffect)

推荐优先考虑使用 watchEffect。

  • watch:侦听器。
  • watchEffect:初始化时会立即执行的侦听器(默认:immdiate: true)。
  • watchPostEffect:watchEffect() 使用 flush: ‘post’ 选项时的别名。(了解)
  • watchSyncEffect:watchEffect() 使用 flush: ‘sync’ 选项时的别名。(了解)

1、watch

在 vue3 中,watch 可以在 setup 函数里实现。除了写法不一样,功能上与 vue2 中的 watch 以及 $watch 是一致的。

setup 里的 watch 完全等同于组件侦听器 property。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

watch 的工作原理:侦听特定的数据源,并在回调函数中执行副作用。它默认是惰性的——只有当被侦听的源发生变化时才执行回调,不过,可以通过配置 immediate 为 true 来指定初始时立即执行第一次。可以通过配置 deep 为 true,来指定深度监视。

watch 的两个属性

  • immdiate: 默认情况下,侦听器需要 data 后面值改变了才会生效,若需要侦听器一进入页面就生效,那就需要使用 immediate。
  • deep: 默认情况下,侦听器只会监听数据本身的改变,若要进行深度监听,那就需要使用 deep。
  • immediate 和 deep 配置在第三个参数对象里。

例如:

<template>
  <div>
    <span>姓名:</span>
    <input type="text" v-model="name" />
  </div>
  <div>
    <span>年龄:</span>{{ age }}
    <input type="button" value="+" @click="age++" />
  </div>
</template>
<script>
  import { ref, toRefs, watch } from 'vue'

  export default {
    setup() {
      const name = ref('小樱')
      const age = ref(0)
      const selectLoves = ref([])
      const user = ref({ age: 0, gender: '女' })
      
      // 侦听一个 ref
      watch(age, (newValue, oldValue) => {
        console.log('111', newValue, oldValue)
      }, { immdiate: true })
      
      // 侦听多个 ref
      watch([name, age], (newValue, oldValue) => {
        console.log('222', newValue, oldValue)
      }, { immdiate: true })
      
	  // 侦听一个数组
      watch(selectLoves, (newVal, oldVal) => {
        console.log('333', newVal, oldVal)
      }, { immdiate: true })
	  
	  // 侦听对象里的某个属性
      watch(
        () => user.gender,
        (newValue, oldValue) => {
          console.log('444', newValue, oldValue)
        },
        { immdiate: true, deep:  true }
      )
      
      // 侦听对象里的多个属性
      watch(
        [() => user.age, () => user.gender],
        (newValue, oldValue) => {
          console.log('555', newValue, oldValue)
        },
        { immdiate: true, deep:  true }
      )
      
	  return { ...toRefs(user), name, age, selectLoves }
    }
  }
</script>

直接侦听一个对象时,建议:直接侦听对象里的具体的属性,而不是侦听对象本身。

【注意】:直接侦听一个对象时有 2 点需要注意:

  • 无法正确的获取 oldValue,因为:新值变了同时旧值也跟着变了——改变一个对象里属性的值时,新值和旧值指向的是同一块内存区,所以无法拿到旧值,也可以理解为是浅拷贝的问题。
  • 默认开启深度监听(deep: true),且将 deep 置为 false 无效。

2、watchEffect 函数

Vue3-侦听器-watch和watchEffect的使用

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

  • watch 默认是惰性的(immedate: false),而 watchEffect 默认会立即执行(immedate: true)并开启深度监听(deep: true)。
  • watch 可以访问变化前的值(oldVal)和变化后的值(newVal),而 watchEffect “无法”访问变化前的值(oldVal)。
  • watch 只追踪明确侦听的数据源。仅在数据源确实改变时才会触发回调(可以避免在发生副作用时追踪依赖)。watchEffect 只有在回调中使用数据才会进行监听,修改属性是不被监听的(即 get 属性才会监听,set 属性不进行监听)。

例如:

<template>
    <h1>Vue3新特性 -watchEffect 监听属性</h1>
	<div>
		<p>{{name}}</p>
		<p>{{nameObj.name}}</p>
	</div>
</template>

<script>
    import { ref, reactive, watch, watchEffect } from 'vue'

    export default {
        setup() {
            // 监听基本类型
            const name = ref('张三')
            setTimeout(() => {
                name.value = '李四'
            }, 1000)

            watch(name, (newVal, oldVal) => {
                console.log(newVal, oldVal)
            }, {immediate: true}) //立即执行

            //监听复杂类型
            const nameObj = reactive({name: 'zhangsan'})
            setTimeout(() => {
                nameObj.name = 'list'
            }, 2000)

            //复杂数据无法直接监听、惰性
            watch(() => nameObj, (newVal, oldVal) => {
                console.log(newVal, oldVal) //不会触发
            })

            //需要深度监听、不惰性
            watch(() => nameObj, (newVal, oldVal) => {
                console.log(newVal, oldVal) //newVal、oldVal具有响应式
            }, { deep: true })

            //也可以直接监听对象的属性
            watch(() => nameObj.name, (newVal, oldVal) => {
                console.log(newVal, oldVal)
            })

            // 同时监听多个对象的属性
            watch([() => nameObj.name, () => nameObj.lastName], ([newName, newLastName], [oldName, oldLastName]) => {
                console.log(newName, oldName, newLastName, oldLastName)
            })

            const stop = watchEffect(() => {
                console.log(name);
                console.log(nameObj.name);
            })

            // 5秒后停止监听
            setTimeout(()=>{  
                stop()
            },5000)
            
            return { 
                name,
                nameObj
            }
        }
    }
</script>

3、vue3 回调的触发时机,以及 watchPostEffect 和 watchSyncEffect

当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

vue3 给 watchEffect 提供了 flush 属性,用来调整回调函数的刷新时机。并分别为他们起了别名:

  • watchPostEffect:watchEffect() 使用 flush: ‘post’ 选项时的别名。
  • watchSyncEffect:watchEffect() 使用 flush: ‘sync’ 选项时的别名。

flush 有 3 个值可以选择:

  • pre:默认值,前置刷新——表示在 dom 更新前调用。比如:在 dom 更新前你需要改变某些数据,就使用 pre,这些数据改变完一起更新 dom,提高性能。类似于 vue2 的 beforeCreated 生命周期钩子函数。
  • post:后置刷新——表示 dom 更新完成后调用。比如:你要获取 dom 或者子组件,这与在 created 生命周期钩子函数里使用 nextTick() 的效果一样。
  • sync:同步调用。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: ‘sync’ 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。

如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: ‘post’ 选项:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post',
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

上述代码,可简写为:

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

同理 watchSyncEffect 也是同理。

【注意】:需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:

// 需要异步请求得到的数据
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

4、如何停止侦听器?

假设我们定义了下面这个侦听器:

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 输出 0

count.value++
// -> 输出 1

副作用清除:

watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

停止侦听器:

const stop = watchEffect(() => {})

// 当不再需要此侦听器时:
stop()

三、watch、watchEffect 和 computed 的对比

  • watch
    • 懒执行副作用——需要手动指明侦听的内容,也要指明侦听的回调。
    • 默认 immdiate 是 false,所以初始化时不会执行,仅在侦听的源数据变更时才执行回调。
    • 不需要有返回值。
    • 可以获得变化前的值(oldVal)。
  • watchEffect
    • 自动收集依赖,不需要手动传递侦听内容——自动侦听回调函数中使用到的响应式数据
    • 默认 immdiate 是 true,所以初始化时会立即执行,同时源数据变更时也会执行回调。
    • 不需要有返回值。
    • 无法获得变化前的值(oldVal)。
  • computed
    • 注重的计算出来的值(回调函数的返回值), 所以必须要写返回值。

【参考】
VUE3(十四)使用计算属性computed和监听属性watch
vue3的计算属性与watch
Vue 3 响应式侦听与计算
vue3计算属性(computed)与监听(watch)
vue3的 computed 计算属性 与 watch监听
watchEffect函数

;