组件生命周期

image.png

周期钩子函数

vue3 已经不使用destroy()
8. 组件生命周期、nexttick、组件 v-model、混入 Mixin - 图2

nexttick

nexttick 可以让回调函数在 dom 更新操作完成后才执行,dom 更新完立即执行。

  • tick 时钟指针转动发出一下滴答声,表示时间走动,进入下一轮的意思。

比如我们有下面的需求:

  • 点击一个按钮,我们会修改在h2中显示的message;
  • message被修改后撑大了 h2,我们要获取 h2 被撑大后的高度;

存在的问题:
如果我们修改 message 后,直接去获取修改后的高度,高度将为 0,获取不到修改后的高度。再次修改 message 后,那就能获取到第一次修改 message 时,h2 变化后的高度。

实现上面的案例我们有三种方式:
方式一:在点击按钮后立即获取到h2的高度(错误的做法)
方式二:在updated生命周期函数中获取h2的高度(但是其他数据更新,也会执行该操作)
方式三:使用nexttick函数;

  1. <template>
  2. <div>
  3. <h2>{{counter}}</h2>
  4. <button @click="increment">+1</button>
  5. <h2 class="title" ref="titleRef">{{message}}</h2>
  6. <button @click="addMessageContent">添加内容</button>
  7. </div>
  8. </template>
  9. <script>
  10. import { ref, onUpdated, nextTick } from "vue";
  11. export default {
  12. setup() {
  13. const message = ref("")
  14. const titleRef = ref(null)
  15. const counter = ref(0)
  16. const addMessageContent = () => {
  17. message.value += "哈哈哈哈哈哈哈哈哈哈"
  18. // console.log(titleRef.value.offsetHeight) // 直接获取失败
  19. // 放入微任务队列底部,等更新DOM后再执行获取操作
  20. nextTick(() => {
  21. console.log(titleRef.value.offsetHeight)
  22. })
  23. }
  24. const increment = () => {
  25. for (let i = 0; i < 100; i++) {
  26. counter.value++
  27. }
  28. }
  29. onUpdated(() => {
  30. })
  31. return {
  32. message,
  33. counter,
  34. increment,
  35. titleRef,
  36. addMessageContent
  37. }
  38. }
  39. }
  40. </script>
  41. <style scoped>
  42. .title {
  43. width: 120px;
  44. }
  45. </style>

nexttick 的原理

vue 内部维持了好几个队列来执行操作,比如部分周期钩子函数,组件更新,watch 回调等等,并且这些队列中的任务都会被添加到微任务队列中执行。
比如我们更新了 dom 中的数据,然后 vue 监听到变化,开始响应自己的任务队列进行处理,任务最终被添加到微任务队列。此时我们去获取 dom 更新后的数据,是获取不到的。因为获取 dom 数据是个同步任务,立即执行。此时微任务队列中的 vue 任务还没执行,所以获取不到。
而 nexttick 可以将本是同步任务的操作放入微任务队列的底部,这样就能在 vue 响应操作完成后再去获取数据,就能获取到了。

为什么 vue 要这么设计,将任务全添加到微任务队列?

因为可以提高性能。
比如我 for 循环更新100次 dom 内容,vue 的响应操作会执行 100 次吗?不会的,因为更新 dom 数据的 100 次操作是同步任务,会在微任务之前执行,所以 vue 的响应操作会等 100 次更新完,直接拿结果,然后开始响应操作,只执行了一次。这样就提高了效率。

组件的 v-model

前面我们在 dom 元素 input 中可以使用 v-model 来完成双向绑定:

  • 这个时候往往会非常方便,因为 v-model 默认帮助我们完成了两件事,这也是 v-model 的实现原理。
  • v-bind:value 的数据绑定和 @input 的事件监听,在事件回调函数中完成了数据从模板到实例的过程;

其实我们也可以在组件中使用 v-model。组件上的 v-model 自动绑定了modelValue属性和自动监听了update事件。

  1. <template>
  2. <div>
  3. <!-- dom 元素上使用 v-model -->
  4. <!-- <input v-model="message">
  5. <input :value="message" @input="message = $event.target.value"> -->
  6. <!-- 组件上使用 v-model -->
  7. <!-- <hy-input v-model="message"></hy-input> -->
  8. <hy-input :modelValue="message" @update:model-value="message = $event"></hy-input>
  9. </div>
  10. </template>

input dom 元素上使用 v-model,实际是监听 input 事件,事件响应函数中通过$event.target.value将输入的值传给绑定的属性 value,完成了视图到 data 的数据流动。
而组件标签上没有 target 属性,直接使用$event接收子组件传来的数据。

子组件就像 input 输入框,父组件就像 data。输入框接收 data 中绑定 value 的值,同样的子组件也要接收父组件的值,所以父向子传值,完成了数据从 data 流向视图的过程。
父组件中监听了 update 事件,子组件中可以触发 update 事件,也就是子向父传值,完成了数据从视图流向 data 的过程。
这样就实现了组件之间的双向数据绑定,组件上的 v-model 依然是个语法糖,帮我们绑定了变量,监听了事件。

  1. <template>
  2. <div>
  3. <home :modelValue="message" @update:model-value="message = $event"></home>
  4. <h2> 父组件:{{ message }}</h2>
  5. </div>
  6. </template>
  7. <script>
  8. import home from './components/home.vue';
  9. export default {
  10. components: { home },
  11. data() {
  12. return {
  13. message: 'hhh'
  14. }
  15. }
  16. }
  17. </script>
  18. ------home------
  19. <template>
  20. <div>
  21. <h2>父组件传来的值:{{modelValue}}</h2>
  22. <input type="text" :value="modelValue" @keyup.enter="inputenter">
  23. </div>
  24. </template>
  25. <script>
  26. export default {
  27. props: ['modelValue'],
  28. emits: ['update:modelValue'],
  29. methods: {
  30. inputenter(event) {
  31. // 将子组件的值传给父组件
  32. this.$emit('update:modelValue', event.target.value)
  33. }
  34. },
  35. }
  36. </script>
  1. <template>
  2. <div>
  3. <h2> 父组件:{{ message }}</h2>
  4. <home v-model="message"></home>
  5. </div>
  6. </template>
  7. <script>
  8. import home from './components/home.vue';
  9. export default {
  10. components: { home },
  11. data() {
  12. return {
  13. message: 'hhh'
  14. }
  15. }
  16. }
  17. </script>

父组件如上,直接 v-model 很简单。并且背后将 modelValue 属性传递给子组件了。

那子组件能不能直接 v-model 直接双向绑定 modelValue 呢?<input type="text" v-model="modelValue">
这是不行的,因为比如子组件中这个 input 输入框,v-model 自动监听了 input 事件,但是它自动监听 input 事件并响应的时候并没有触发父组件自动监听的 update:modelValue 事件。所以子组件的值无法传给父组件,它只是在改 modelValue 的值,这和父组件没有半毛钱关系;而且 props 中的值最好不要修改。

computed 实现

可以让子组件中的 v-model 绑定计算属性解决这个问题。
计算属性有读和写的操作,get、set。
v-model 中的 v-bind 肯定要读取计算属性,调用 get 方法,我们就可以在 get 方法中返回 modelValue,相当于实现了原生实现中的:value="modelValue",整体上子组件这个“input”,获取到了父组件这个“data”的数据。
而当输入框输入时,v-model 响应 input 事件,将输入值写入计算属性中,调用 set 方法,这个 set 方法就相当于我们手动响应的 input 事件函数,所以就可以在其中触发 update:modelValue 事件,并将数据传到父组件。

  1. <template>
  2. <div>
  3. <h2>父组件传来的值:{{modelValue}}</h2>
  4. <!-- 手动实现 v-model -->
  5. <!-- <input type="text" :value="modelValue" @keyup.enter="inputenter"> -->
  6. <!-- v-model 绑定计算属性 -->
  7. <input type="text" v-model="hhh">
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. props: ['modelValue'],
  13. emits: ['update:modelValue'],
  14. // methods: {
  15. // inputenter(event) {
  16. // this.$emit('update:modelValue', event.target.value)
  17. // }
  18. // },
  19. computed: {
  20. hhh: {
  21. set(value) {
  22. this.$emit('update:modelValue', value)
  23. },
  24. get() {
  25. return this.modelValue
  26. }
  27. }
  28. }
  29. }
  30. </script>

组件中绑定多个 v-model

一个组件中可以绑定多个 v-model,针对子组件不同区域进行双向数据绑定。
我们知道,默认情况下的 v-model 其实是绑定了 modelValue 属性和 @update:modelValue的事件;
如果我们希望绑定更多,可以给 v-model 传入一个参数,那么这个参数的名称就是我们绑定属性的名称;

  • v-model:名称="":相当于绑定了 名称 属性和监听了 update:名称 事件。
    1. <home v-model="message" v-model:ppt="info"> </home>
    v-model:title 相当于做了两件事:绑定了title属性;监听了 @update:title 的事件
    1. props: ['modelValue', 'ppt'],
    2. emits: ['update:modelValue', 'update:ppt'],

    混入 Mixin

    目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我
    希望对相同的代码逻辑进行抽取

注意:混入是公共代码的抽取;公共模板抽取目前还没有,除非封装成一个组件;vuex 是数据共享和公共代码抽取不一样。

在Vue2和Vue3中都支持的一种方式就是使用 Mixin 来完成:

  • 我们抽取公共的代码逻辑到一个 Mixin 对象中,一个 Mixin 对象可以包含任何组件选项,比如 data,methods 等;
  • 当组件导入使用 Mixin 对象时,所有 Mixin 对象的选项将被混合进入该组件本身的选项中,相当于代码的合并。
    1. export const demoMixin = {
    2. data() {
    3. return {
    4. message: "Hello DemoMixin"
    5. }
    6. },
    7. methods: {
    8. foo() {
    9. console.log("demo mixin foo");
    10. }
    11. },
    12. created() {
    13. console.log("执行了demo mixin created");
    14. }
    15. }
    ```html
``` ## Mixin 的合并规则 如果 Mixin 对象中的选项和组件对象中的选项内容存在相同,发生了冲突,那么Vue会如何操作呢?分情况考虑:
情况一:如果是 data 函数的返回值对象 - 返回值对象默认情况下会进行合并; - 如果 data 返回值对象的属性发生了冲突,那么**会保留组件自身的数据**; 情况二:如果是生命周期钩子函数 - 生命周期的钩子函数的内容会被合并到数组中,**都会被执行**; 情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。 - 比如都有 methods 选项,并且都定义了方法,那么它们都会生效; - 但是如果对象的 key 相同,那么会**取本组件对象的键值对**; ## 全局混入 Mixin 如果组件中的某些选项代码逻辑,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的 mixin:
全局的 Mixin 可以使用应用 app 的`mixin()` 方法来完成注册;一旦注册,那么全局混入的选项将会影响每一个组件; ```javascript import { createApp } from 'vue' import App from './App.vue' // 全局混入 app.mixin({ data() { return {} }, methods: { }, created() { console.log("全局的created生命周期"); } }); createApp(App).mount('#app') ``` # extends extends 允许声明扩展另外一个组件,类似于Mixin
extends 是早期 vue 实现公共代码抽取的方式,但是发现不够灵活就设计出了 mixin 。但是随着 vue3 composition API 的出现,混入也很少用了,都被扫入了历史的垃圾堆。 ```html

——app.vue——

```