Vue 生命周期

文章目录

  • 一、Vue2的生命周期函数
    • Vue2请求接口一般放在哪个生命周期
    • Vue2生命周期运行解析
    • 源码解析
      • 初始化阶段
      • 模板编译阶段
      • 挂载阶段
        • mountComponent()
        • Watcher
        • _update()
      • 销毁阶段
        • 什么时候会调用 $destroy()
  • 二、Vue3的生命周期函数
    • setup函数
    • onBeforeMount
    • onMounted
    • onBeforeUpdate
    • onUpdated
    • onBeforeUnmount
    • onUnmounted

一、Vue2的生命周期函数

Vue 生命周期_第1张图片

Vue2.0的生命周期钩子一共有10个分别简单介绍如下:

  • beforeCreate:在实例初始化之后,数据监听(data observer) 和 event/watcher 事件配置之前被调用。
  • created:实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据监听(data observer),属性和方法的运算, watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
  • beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
  • mounted:当Vue实例被创建并挂载到DOM元素上时,mounted立即执行。可以通过this.$el访问挂载的DOM元素,进行一些依赖于DOM的操作,比如添加事件监听器、获取数据等
  • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
  • activatedkeep-alive 组件激活时调用。
  • deactivatedkeep-alive 组件停用时调用。
  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue2请求接口一般放在哪个生命周期

在Vue中,请求接口通常放在createdmounted生命周期钩子函数中。

created钩子函数在实例创建之后、挂载之前被调用,此时还没有挂载到DOM上,适合做一些不依赖于DOM的操作,比如数据请求。

mounted钩子函数在实例挂载到DOM上之后被调用,此时已经完成了DOM的渲染,适合做一些依赖于DOM的操作,比如初始化组件内的某些DOM元素。

根据具体情况,你可以选择在createdmounted钩子函数中发起数据请求。

Vue2生命周期运行解析

我们创建了一个 app 的Vue根实例,将其挂载到页面 id 为 app 的 Dom 元素上。
然后局部注册了一个组件名为 haohao 并在根实例中将其注册,使其可以在根实例的作用域中使用。将子组件用 包裹,为接下来的测试作准备。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Vue-LifeClyle</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
    <div id="app" class="jing">
        <p>{{message}}</p>
        <keep-alive>
            <jh-component msg="2017年6月9日" v-if="show"></jh-component>
        </keep-alive>
    </div>
</body>
<script>
    var haohao = {
        template: '
from haohao: {{msg}}
'
, props: ['msg'], deactivated: function() { console.log('component deactivated!'); }, activated: function() { console.log('component activated'); } }; var app = new Vue({ el: '#app', data: function() { return { message: 'jingjing', show: true //控制子组件是否显示 }; }, beforeCreate: function() { console.group('beforeCreate Vue实例创建前的状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(state); }, created: function() { console.group('created Vue实例创建完毕后状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(state); }, beforeMount: function() { console.group('beforeMount 挂载前状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); }, mounted: function() { console.group('mounted 挂载后状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); }, beforeUpdate: function() { console.group('beforeUpdate 更新前状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); console.log('beforeUpdate = ' + document.getElementsByTagName('p')[0].innerHTML); }, updated: function() { console.group('updated 更新完成状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); console.log('Updated = ' + document.getElementsByTagName('p')[0].innerHTML); }, beforeDestroy: function() { console.group('beforeDestroy 销毁前状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); }, destroyed: function() { console.group('destroyed 销毁完成状态————————————————————'); var state = { 'el': this.$el, 'data': this.$data, 'message': this.message } console.log(this.$el); console.log(state); }, components: { 'jh-component': haohao } }); </script> <style> .jing { font-size: 50px; font-weight: bolder; } </style> </html>

1、beforeCreatecreated
Vue 生命周期_第2张图片

  • beforeCreate 执行时:datael均未初始化,值为undefined
  • created 执行时:Vue 实例观察的数据对象 data 已经配置好,已经可以得到app.message的值,但 Vue 实例使用的根 DOM 元素el还未初始化

2、beforeMountmountedactivateddeactivated
Vue 生命周期_第3张图片

  • beforeMount 执行时:datael均已经初始化,但从{{message}} 的展示情况可以看出此时 el 并没有渲染数据,这里就是应用的 Virtual DOM(虚拟Dom)技术,先把坑占住了。到后面 mounted 挂载的时候再把值渲染上去
  • mounted 执行时:此时 el 已经渲染完成并挂载到实例上
  • 在控制台看到component activated被打印出来了,说明子组件jh-component 包裹,随 el 的挂载而触发了。

然后我们进行一些操作,在控制台输入 app.show = false我们再来看看有什么变化,测试结果如下图:
Vue 生命周期_第4张图片

  • 因为我们在这里修改了data的值,所以会触发beforeUpdateupdated钩子函数,我们看到deactivated钩子已经触发,表示已经停用。

3、beforeUpdateupdated

beforeUpdateupdated触发时,el中的数据都已经渲染完成,但根据控制台打印的信息可知,只有当updated钩子被调用时候,组件dom才会被更新。

Vue 生命周期_第5张图片

源码解析

初始化阶段

这个阶段做的第一件事,就是用 new 创建一个 Vue 实例对象

new Vue({
    el:'#app',
    store,
    router,
    render: h => h(App)
})

能用 new 那肯定是有一个构造函数的,我们来看一下

源码地址:src/core/instance/index.js - 8行

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
initMixin(Vue)

_init()源码地址:src/core/instance/init.js - 15行
这里去掉了一些环境判断的,主要流程就是

  • 合并配置,主要是把一些内置组件directivefilter、本文最开始的钩子函数名称列表等合并到 Vue.options上面
  • 调用一些初始化函数,这里具体初始化了哪些东西我写在注释里了
  • 触发生命周期钩子,beforeCreatecreated
  • 最后调用 $mount 挂载 进入下一阶段
export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    // 保存当前实例
    const vm: Component = this
    // 合并配置
    if (options && options._isComponent) {
      // 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$options
      initInternalComponent(vm, options)
    } else {
      // 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数

    if (vm.$options.el) {
      // 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
      // 如果没有传就需要手动执行 $mount 才会进入下一阶段
      vm.$mount(vm.$options.el)
    }
  }
}

模板编译阶段

Vue 生命周期_第6张图片
$mount()源码地址:dist/vue.js - 11927行

  Vue.prototype.$mount = function ( el, hydrating ) {
    el = el && query(el);
    var options = this.$options;
    // 如果没有 render 
    if (!options.render) {
      var template = options.template;
      // 再判断,如果有 template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          return this
        }
      // 再判断,如果有 el
      } else if (el) {
        template = getOuterHTML(el);
      }
    }
    return mount.call(this, el, hydrating)
  };

$mount 主要就是判断要不要编译,使用哪一个模板编译,需要注意的就是判断顺序了,我们来看一下这段代码

<div id='app'>
    <p>{{ name }}</p>
</div>
<script>
    new Vue({
        el:'#app',
        data:{ name:'123' },
        template:'
掘金
'
, render(h){ return h('div', {}, '好好学习,天天向上') } }) </script>

结合源码马上就知道会渲染出来的只有

好好学习,天天向上

因为源码中是优先判断 render 是否存在,如果存在,就直接使用 render 函数了

如果没有,再判断 templateel,如果有 template,就不会管 el

所以优先级顺序是:render > template > el,因为不管是 el 挂载的,还是 template 最后都会被编译成 render 函数

挂载阶段

Vue 生命周期_第7张图片
如图也可以看得出来,这里阶段主要做的事有两件:

  1. 根据 render 返回的虚拟 DOM 创建真实的 DOM 节点,插入到视图中,完成渲染
  2. 对模板中数据或状态做响应式处理
mountComponent()

这里主要做的事就是

  • 调用钩子函数 beforeMount
  • 调用 _update() 方法对新老虚拟 DOM 进行 patch 以及 new Watcher 对模板数据做响应式处理
  • 再调用钩子函数 mounted

源码地址:src/core/instance/lifecycle.js - 141行

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 判断有没有渲染函数 render
  if (!vm.$options.render) {
    // 如果没有,默认就创建一个注释节点
    vm.$options.render = createEmptyVNode
  }
  // 调用生命周期钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
  new Watcher(vm, updateComponent, noop, {
    // 当触发更新的时候,会在更新之前调用
    before () {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  // 没有老的 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher

关于响应式原理

_update()

有关 Diff 算法源码的完整流程剖析

源码地址:src/core/instance/lifecycle.js - 59行

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el // 当前组件根节点
    const prevVnode = vm._vnode // 老的 vnode
    vm._vnode = vnode // 更新老的 vnode
    // 如果是首次渲染
    if (!prevVnode) {
      // 对 vnode 进行 patch 创建真实的 DOM,挂载到 vm.$el 上
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 修改的时候,进行新老 vnode 对比,并回修改后的真实 DOM
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // 删除老根节点的引用
    if (prevEl) prevEl.__vue__ = null
    // 更新当前根节点的引用
    if (vm.$el) vm.$el.__vue__ = vm
    // 更新父级的引用
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

销毁阶段

Vue 生命周期_第8张图片
$destroy()这个阶段比较简单,源码不多,主要就是:

  • 调用生命周期钩子函数 beforeDestory
  • 从父组件中删除当前组件
  • 移除当前组件内的所有观察者(依赖追踪),删除数据对象的引用,删除虚拟 DOM
  • 调用生命周期钩子函数 destoryed
  • 关闭所有事件监听,删除当前根组件的引用,删除父级的引用

源码地址:src/core/instance/lifecycle.js - 97行

Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 如果实例正在被销毁的过程中,直接跳过
    if (vm._isBeingDestroyed) {
      return
    }
    // 调用生命周期钩子函数
    callHook(vm, 'beforeDestroy')
    // 更新销毁过程状态
    vm._isBeingDestroyed = true
    // 获取父级
    const parent = vm.$parent
    // 如果父级存在,并且父级没有在被销毁,并且不是抽象组件而是真实组件(就是抽象组件,它的abstract就为true)
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      // 从父级中删除当前组件
      remove(parent.$children, vm)
    }
    // 移除实例的所有观察者
    if (vm._watcher) {
      // 删除实例自身依赖
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      // 删除实例内数据对其他数据的依赖
      vm._watchers[i].teardown()
    }
    // 删除数据对象的引用
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // 更新组件销毁状态
    vm._isDestroyed = true
    // 删除实例的虚拟 DOM
    vm.__patch__(vm._vnode, null)
    // 调用生命周期钩子函数
    callHook(vm, 'destroyed')
    // 关闭所有事件监听
    vm.$off()
    // 删除当前根组件的引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 删除父级的引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
什么时候会调用 $destroy()

patch 源码里看到有三次调用,源码地址:src/core/vdom/patch.js

  • 新vnode 不存在,老vnode 存在的时候,就触发卸载老vnode 对应组件的 destroy。702行
  • 如果新vnode 根节点被修改的时候,调用老vnode 对应组件的 destroy。767行
  • 新老 vnode 对比结束后,调用老vnode 对应组件的 destroy

二、Vue3的生命周期函数

Vue 生命周期_第9张图片
4、beforeDestroydestroyed

实例销毁后,Vue实例指示的所有东西都会解除绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue 生命周期_第10张图片

setup函数

setup函数是组合式API的入口函数,默认导出配置选项,setup函数声明,返回模板需要数据与函数。

  • setup 函数是 Vue3 特有的选项,作为组合式API的起点
  • 从组件生命周期看,它在 beforeCreate 之前执行
  • 函数中 this 不是组件实例,是 undefined
  • 如果数据或者函数在模板中使用,需要在 setup 返回
import { reactive, computed } from 'vue';  
  
export default {  
  setup() {  
    // 定义响应式数据  
    const count = reactive(0);  
  
    // 定义计算属性  
    const double = computed(() => count * 2);  
  
    // 定义方法  
    const increment = () => {  
      count++;  
    };  
  
    // 返回响应式数据和方法,以便在模板中使用  
    return {  
      count,  
      increment,  
      double  
    };  
  }  
}

setup 中,我们可以使用 Vue3 提供的多个工具函数来定义响应式数据监听生命周期钩子处理计算属性声明事件处理函数等。这些函数包括:

  • reactive:用于创建响应式对象
  • ref:用于创建一个单一的响应式值
  • computed:用于创建计算属性
  • watch:用于监听响应式数据的变化
  • onMountedonUpdatedonUnmounted:用于监听生命周期钩子
  • toRefs:用于将响应式对象转换为普通对象
  • injectprovide:用于跨层级组件传递数据
  • getCurrentInstance:用于访问当前组件实例

onBeforeMount

onBeforeMount 钩子函数会在组件挂载到 DOM 前运行,可以用来在组件挂载前执行一些初始化操作。

<script setup>
import { onBeforeMount } from 'vue'
    onBeforeMount(() => {
      console.log('Before mount')
    })
</script>

onMounted

onMounted 钩子函数会在组件挂载到 DOM 后运行,通常用于获取数据初始化页面状态等操作。

<template>
  <div>{{ message }}</div>
</template>

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

    const state = reactive({
      message: ''
    })

    onMounted(() => {
      // 发送 AJAX 请求,获取数据
      fetch('/api/data')
        .then(res => res.json())
        .then(data => {
          state.message = data.message
        })
    })
</script>

在上面的例子中,我们通过 onMounted 钩子在组件挂载后发送 AJAX 请求,获取数据并更新组件状态中的 message 字段。

需要注意的是,在 Vue3 中,onMountedonBeforeMount 钩子需要在 setup 函数中使用。

onBeforeUpdate

onBeforeUpdate 钩子函数会在数据重新渲染之前运行,可以用来在组件更新前执行一些操作。

<script setup>
import { onBeforeUpdate } from 'vue'
    let count = 1
    onBeforeUpdate(() => {
      console.log('Before update', count)
    })
    const handleClick = () => {
      count++
    }
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="handleClick">增加</button>
  </div>
</template>

onUpdated

onUpdated 钩子函数会在数据重新渲染后运行,通常用于更新 DOM执行动画获取最新的状态等操作。

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="handleClick">获取最新的消息</button>
  </div>
</template>

<script setup>
import { onMounted, onUpdated, ref } from 'vue'
    const message = ref('Hello, world!')
    onMounted(() => {
      // 模拟异步获取消息
      setTimeout(() => {
        message.value = 'Hello, Vue 3!'
      }, 2000)
    })
    onUpdated(() => {
      console.log('DOM updated')
    })
    const handleClick = () => {
      alert(message.value)
    }
</script>

onBeforeUnmount

onBeforeUnmount 钩子函数会在组件卸载之前运行,可以用来清除定时器取消事件监听器等操作。

<script setup>
import { onBeforeUnmount, ref } from 'vue'
    const timer = ref(null)
    onBeforeUnmount(() => {
      clearInterval(timer.value)
    })
    const startTimer = () => {
      timer.value = setInterval(() => {
        console.log('Hello, world!')
      }, 1000)
    }
    const stopTimer = () => {
      clearInterval(timer.value)
    }
</script>

<template>
  <div>
    <p>定时器示例</p>
    <button @click="startTimer">开始</button>
    <button @click="stopTimer">停止</button>
  </div>
</template>

通过 onBeforeUnmount 钩子注册了一个函数,在组件卸载之前清除定时器。同时,在方法中添加了两个按钮事件,用于启动和停止计时器。

onUnmounted

onUnmounted 钩子函数会在组件卸载后运行,通常用于清理一些资源取消订阅

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="unsubscribe">取消订阅</button>
  </div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
    const message = ref('')
    let subscription = null
    onMounted(() => {
      // 模拟创建一个订阅
      subscription = setInterval(() => {
        message.value = new Date().toLocaleTimeString()
      }, 1000)
    })
    onUnmounted(() => {
      // 在组件卸载后取消订阅
      clearInterval(subscription)
    })
    const unsubscribe = () => {
      clearInterval(subscription)
    }
</script>

你可能感兴趣的:(vue.js,javascript,前端)