岁岁年,碎碎念

Vue Doc 随手记 ——十-十三节

2023.08.04     832

如果好的老师,好的医生, 评价标准是金钱, 那么也就没必要区分任何职业了 你干嘛的,我赚钱的。 你学啥的,我学赚钱的。 —— 罗翔

十-十三节

  1. 生命周期
  2. 侦听器
  3. 模板引用
  4. 组件基础

生命周期

钩子函数 描述
beforeCreate 实例刚在内存中创建,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created 实例已经完全创建,数据观测 (data observer) 和 event/watcher 事件配置之后被调用。
beforeMount 实例在挂载之前被调用。相关的 render 函数首次被调用。
mounted 实例挂载到 DOM 后调用,对应 el 被新创建的 vm.$el 替换。
beforeUpdate 数据更新时调用,但是在虚拟 DOM 重新渲染和打补丁之前。可以在这里访问更新前的状态。
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed Vue 实例销毁后调用,清理工作应在这里进行。
errorCaptured 捕获子孙组件抛出的错误,不会向上冒泡。

侦听器 watch

watch 是一个用于监听 Vue 实例数据变化的选项。

通过使用 watch,你可以在数据发生变化时执行特定的逻辑操作,比如发送网络请求、更新其他数据、触发事件等。

watch 提供了一种响应式地监视数据变化的方式,使得你可以在数据状态改变时做出相应的反应。

watch 选项有两种用法:

  1. 直接在组件的选项中定义
  2. 是使用实例方法 $watch

1. 在组件选项中使用 watch:

export default {
  data() {
    return {
      message: '',
    };
  },
  watch: {
    message(newValue, oldValue) {
      // 当 this.message 改变时会执行这里的逻辑
      console.log('Message changed from', oldValue, 'to', newValue);
    },
  },
};

2. 使用实例方法 $watch

export default {
  data() {
    return {
      message: '',
    };
  },
  created() {
    // 使用 $watch 方法监视 message 的变化
    this.$watch('message', (newValue, oldValue) => {
      console.log('Message changed from', oldValue, 'to', newValue);
    });
  },
};

watch 选项可以监听一个或多个数据属性的变化,并在数据发生变化时执行特定的回调函数。这在很多场景下都很有用,例如:

  • 实时校验表单输入并显示错误信息。
  • 监听数据变化触发网络请求,实现自动搜索功能。
  • 响应数据的增删改操作,更新其他相关数据。
  • 监听路由参数的变化,根据参数变化更新页面内容。

需要注意的是,尽管 watch 提供了一种方便的方式来监视数据变化,但在某些情况下,你可能会更倾向于使用计算属性(computed)来实现类似的功能。计算属性可以更直接地响应数据变化并进行计算,但对于一些需要异步操作或监听多个数据的情况,使用 watch 更为合适。

侦听数据源类型

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}`)
})


const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})
// 正确,提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

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

即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

下面是一个表格形式的比较,展示了 watchwatchEffect 在不同方面的区别:

特性 watch watchEffect
定义方式 选项对象中定义,可以指定要监听的数据源 作为函数使用,内部访问响应式数据并自动追踪依赖关系
数据监听 需要明确指定要监听的数据源 自动捕获函数内部使用的响应式数据
灵活性 提供更精细的控制,可以指定多个选项 更简洁,适用于处理函数内的副作用
适用场景 监听特定的数据变化,需要精细控制选项 在函数内部处理响应式数据的变化和副作用

回调的触发时机

当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

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

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

watchEffect(callback, {
  flush: 'post'
})

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

停止侦听器

用的比较少

<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()

// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

模板引用

模板引用(Template Refs)是 Vue 中的一个特性,它允许你在模板中使用 $refs 对象来引用组件或 DOM 元素。通过模板引用,你可以在 Vue 组件中直接操作 DOM 元素或其他组件实例,而不必通过选择器或其他方式进行查找。

在 Vue 中,通过在模板中使用 ref 特性来创建模板引用。ref 特性可以应用在普通 HTML 元素上,也可以应用在组件上。

以下是使用模板引用的示例:

<template>
  <div>
    <button ref="myButton" @click="changeText">Click me</button>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Initial text',
    };
  },
  methods: {
    changeText() {
      // 通过 this.$refs.myButton 访问 DOM 元素
      this.$refs.myButton.innerText = 'Text changed';
      // 修改组件中的数据
      this.message = 'Text changed';
    },
  },
};
</script>

在上面的示例中,我们通过 ref 特性给 <button> 元素创建了一个模板引用 myButton。然后,在组件的方法中,我们可以通过 this.$refs.myButton 来访问这个 DOM 元素,并修改其文本内容。

需要注意的是,模板引用只在组件渲染完成后才能访问。也就是说,当组件的模板渲染完毕后,$refs 对象中才会包含模板引用。

此外,模板引用也可以应用在组件上,类似地,你可以通过模板引用来直接访问和调用组件实例的方法和属性。

<template>
  <div>
    <my-component ref="myComponentRef"></my-component>
    <button @click="callComponentMethod">Call Component Method</button>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue';

export default {
  components: {
    MyComponent,
  },
  methods: {
    callComponentMethod() {
      // 通过 this.$refs.myComponentRef 访问子组件实例
      this.$refs.myComponentRef.myMethod();
    },
  },
};
</script>

如果一个子组件使用的是选项式 API 或没有使用 ,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。

有一个例外的情况,使用了 的组件是默认私有的:一个父组件无法访问到一个使用了 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

v-for 中的模板引用

<script setup>
import { ref, onMounted } from 'vue'

const list = ref([
  /* ... */
])

// ref 数组并不保证与源数组相同的顺序
const itemRefs = ref([])

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>

组件基础

示例

<!-- ParentComponent.vue -->
<template>
  <div>
    <h2>Parent Component</h2>
    <p>Counter Value: {{ counter }}</p>
    <button @click="incrementCounter">Increment Counter</button>
    <child-component :message="message" @childEvent="handleChildEvent" />
    <p>Computed Reversed Message: {{ reversedMessage }}</p>
  </div>
</template>

<script>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent,
  },
  setup() {
    const counter = ref(0);
    const message = ref('Hello from parent');

    const incrementCounter = () => {
      counter.value++;
    };

    const handleChildEvent = (dataFromChild) => {
      console.log('Received from child:', dataFromChild);
      message.value = 'Updated from child';
    };

    const reversedMessage = computed(() => message.value.split('').reverse().join(''));

    onMounted(() => {
      console.log('Parent Component mounted');
    });

    onBeforeUnmount(() => {
      console.log('Parent Component before unmount');
    });

    return {
      counter,
      message,
      incrementCounter,
      handleChildEvent,
      reversedMessage,
    };
  },
};
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h3>Child Component</h3>
    <p>Received Message: {{ message }}</p>
    <button @click="sendToParent">Send to Parent</button>
  </div>
</template>

<script>
import { ref, emit, onMounted } from 'vue';

export default {
  props: {
    message: String,
  },
  setup(props) {
    const sendToParent = () => {
      const dataToSend = 'Data from child';
      emit('childEvent', dataToSend);
    };

    onMounted(() => {
      console.log('Child Component mounted');
    });

    return {
      sendToParent,
    };
  },
};
</script>