深入理解Vue实例生命周期(下)

简介: 每个 Vue 实例在被创建时都会经过一系列的初始化过程。例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。 为了让开发者在Vue实例生命周期的不同阶段有机会插入自己的代码逻辑,vue提供了一种叫做生命周期钩子的函数。

beforeUpdate


在初始化阶段,vue实例中的数据(data/props/computed/watch)已经被处理成响应式的了。任何对数据的访问(getter)都会被watcher添加为依赖,任何对数据的变更,都将触发对数据有依赖的watcher的更新。watcher在更新之前会调用watcher.before方法,该方法是在挂载阶段创建watcher实例的时候定义的。

watcher.before中会触发beforeUpdate钩子。此时vue实例只是确定了最终需要更新的数据,尚未真正开始更新。


updated


从代码中可以看到,在触发updated钩子前,vue实例需要对DOM元素进行更新。更新的过程是异步的。具体方式通过实例的render watcher执行run方法。该方法会去调用我们在挂载阶段介绍的updateComponent函数。从而重新创建vnode,并进行vnode的diff操作后更新DOM元素。


我们还注意到,代码中调用了callActivatedHooks函数,该函数用来触发activated钩子。下文我们再做说明,这里不展开。


销毁


当vue实例的$destroy方法时,实例将进入销毁阶段。此时触发的钩子是:

beforeDestorydestroyed


// 销毁Vue实例
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 避免重复执行销毁操作
    if (vm._isBeingDestroyed) {
      return
    }
    // 触发实例的beforeDestroy钩子
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // 将实例从其父实例中的$chilren中移除(断开与父实例的联系)
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    //  销毁实例的 watchers
    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
    // 销毁指令、ref等
    vm.__patch__(vm._vnode, null)
    // 触发destroyed事件
    callHook(vm, 'destroyed')
    // 移除实例的所有事件监听器
    vm.$off()
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 释放循环引用(#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}


beforeDestroy


从代码中可以看到,$destroy被调用时,在执行实际的销毁动作前触发beforeDestroy。此时,由于并未开始执行实际的销毁代码,实例及DOM元素仍可正常访问。


destroyed


从代码中可以看到,销毁操作主要包括以下几点:清空所有的watcher、删除所有的指令、删除DOM元素并关闭DOM上的所有的事件、断开与父实例之间的关系、将实例标记成已销毁状态。此时实例已经被销毁,已经无法访问实例的属性和DOM元素了。


keep-alive包裹下的组件的生命周期钩子


下面的两个钩子只有当组件包裹在keep-alive时才会触发。


activated


HTML标签和组件标签在vue内部实现中都有对应的vnode,组件vnode在设计上与普通的HTML标签的vnode有所不同。例如组件vnode上包含initprepatchinsertdestroy等钩子。这些钩子在组件实例初始化、更新和销毁等不同的阶段进行调用。


// 组件vnode的钩子
const componentVNodeHooks = {
    // ...省略其他钩子
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      // 触发组件的mounted钩子
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
// 触发activated钩子
export function activateChildComponent (vm: Component, direct?: boolean) {
  // ...省略部分代码
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    // 调用实例的activated钩子
    callHook(vm, 'activated')
  }
}


与根实例一样,keep-alive包裹下的组件实例初始化时同样会依次经历初始化阶段、挂载阶段,但在挂载阶段之后会调用组件vnode的insert钩子,insert钩子会触发组件实例的activated钩子。因为insert 是在组件实例挂载完成后调用的,所以mounted的触发早于activated


当组件切换回来的同时,组件的数据发生了变化,此时组件将进入更新阶段,意味着将会依次触发beforeUpdateupdated钩子。


那么问题来了,activatedbeforeUpdateupdated钩子哪个先触发呢?

答案是先触发beforeUpdate,再触发activated,最后触发updated


我们回顾下前面更新阶段的代码:


// 触发activated钩子
callActivatedHooks(activatedQueue)
// 触发updated钩子
callUpdatedHooks(updatedQueue)
//...省略部分代码
// 触发activated钩子
function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}


可以看到在函数flushSchedulerQueue中,callActivatedHooks函数用来触发activated。而callActivatedHooks的顺序在callUpdatedHooks前面,所以activated钩子的触发早于updated钩子。


deactivated


const componentVNodeHooks = {
  // ... 省略其他钩子
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      // 未被keep-alive包裹销毁组件
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        // 被keep-alive包裹
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    // 调用实例的deactivated钩子
    callHook(vm, 'deactivated')
  }
}


当组件被切换到其他的组件时,会调用组件vnode的destroy钩子,在组件被keep-alive包裹的情况下,只会将组件对应的DOM元素从DOM树删除,但不会销毁组件实例,此时会调用deactivateChildComponent 从而触发deactivated钩子。组件在没有被keep-alvie包裹的情况下,才会调用$destroy销毁组件实例,触发beforeDestroydestroyed钩子。


总结


关于vue实例的生命周期,官网讲解的其实是比较简单易懂的。本文主要还是希望能从源码的角度,进一步让大家理解每个生命周期做了什么处理。更好地理解钩子的触发时机及先后顺序。


相关文章
|
7天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
7天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
7天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
7天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
6天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
8天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
6天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
JavaScript
vue 的实例生命周期
vue 的实例生命周期
60 0
|
6月前
|
JavaScript 前端开发 开发者
vue实例、指令、生命周期
vue实例、指令、生命周期
58 1
|
6月前
|
JavaScript
vue 的实例生命周期
vue 的实例生命周期