likes
comments
collection
share

watch、watchEffect 用法详解 - vue3

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

watchEffect

watchEffect 接收一个回调函数,会在组件渲染时立即调用一次回调,同时自动追踪回调中的响应式依赖,当响应式依赖数据变更时会重新执行回调。

第一个参数,侦听器回调,该回调提供了一个参数

第二个为可选参数,是一个配置对象,有以下属性:

立即调用

侦听器回调默认会在 组件挂载前 立即调用一次,(可改变配置对象属性 flush 更改执行时机,后边会讲到:回调触发时机)

例子

const person = reactive({
    count: 0,
});
onBeforeMount(() => {
    console.log("onBeforeMount");
});
onMounted(() => {
    console.log("onMounted");
});
watchEffect(
    () => {
        const el = document.querySelector(".h1");
        console.log(el);
        console.log(person.count);
    }
);

watch、watchEffect 用法详解 - vue3

例子中,回调会立即执行,并且是在 onBeforeMount 之前执行的,所以获取不到 DOM .h1 元素。而且执行期间,它会自动追踪 person.count 作为依赖。person.count 变化会再次执行回调。

异步回调

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

例子

const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const branches = ['main', 'v2-compat']

const currentBranch = ref(branches[0])
const count = ref(0);

watchEffect(async () => {
  commits.value = await (await fetch(`${API_URL}${currentBranch.value}`)).json()
  count.value
})

currentBranch.value 改变时,异步回调会重新运行,而 count.value 改变时,异步回调是不会重新运行的。

停止监听器

调用 watchEffect 返回值会停止监听,或 组件卸载时也会自动停止监听。(异步创建的监听器,组件卸载时是不会自动停止监听的,所以需要手动调用,防止内存泄漏)

例子:

const stop = watchEffect(() => {
    console.log(person.count);
});
stop()

// 注意!异步创建,组件卸载是不会自动停止监听的
setTimeout(() => {
    watchEffect(() => {})
}, 100)
 

回调触发时机

Vue 默认会缓存侦听器回调,进行异步刷新,当有多个依赖数据同时改变时,也只会触发一次。并且会监听组件更新函数,在 组件挂载前组件更新前 触发回调。意味着你在侦听器回调中访问的 DOM 将是被 Vue 组件挂载前 或 组件更新前 的状态。

可给 watchEffect 传递第二个可选参数,配置对象,并给定 flush 属性,改变 副作用函数触发时机,和执行次数。该属性有以下三个值:

  • pre: 默认配置,组件挂载前组件更新前 执行侦听器回调,并且会缓存侦听器回调,异步刷新,所以同时改变多个依赖数据只会调用一次侦听器回调。

  • post组件挂载后组件更新后 执行侦听器回调,并且会缓存侦听器回调,异步刷新,所以同时改变多个依赖数据只会调用一次侦听器回调。

  • sync组件挂载前组件更新前 执行侦听器回调,并且不会缓存侦听器回调,同步刷新,所以同时改变多个依赖数据会多次调用侦听器回调。(性能不好

假设有以下例子:

<template>
  <button @click="change2">点击1</button>
  <h1 class="h1">{{ state }}</h1>
</template>
<script setup lang="ts">
import { onBeforeMount, onMounted, watchEffect, onBeforeUpdate, onUpdated, ref, reactive } from "vue";
onBeforeMount(() => {
  console.log("onBeforeMount");
});
onMounted(() => {
  console.log("onMounted");
});
onBeforeUpdate(() => {
  console.log("onBeforeUpdate");
});
onUpdated(() => {
  console.log("onUpdated");
});
const state = ref(0);
const person = reactive({
  count: 0,
});
const change2 = () => {
  state.value++;
  person.count++;
};
</script>

flush 属性值为 pre 时(默认配置)

watchEffect(() => {
    console.log('----------------');
    const el = document.querySelector(".h1");
    console.log('Dom 内容:', el?.textContent);
    console.log(person.count);
    console.log(state.value);
});

watch、watchEffect 用法详解 - vue3

例子中,侦听器回调会在 组件挂载前 立即调用,所以获取不到 DOM .h1 的内容。响应式依赖更改后,会在 组件更前 调用回调,所以获取到的 DOM .h1 内容是没更新前的状态。

并且调用 change2 同时改变 stateperson.count,只调用了一次侦听器回调,说明会缓存回调,并异步刷新

flush 属性值为 post

watchEffect(
  () => {
    console.log('----------------');
    const el = document.querySelector(".h1");
    console.log('Dom 内容:', el?.textContent);
    console.log(person.count);
    console.log(state.value);
  },
  {
    flush: "post"
  }
);
// --------等同于
// watchPostEffect(
//   () => {
//     console.log('----------------');
//     const el = document.querySelector(".h1");
//     console.log('Dom 内容:', el?.textContent);
//     console.log(person.count);
//     console.log(state.value);
//   }
// );

watch、watchEffect 用法详解 - vue3

例子中,侦听器回调会在 组件挂载后 立即调用,所以获取到了 DOM .h1 的内容。响应式依赖更改后,会在 组件更新后 调用回调,所以获取到的 DOM .h1 内容是最新的。

并且调用 change2 同时改变 stateperson.count,只调用了一次侦听器回调,说明会缓存回调,并异步刷新

flush 属性值为 sync

watchEffect(
  () => {
    console.log('----------------');
    const el = document.querySelector(".h1");
    console.log('Dom 内容:', el?.textContent);
    console.log(person.count);
    console.log(state.value);
  },
  {
    flush: "sync"
  }
);
// --------等同于
// watchSyncEffect(
//   () => {
//     console.log('----------------');
//     const el = document.querySelector(".h1");
//     console.log('Dom 内容:', el?.textContent);
//     console.log(person.count);
//     console.log(state.value);
//   }
// );

watch、watchEffect 用法详解 - vue3

例子中,侦听器回调会在 组件挂载前 立即调用,所以获取不到 DOM .h1 的内容。响应式依赖更改后,会在 组件更前 调用回调,所以获取到的 DOM .h1 内容是没更新前的状态。

并且调用 change2 同时改变 stateperson.count,调用了多次侦听器回调,说明不会缓存回调,并同步刷新

过期的副作用

有时侦听器会执行一些异步操作,可能会发生竞态问题。

例子:

const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const branches = ['main', 'v2-compat']

const currentBranch = ref(branches[0])
const finalData, = ref(null)

const change = () => {
  currentBranch.value = currentBranch.value === "v2-compat" ? 'main' : 'v2-compat'
};

watchEffect(async () => {
  const url = `${API_URL}${currentBranch.value}`
  finalData.value = await (await fetch(url)).json()
})

例子中,如果调用了change 函数,改变数据依赖 currentBranch,会重新调用侦听器回调,可能会发生如下图的问题:

watch、watchEffect 用法详解 - vue3

由于请求 A 比请求 B 响应慢,所以导致 finalData 最终赋值的是请求 A,所以我们必须把失效的副作用(将 A 的结果设置给 finalData)给清除掉。

侦听器回调提供了一个 onCleanup 参数,可解决这个问题。

onCleanup

onCleanup 接收一个回调函数,这个回调会在响应式依赖变更后,重新执行侦听器回调之前执行。

例子:

const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const branches = ['main', 'v2-compat']

const currentBranch = ref(branches[0])
const finalData, = ref(null)

const change = () => {
  currentBranch.value = currentBranch.value === "v2-compat" ? 'main' : 'v2-compat'
};

watchEffect(async (onCleanup) => {
  // 定义一个标志,代表当前副作用函数是否过期,默认 false,代表没有过期
  let expired = false;
  // 调用 onCleanup 函数,注册一个过期回调
  onCleanup(()=>{
    // 过期时,将 expired 赋值为 true
    expired = true
  })
  
  const url = `${API_URL}${currentBranch.value}`;
  const res = await (await fetch(url)).json();
  
  // 只有该副作用函数的执行没过期时,才会执行后续操作
  if(!expired) {
      finalData.value = res
  }
})

watch、watchEffect 用法详解 - vue3

现在,无论 currentBranch 改变多少次,每次请求快慢,finalData 最终设置的结果会是我们预期的最后一次请求的结果。

onCleanup 实现原理:

其实很简单,把通过 onCleanup 函数注册的过期回调存起来,响应式依赖变更后,在侦听器回调重新执行之前调用,仅此而已。

如下 源码 所示:

watch、watchEffect 用法详解 - vue3

watch、watchEffect 用法详解 - vue3

watch、watchEffect 用法详解 - vue3

侦听器停止的时候也会调用过期函数(组件卸载前也会调用,因为组件卸载就会停止侦听器)

上面图片中给 cleanup 赋值过期回调的时候,也同时赋值给了侦听器 onStop 属性,侦听器停止的时候会去调用该属性

watch、watchEffect 用法详解 - vue3

侦听器调试

侦听器配置对象中,onTrackonTrigger  选项可用于调试侦听器行为。

只会在开发模式下工作

onTrack:在响应式数据被追踪的时候被调用。

onTrigger: 在响应式依赖更改时被调用。

例子

const state = ref(0);
const person = reactive({
  count: 0,
});
const change = () => {
  state.value++;
  person.count++;
};
watchEffect(
  () => {
    state.value
    person.count
  },
  {
    onTrack(e) {
      console.log(e, "track");
    },
    onTrigger(e) {
      console.log(e, "trigger");
    }
  }
)

立即执行侦听器回调,获取响应式数据依赖,触发 onTrack

watch、watchEffect 用法详解 - vue3

调用 change,更改响应式数据依赖,触发 onTrigger

watch、watchEffect 用法详解 - vue3

watch

watch 可侦听特定的数据源,数据源改变会调用侦听器回调,默认只有数据源改变才会执行副作用函数, 不会立即调用副作用函数。

第一个参数,所侦听的数据源,该数据源可以是:

  • 一个 ref(包括计算属性)
  • 一个 reactive
  • 一个 getter 函数
  • 多个数据源组成的数组。

第二个参数,侦听器回调,该回调提供了三个参数

  1. 数据源新值,当侦听多个数据源时为数组,元素为数据源
  2. 数据源旧值,当侦听多个数据源时为数组,元素为数据源
  3. onCleanup(可清除过期的副作用函数)

第三个为可选参数,是一个配置对象:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,进行深层侦听。参考 深层侦听
  • flush:调整回调函数的刷新时机。参考 回调的刷新时机
  • onTrack / onTrigger:调试侦听器的依赖。参考 调试侦听器

侦听数据源

以下是侦听各种数据源,和侦听器回调提供的数据源新、旧值用例。(onCleanup 上面已经介绍过了

例子

const count = ref(0);
const panda = reactive({ foo: 0 });
// 直接侦听 ref
watch(count, (newV, oldV) => {
  console.log("count: ", oldV, '=>', newV);
});
// 直接侦听 reactive
watch(panda, (newV, oldV) => {
  console.log("panda: ", oldV, '=>', newV);
});
// 侦听一个 getter 函数
watch(
  () => panda.foo,
  (newV, oldV) => {
    console.log("panda: ", oldV, '=>', newV);
  }
);
// 侦听多个数据源
watch(
  [count, () => panda.foo],
  ([count, foo], [preCount,preFoo]) => {
    console.log("count and panda: ", count, foo, '=>', preCount, preFoo);
  }
);

配置对象

这里主要讲 immediate 属性和 deep 属性,flushonTrack / onTrigger 可参考上文介绍。

立即执行 immediate

在侦听器创建时立即触发回调。第一次调用时旧值是 undefined

const count = ref(0);
watch(count, (newV, oldV) => {
  console.log("count: ", oldV, '=>', newV);
}, {
  immediate: true
});

watch、watchEffect 用法详解 - vue3

深层侦听器 deep

深度侦听的 watch 会递归跟踪数据源的所有属性。以下情况会触发侦听器深层侦听:

  • 直接侦听一个 reactive
  • 配置对象有 deep 属性

例子

const panda = reactive({
  foo: {
    bar: 0
  },
});
const change = () => {
  panda.foo.bar++;
};

watch(panda, (newV, oldV) => {
  console.log("panda: ", oldV, '=>', newV);
});

watch、watchEffect 用法详解 - vue3

例子中,直接侦听一个 reative,并且没有配置对象,调用 change 的时候,会触发侦听器回调。

除了 reative 外的侦听数据源,想要深层侦听,都必须添加配置对象 deep 属性值为 true

// 深层侦听 ref
const panda = ref({
  foo: 0,
});
const change = () => {
  panda.value.foo++;
};

watch(panda, (newV, oldV) => {
  console.log("panda: ", oldV, '=>', newV);
}, {
  deep: true
});

// 深层侦听 getter 函数
const deer = reactive({
  foo: 0
});
const change2 = () => {
  deer.foo++
};

watch(() => deer, (newV, oldV) => {
  console.log("deer: ", oldV, '=>', newV);
}, {
  deep: true
});

下图来自 vue 源码

watch、watchEffect 用法详解 - vue3

深层侦听原理,其实很简单,当有侦听器回调和 deep: true 的时候,调用一个 traverse 函数,传递侦听数据源 source,递归获取 source 的所有属性,触发属性访问器 getter,达到跟踪效果。如下图:

watch、watchEffect 用法详解 - vue3

watch、watchEffect 用法详解 - vue3

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

新值和旧值为同一引用

当侦听数据源为对象,并且是深层侦听的时候,属性的变更,会发生侦听器回调给予的新值和旧值为同一引用的问题。

解决方法:通过拷贝一个新对象侦听此新对象,并使用 getter 写法。

const deer = reactive({
  foo: 0
});
const change = () => {
  deer.foo++
};

watch(deer, (newV, oldV) => {
  console.log("deer: ", oldV === newV);
});

watch(() => JSON.parse(JSON.stringify(deer)), (newV, oldV) => {
  console.log("deer2: ", oldV === newV);
});

watch、watchEffect 用法详解 - vue3

源码:

watch、watchEffect 用法详解 - vue3

watchEffect vs. watch

什么时候使用 watchEffect ,而又什么时候使用 watch 呢?

需要以下需求的请使用 watch

  • 更加明确是应该由哪个状态触发侦听器重新执行

  • 访问监听数据变化前后状态

需要以下需求的请使用 watchEffect

  • 有多个依赖项时,因为当有多个依赖项时,watch 传入一个数组,增加了得手动维护这个依赖列表的负担。

  • 需要侦听一个嵌套数据结构中的其中几个属性时,watchEffect 会比添加 options.deepwatch 有效。因为 watchEffect 只跟踪回调中使用到的属性,而深度侦听的 watch 会递归跟踪所有的属性。(上面有解释

转载自:https://juejin.cn/post/7235547967112167461
评论
请登录