首页菜鸟笔记Vue3中如何watch props的变化
Created At : 2021-12-28
Last Updated: 2021-12-28

Vue3中如何watch props的变化

Vue3 提供了 watch 、computed、watchEffect、watchPostEffect、watchSyncEffect 等众多API,那如何正确的监听props的变化呢?

Watch侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

watch 需要侦听特定的数据源,并在回调函数中执行副作用。

默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

与 watchEffect 比较,watch 允许我们:

  • 懒执行副作用;
  • 更具体地说明什么状态应该触发侦听器重新运行;
  • 访问侦听状态变化前后的值。

侦听单个数据源

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref:

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

侦听多个数据源

侦听器还可以使用数组同时侦听多个源

const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues)
})

firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]

注意多个同步更改只会触发一次侦听器。

setup() {
  const firstName = ref('')
  const lastName = ref('')

  watch([firstName, lastName], (newValues, prevValues) => {
    console.log(newValues, prevValues)
  })

  const changeValues = () => {
    firstName.value = 'John'
    lastName.value = 'Smith'
    // 打印 ["John", "Smith"] ["", ""]
  }

  return { changeValues }
}

通过更改设置 flush: 'sync',我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行。例如:

const changeValues = async () => {
  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
  await nextTick()
  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}

侦听响应式对象

使用侦听器来比较一个数组或对象的值,这些值是响应式的,要求它有一个由值构成的副本。

const numbers = reactive([1, 2, 3, 4])

watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers)
  }
)

numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。

const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
    console.log('not deep', state.attributes.name, prevState.attributes.name)
  }
)

watch(
  () => state,
  (state, prevState) => {
    console.log('deep', state.attributes.name, prevState.attributes.name)
  },
  { deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

然而,侦听一个响应式对象或数组将始终返回该对象的当前值和上一个状态值的引用。 为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。 这可以通过诸如 lodash.cloneDeep 这样的实用工具来实现。

import _ from 'lodash'

const state = reactive({
  id: 1,
  attributes: {
    name: '',
  }
})

watch(
  () => _.cloneDeep(state),
  (state, prevState) => {
    console.log(state.attributes.name, prevState.attributes.name)
  }
)

state.attributes.name = 'Alex' // 日志: "Alex" ""

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

const count = ref(0)

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

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

Computed

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

即:默认computed是不可变的。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

Props

Props有两大特性:

props具有响应性

props 具有响应性,有两点需要理解:

    1. 不能使用 ES6 解构它,这会使其失去响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作:

// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}
    1. 通过子组件不需要直接watch props

因为:每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。

props是只读的

从props的定义来看,在Vue3中,props的确是只读的

export declare function defineProps<PropNames extends string = string>(props: PropNames[]): Readonly<{
    [key in PropNames]?: any;
}>;

export declare function defineProps<PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions>(props: PP): Readonly<ExtractPropTypes<PP>>;

export declare function defineProps<TypeProps>(): Readonly<TypeProps>;

由于props是只读的,因此,子组件不能直接修改props,而且子组件中,props 不建议和v-model绑定。

通常有两种方式进行转换:

//1.定义一个本地的 data property 并将这个 prop 作为其初始值
props: ['initialCounter'],
data() {
  return {
    counter: this.initialCounter
  }
}

//2.定义一个计算属性
props: ['size'],
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}

props单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。

因此,我们父组件可以异步更新props,这是没有问题的。

Vue3侦听props

<script lang="ts" setup>
const isShow = ref(false)
const props = defineProps({
    show: {
      type: Boolean,
      default: false
    },
    msg: {
      type: String,
      default: '哦,出错了!'
    }
})

// 侦听整个props
watch(props, (val) => {
  console.log("--------------watch--", val)
  // 当父组件传入 show 为true 时,输出
  // Proxy { show: true, msg: '哦,出错了!'}
})

// 侦听单个prop
watch(()=>props.show, (val) => {
    console.log("--------------watch--", val)
    // 当父组件传入 show 为true 时,输出
    // --------------watch-- true
})

一种错误的写法如下:

// 侦听单个prop
watch(props.show, (val) => {
  console.log("--------------watch--", val)
})

因为,props是响应式的,只能侦听其setter返回值。 具体见上面的说明。