MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue中生命周期钩子的全面解读

2022-09-016.1k 阅读

Vue 生命周期概述

在 Vue 开发中,生命周期钩子函数是非常重要的概念。Vue 实例从创建到销毁的过程,就是它的生命周期。在这个过程中,Vue 提供了一系列的生命周期钩子函数,让开发者可以在特定的阶段执行自定义的代码逻辑。这些钩子函数贯穿于实例的整个生命周期,使得开发者能够更好地控制和管理组件的行为。

Vue 实例有一个完整的生命周期,从开始创建、初始化数据、编译模板、挂载 DOM、数据更新时重新渲染,到最后销毁实例。每一个阶段都有对应的生命周期钩子函数,通过在这些钩子函数中编写代码,我们可以实现不同的功能需求,比如在组件创建时发起数据请求,在组件销毁时清理定时器等。

生命周期钩子函数分类

Vue 的生命周期钩子函数可以大致分为以下几类:创建前后、挂载前后、更新前后、销毁前后。下面我们将详细介绍每一类钩子函数的特点和用途。

创建前后钩子函数

  1. beforeCreate
    • 触发时机:在 Vue 实例初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用。此时,实例虽然已经被创建,但是 data 和 methods 等属性还未被初始化,所以在这个钩子函数中无法访问到 data 中的数据和 methods 中的方法。
    • 用途:一般很少在这个钩子函数中编写具体的业务逻辑。不过,它可以用于一些初始化操作,比如在这个阶段可以添加一些自定义的事件监听器等,为后续的实例化过程做准备。
    • 代码示例
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    }
  },
  beforeCreate() {
    console.log('beforeCreate 钩子被调用');
    // 这里无法访问 data 中的 message
    console.log(this.message); // undefined
  }
}
</script>
  1. created
    • 触发时机:在实例创建完成后被立即调用。此时,data 和 methods 等属性已经被初始化,我们可以访问到 data 中的数据和 methods 中的方法,并且可以在这个钩子函数中进行一些数据的初始化操作,比如发起异步数据请求。
    • 用途:通常用于在组件创建后,需要立即执行的操作,比如获取初始数据。由于这个阶段 DOM 还未挂载,所以不能直接操作 DOM。
    • 代码示例
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  },
  created() {
    console.log('created 钩子被调用');
    // 模拟异步数据请求
    setTimeout(() => {
      this.message = 'Data fetched successfully';
    }, 1000);
  }
}
</script>

挂载前后钩子函数

  1. beforeMount
    • 触发时机:在挂载开始之前被调用。此时,render 函数首次被调用,生成虚拟 DOM,但真实的 DOM 还未被创建并插入到页面中。在这个阶段,我们可以对即将挂载的模板进行最后的修改。
    • 用途:如果需要在挂载之前对模板进行一些预处理操作,比如修改模板的结构或者添加一些自定义的属性等,可以在这个钩子函数中进行。
    • 代码示例
<template>
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Before mount'
    }
  },
  beforeMount() {
    console.log('beforeMount 钩子被调用');
    // 可以对模板进行一些操作
    const div = document.createElement('div');
    div.textContent = 'Additional content';
    this.$el.appendChild(div);
  }
}
</script>
  1. mounted
    • 触发时机:在实例被挂载后调用,此时 el 被新创建的 vm.$el 替换,并挂载到了实例的 $el 上。也就是说,真实的 DOM 已经被创建并插入到页面中,我们可以通过 $el 或者其他 DOM 操作库(如 jQuery)来操作 DOM 元素。
    • 用途:常用于需要操作 DOM 的场景,比如初始化第三方插件(如 Chart.js 绘制图表),或者对 DOM 元素进行事件绑定等。
    • 代码示例
<template>
  <div id="app">
    <p id="message">{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Mounted successfully'
    }
  },
  mounted() {
    console.log('mounted 钩子被调用');
    const messageElement = document.getElementById('message');
    messageElement.style.color ='red';
  }
}
</script>

更新前后钩子函数

  1. beforeUpdate
    • 触发时机:在数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。此时,数据已经发生了变化,但 DOM 还未更新,我们可以在这个钩子函数中获取到更新前的数据状态。
    • 用途:如果需要在数据更新但 DOM 未更新之前执行一些操作,比如记录更新前的数据,或者进行一些数据的预处理,可以在这个钩子函数中实现。
    • 代码示例
<template>
  <div>
    <input v-model="message">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Initial value'
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate 钩子被调用');
    console.log('更新前的 message:', this.message);
  }
}
</script>
  1. updated
    • 触发时机:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时,DOM 已经更新为最新的数据状态,我们可以在这个钩子函数中操作更新后的 DOM。
    • 用途:常用于在 DOM 更新后执行一些副作用操作,比如根据更新后的 DOM 状态重新计算某些布局,或者触发一些基于新 DOM 状态的动画效果等。但需要注意的是,在这个钩子函数中再次修改数据可能会导致无限循环更新,因为 updated 钩子函数在数据更新时会被再次触发。
    • 代码示例
<template>
  <div>
    <input v-model="message">
    <p :id="messageId">{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Initial value',
      messageId: 'initial - id'
    }
  },
  updated() {
    console.log('updated 钩子被调用');
    const messageElement = document.getElementById(this.messageId);
    messageElement.style.fontWeight = 'bold';
  }
}
</script>

销毁前后钩子函数

  1. beforeDestroy
    • 触发时机:在实例销毁之前调用。此时,实例仍然完全可用,我们可以在这个钩子函数中进行一些清理工作,比如清除定时器、解绑事件监听器等,以避免内存泄漏。
    • 用途:常用于释放资源和清理操作,确保在组件销毁后不会留下不必要的引用或任务。
    • 代码示例
<template>
  <div>
    <button @click="destroyComponent">Destroy Component</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      timer: null
    }
  },
  created() {
    this.timer = setInterval(() => {
      console.log('Timer is running');
    }, 1000);
  },
  beforeDestroy() {
    console.log('beforeDestroy 钩子被调用');
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
  methods: {
    destroyComponent() {
      this.$destroy();
    }
  }
}
</script>
  1. destroyed
    • 触发时机:在实例销毁之后调用。此时,所有的指令都被解绑,所有的事件监听器都被移除,所有的子实例也都被销毁。在这个钩子函数中,实例已经不再可用,一般用于记录日志或者进行一些最终的清理确认操作。
    • 用途:可以用于确认组件已经成功销毁,并且所有相关资源都已清理完毕。虽然在这个阶段无法对实例进行太多操作,但可以用于一些提示性的输出或者简单的状态记录。
    • 代码示例
<template>
  <div>
    <button @click="destroyComponent">Destroy Component</button>
  </div>
</template>

<script>
export default {
  beforeDestroy() {
    console.log('beforeDestroy 钩子被调用');
  },
  destroyed() {
    console.log('destroyed 钩子被调用');
  },
  methods: {
    destroyComponent() {
      this.$destroy();
    }
  }
}
</script>

父子组件生命周期钩子执行顺序

  1. 加载渲染过程
    • 父组件 beforeCreate
    • 父组件 created
    • 父组件 beforeMount
    • 子组件 beforeCreate
    • 子组件 created
    • 子组件 beforeMount
    • 子组件 mounted
    • 父组件 mounted
    • 代码示例:
<!-- Parent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent />
  </div>
</template>

<script>
import ChildComponent from './Child.vue';

export default {
  components: {
    ChildComponent
  },
  beforeCreate() {
    console.log('Parent beforeCreate');
  },
  created() {
    console.log('Parent created');
  },
  beforeMount() {
    console.log('Parent beforeMount');
  },
  mounted() {
    console.log('Parent mounted');
  }
}
</script>
<!-- Child.vue -->
<template>
  <div>
    <h2>Child Component</h2>
  </div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('Child beforeCreate');
  },
  created() {
    console.log('Child created');
  },
  beforeMount() {
    console.log('Child beforeMount');
  },
  mounted() {
    console.log('Child mounted');
  }
}
</script>
  1. 子组件更新过程
    • 父组件 beforeUpdate
    • 子组件 beforeUpdate
    • 子组件 updated
    • 父组件 updated
    • 代码示例:
<!-- Parent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent :message="parentMessage" />
    <button @click="updateParentMessage">Update Parent Message</button>
  </div>
</template>

<script>
import ChildComponent from './Child.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: 'Initial value'
    }
  },
  beforeUpdate() {
    console.log('Parent beforeUpdate');
  },
  updated() {
    console.log('Parent updated');
  },
  methods: {
    updateParentMessage() {
      this.parentMessage = 'Updated value';
    }
  }
}
</script>
<!-- Child.vue -->
<template>
  <div>
    <h2>Child Component: {{ message }}</h2>
  </div>
</template>

<script>
export default {
  props: ['message'],
  beforeUpdate() {
    console.log('Child beforeUpdate');
  },
  updated() {
    console.log('Child updated');
  }
}
</script>
  1. 父组件销毁过程
    • 父组件 beforeDestroy
    • 子组件 beforeDestroy
    • 子组件 destroyed
    • 父组件 destroyed
    • 代码示例:
<!-- Parent.vue -->
<template>
  <div>
    <h1>Parent Component</h1>
    <ChildComponent />
    <button @click="destroyParentComponent">Destroy Parent Component</button>
  </div>
</template>

<script>
import ChildComponent from './Child.vue';

export default {
  components: {
    ChildComponent
  },
  beforeDestroy() {
    console.log('Parent beforeDestroy');
  },
  destroyed() {
    console.log('Parent destroyed');
  },
  methods: {
    destroyParentComponent() {
      this.$destroy();
    }
  }
}
</script>
<!-- Child.vue -->
<template>
  <div>
    <h2>Child Component</h2>
  </div>
</template>

<script>
export default {
  beforeDestroy() {
    console.log('Child beforeDestroy');
  },
  destroyed() {
    console.log('Child destroyed');
  }
}
</script>

深入理解生命周期钩子的执行原理

Vue 的生命周期钩子函数的执行是基于 Vue 的响应式系统和虚拟 DOM 机制。当一个 Vue 实例被创建时,Vue 会首先进行初始化操作,包括数据观测、事件和 watcher 的配置等。在这个过程中,beforeCreate 和 created 钩子函数会按照顺序被调用。

在挂载阶段,Vue 会根据模板生成虚拟 DOM,并通过 diff 算法将虚拟 DOM 与真实 DOM 进行对比和更新。在挂载开始前,beforeMount 钩子函数被调用,此时虚拟 DOM 已经生成但真实 DOM 还未挂载。当虚拟 DOM 被挂载到真实 DOM 上后,mounted 钩子函数被调用。

当数据发生变化时,Vue 的响应式系统会检测到变化,并触发重新渲染。在重新渲染之前,beforeUpdate 钩子函数被调用,此时数据已经变化但 DOM 还未更新。重新渲染完成后,updated 钩子函数被调用。

在销毁阶段,Vue 会逐步解除绑定、移除事件监听器和销毁子实例等操作。在销毁之前,beforeDestroy 钩子函数被调用,允许开发者进行清理工作。销毁完成后,destroyed 钩子函数被调用。

生命周期钩子的实际应用场景

  1. 数据请求 在 created 钩子函数中发起异步数据请求是非常常见的场景。因为在这个阶段,实例已经创建且 data 和 methods 都已初始化,同时又不需要操作 DOM,正好适合进行数据获取操作。例如,在一个博客列表组件中,可以在 created 钩子函数中请求服务器获取博客文章列表数据,并将其赋值给 data 中的属性,以便在模板中进行展示。
  2. DOM 操作与第三方插件集成 在 mounted 钩子函数中进行 DOM 操作和第三方插件的初始化是很合适的。比如,要在页面中使用百度地图插件,就可以在 mounted 钩子函数中初始化地图实例,并设置地图的相关参数和事件。
  3. 资源清理 在 beforeDestroy 钩子函数中进行资源清理工作,如清除定时器、解绑事件监听器等。这可以避免在组件销毁后,这些资源仍然占用内存,从而导致内存泄漏问题。例如,在一个轮播图组件中,如果使用了定时器来自动切换图片,那么在组件销毁前,需要在 beforeDestroy 钩子函数中清除该定时器。

注意事项

  1. 避免在 updated 钩子中修改数据导致无限循环 由于 updated 钩子函数在数据更新时会被再次触发,所以在这个钩子函数中如果不小心再次修改数据,可能会导致无限循环更新,使页面陷入卡顿甚至崩溃。因此,在 updated 钩子函数中操作数据时要格外小心,确保数据的修改是有条件且不会引发重复更新的。
  2. 父子组件生命周期钩子执行顺序的影响 在开发过程中,要清楚父子组件生命周期钩子的执行顺序,特别是在进行一些依赖于组件状态的操作时。比如,如果父组件需要在子组件完全挂载后再执行某些操作,就需要利用子组件的 mounted 钩子函数来进行通信和协作。

总之,熟练掌握 Vue 的生命周期钩子函数,对于编写高效、稳定且易于维护的前端应用至关重要。通过合理运用这些钩子函数,我们可以更好地控制组件的行为,实现复杂的业务逻辑,并提升用户体验。无论是简单的页面渲染,还是大型的单页面应用开发,生命周期钩子函数都发挥着不可或缺的作用。开发者应该根据具体的业务需求,准确选择合适的生命周期钩子函数,并在其中编写恰当的代码逻辑,以达到最佳的开发效果。