Vue 生命周期实现分析 (lifecycle.js)

91 阅读7分钟

Vue 生命周期实现分析 (lifecycle.js)

文件概述

lifecycle.js 是 Vue 核心实现中管理组件生命周期的关键文件,它定义了 Vue 实例从创建到销毁的完整生命周期流程。主要功能包括:

  • 组件生命周期的初始化
  • 组件的挂载与更新机制
  • 父子组件关系的建立
  • 生命周期钩子的触发机制
  • 组件的销毁与资源回收

该文件是理解 Vue 组件生命周期工作原理的核心入口。

全局变量

export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false
  • activeInstance: 记录当前正在渲染的组件实例,用于建立父子组件关系
  • isUpdatingChildComponent: 标识是否正在更新子组件,避免某些检查在更新过程中触发

核心函数分析

1. initLifecycle - 生命周期初始化

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

这个函数在 Vue 实例创建阶段被调用,主要功能:

1) 建立父子组件关系
let parent = options.parent
if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}

这段代码建立 Vue 组件树的层级关系:

  • 首先获取初始父组件引用 options.parent
  • 检查条件:当前组件不是抽象组件且有父组件
  • 关键点:跳过抽象组件层级,寻找第一个非抽象父组件
    • 循环向上查找直到找到非抽象父组件或达到顶层
    • 抽象组件如 <keep-alive><transition> 不会形成额外的组件层级
  • 将当前组件添加到实际父组件的 $children 数组

这种设计使组件树结构更加清晰,避免抽象组件造成不必要的嵌套层级。

2) 初始化实例属性
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

这段代码初始化组件实例的核心属性:

  • 组件树引用:

    • $parent: 指向父组件(可能是实际父组件,非抽象)
    • $root: 指向根组件(有父组件时使用父的根引用,否则自身为根)
    • $children: 初始化为空数组,用于存储子组件实例
  • DOM 引用:

    • $refs: 初始化为空对象,用于存储模板引用
  • 生命周期状态标志:

    • _watcher: 渲染 watcher 实例,联系数据变化和视图更新
    • _inactive: 用于 keep-alive 组件,标记是否处于非活动状态
    • _directInactive: 直接非活动状态标记
    • _isMounted: 是否已完成 DOM 挂载
    • _isDestroyed: 是否已完成销毁
    • _isBeingDestroyed: 是否正在销毁过程中

这些属性构成了 Vue 组件实例的骨架,在不同生命周期阶段被更新,帮助 Vue 正确执行组件的生命周期流程。

2. lifecycleMixin - 生命周期方法混入

这个函数向 Vue 原型添加了三个核心方法:

_update - 组件更新方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  
  // 渲染流程...
  if (!prevVnode) {
    // 初始渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  
  restoreActiveInstance()
  // 更新引用...
}

_update 是将虚拟 DOM 转换为实际 DOM 的核心方法,详细流程:

  1. 准备阶段:

    • 保存当前的真实 DOM 引用 prevEl 和旧的虚拟节点 prevVnode
    • 调用 setActiveInstance(vm) 设置当前活动实例,返回恢复函数
    • 保存新的虚拟节点 vm._vnode = vnode
  2. DOM 渲染核心 - 两种路径:

    • 首次渲染 (当 prevVnode 不存在):

      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
      
      • 将挂载点转换为虚拟节点,与新 vnode 对比
      • 根据新 vnode 创建真实 DOM 结构
      • 返回新的根 DOM 元素
    • 更新渲染 (当已有旧虚拟节点):

      vm.$el = vm.__patch__(prevVnode, vnode)
      
      • 对比新旧虚拟节点树的差异
      • 只更新变化的部分,最小化 DOM 操作
      • 返回更新后的根 DOM 元素
  3. 清理与引用更新:

    • 调用 restoreActiveInstance() 恢复先前的活动实例
    • 清除旧 DOM 元素上的 Vue 实例引用 prevEl.__vue__ = null
    • 在新 DOM 元素上设置 Vue 实例引用 vm.$el.__vue__ = vm
    • 处理高阶组件特殊情况,更新父组件的 DOM 引用

这种设计使 Vue 能够高效地将声明式模板转换为精确的 DOM 操作,同时维护组件实例与 DOM 的正确关联。

$forceUpdate - 强制更新方法
Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

这个方法允许手动触发组件更新:

  • 直接调用渲染 watcher 的 update 方法
  • 不检查数据变化,强制执行渲染流程
  • 常用于处理一些响应式系统无法自动检测的变化

使用场景示例

  1. 处理非响应式数据变化
export default {
  data() {
    return { user: { name: '张三' } }
  },
  methods: {
    updateUserInfo() {
      // 直接添加新属性,Vue 无法检测到这个变化
      this.user.age = 25;
      
      // 需要手动触发更新
      this.$forceUpdate();
    }
  }
}
  1. 处理复杂第三方库
export default {
  mounted() {
    this.mapInstance = new ExternalMapLibrary('#map');
  },
  methods: {
    updateMapMarkers(newMarkers) {
      // 直接修改第三方库内部数据
      this.mapInstance.setMarkers(newMarkers);
      // Vue 无法检测此变化,需强制更新
      this.$forceUpdate();
    }
  }
}

通常应避免过度依赖 $forceUpdate,优先使用 Vue.set 或重构数据结构以利用 Vue 的响应式系统。

$destroy - 组件销毁方法
Vue.prototype.$destroy = function () {
  const vm: Component = this
  if (vm._isBeingDestroyed) {
    return
  }
  
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  
  // 从父组件中移除自身
  // 销毁所有 watcher
  // 解除引用...
  
  vm._isDestroyed = true
  vm.__patch__(vm._vnode, null)
  callHook(vm, 'destroyed')
  vm.$off()
  // 清理引用...
}

$destroy 方法负责彻底清理组件实例,详细流程:

  1. 防重入保护:

    if (vm._isBeingDestroyed) {
      return
    }
    

    防止组件被重复销毁,确保销毁过程的幂等性

  2. 生命周期钩子与状态标记:

    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    
    • 触发 beforeDestroy 钩子,允许用户执行自定义清理
    • 设置状态标记,防止重复销毁
  3. 从组件树中移除:

    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    

    从父组件的 $children 数组中移除当前组件实例

  4. 清理响应式系统:

    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    
    • 拆解主渲染 watcher
    • 循环拆解所有用户 watchers (computed, watch 等)
    • 断开与响应式系统的连接
  5. 解除数据引用计数:

    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    

    减少数据观察者的引用计数

  6. 清理 DOM 与触发钩子:

    vm._isDestroyed = true
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')
    
    • 标记完成销毁
    • 通过传入 null 移除组件的实际 DOM
    • 触发 destroyed 钩子
  7. 清理事件与引用:

    vm.$off()
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
    
    • 移除所有事件监听器
    • 解除 DOM 元素与 Vue 实例的关联
    • 解除 VNode 树的循环引用

这种多层次的清理机制确保了组件资源的完全释放,防止内存泄漏,是长时间运行的单页应用的重要保障。

3. mountComponent - 组件挂载

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  
  // 处理渲染函数...
  
  callHook(vm, 'beforeMount')
  
  // 定义更新函数
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  // 创建渲染 watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  
  hydrating = false
  
  // 触发 mounted 钩子
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  
  return vm
}

mountComponent 函数是组件挂载到 DOM 的核心实现,详细流程:

  1. 挂载准备:

    vm.$el = el
    

    保存挂载点 DOM 元素引用

    // 处理渲染函数缺失的情况
    if (!vm.$options.render) {
      vm.$options.render = createEmptyVNode
      // 开发环境警告...
    }
    

    确保渲染函数存在,否则使用空节点渲染函数并在开发环境发出警告

    callHook(vm, 'beforeMount')
    

    触发挂载前钩子,此时可访问实例但尚未渲染

  2. 定义更新函数:

    let updateComponent
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      // 开发环境带性能标记版本
      updateComponent = () => {
        const name = vm._name
        const id = vm._uid
        const startTag = `vue-perf-start:${id}`
        const endTag = `vue-perf-end:${id}`
    
        mark(startTag)
        const vnode = vm._render()
        mark(endTag)
        measure(`vue ${name} render`, startTag, endTag)
    
        mark(startTag)
        vm._update(vnode, hydrating)
        mark(endTag)
        measure(`vue ${name} patch`, startTag, endTag)
      }
    } else {
      // 生产环境简化版本
      updateComponent = () => {
        vm._update(vm._render(), hydrating)
      }
    }
    
    • 创建封装渲染逻辑的函数
    • 开发环境版本添加性能测量标记
    • 两个核心步骤:_render() 生成 VNode,_update() 应用到 DOM
  3. 创建渲染 Watcher:

    new Watcher(vm, updateComponent, noop, {
      before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    }, true /* isRenderWatcher */)
    
    • 创建组件的渲染 Watcher
    • 通过 before 钩子在更新前触发 beforeUpdate
    • 标记为渲染 watcher
    • Watcher 构造函数会立即调用 updateComponent,触发首次渲染
  4. 挂载完成处理:

    hydrating = false
    if (vm.$vnode == null) {
      vm._isMounted = true
      callHook(vm, 'mounted')
    }
    
    • 重置服务端渲染标志
    • 检查是否是根组件($vnode == null
    • 设置挂载完成标志 _isMounted = true
    • 触发 mounted 钩子

这个函数建立了 Vue 响应式系统与 DOM 渲染之间的桥梁,通过 Watcher 机制确保数据变化时视图能自动更新。

4. updateChildComponent - 子组件更新

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // 更新子组件...
  
  // 判断是否需要强制更新...
  
  // 更新 props
  if (propsData && vm.$options.props) {
    // props 更新逻辑...
  }
  
  // 更新事件监听器
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)
  
  // 解析插槽并强制更新...
}

updateChildComponent 函数负责在父组件更新时同步更新子组件,详细流程:

  1. 动态插槽检测:

    const newScopedSlots = parentVnode.data.scopedSlots
    const oldScopedSlots = vm.$scopedSlots
    const hasDynamicScopedSlot = !!(
      (newScopedSlots && !newScopedSlots.$stable) ||
      (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
      (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) ||
      (!newScopedSlots && vm.$scopedSlots.$key)
    )
    

    复杂的条件检查作用域插槽是否发生动态变化

  2. 强制更新条件判断:

    const needsForceUpdate = !!(
      renderChildren ||               // 有新的静态插槽子节点
      vm.$options._renderChildren ||  // 有旧的静态插槽子节点
      hasDynamicScopedSlot            // 有动态作用域插槽变化
    )
    

    确定是否需要强制更新子组件

  3. 更新虚拟节点引用:

    vm.$options._parentVnode = parentVnode
    vm.$vnode = parentVnode 
    if (vm._vnode) {
      vm._vnode.parent = parentVnode
    }
    vm.$options._renderChildren = renderChildren
    

    更新各种虚拟节点引用,维护正确的节点树结构

  4. 更新响应式属性:

    vm.$attrs = parentVnode.data.attrs || emptyObject
    vm.$listeners = listeners || emptyObject
    

    更新透传属性和事件监听器,它们是响应式的

  5. 更新 Props:

    if (propsData && vm.$options.props) {
      toggleObserving(false)
      const props = vm._props
      const propKeys = vm.$options._propKeys || []
      for (let i = 0; i < propKeys.length; i++) {
        const key = propKeys[i]
        const propOptions = vm.$options.props
        props[key] = validateProp(key, propOptions, propsData, vm)
      }
      toggleObserving(true)
      vm.$options.propsData = propsData
    }
    
    • 临时关闭观察者避免不必要的响应式转换
    • 循环处理每个已定义的 prop
    • 验证并更新 prop 值
    • 重新开启观察者
    • 保存原始 props 数据副本
  6. 更新事件监听器:

    listeners = listeners || emptyObject
    const oldListeners = vm.$options._parentListeners
    vm.$options._parentListeners = listeners
    updateComponentListeners(vm, listeners, oldListeners)
    

    更新事件监听器,处理新增和移除的事件

  7. 插槽解析与强制更新:

    if (needsForceUpdate) {
      vm.$slots = resolveSlots(renderChildren, parentVnode.context)
      vm.$forceUpdate()
    }
    
    • 当满足强制更新条件时解析新的插槽内容
    • 调用 $forceUpdate 强制子组件重新渲染

这个函数展示了 Vue 精细的组件更新策略,确保子组件能够准确反映父组件的变化,同时通过条件判断避免不必要的更新。

5. callHook - 钩子调用函数

export function callHook (vm: Component, hook: string) {
  // 禁用依赖收集
  pushTarget()
  
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  
  popTarget()
}

callHook 函数是 Vue 生命周期钩子调用的核心实现,详细流程:

  1. 暂停依赖收集:

    // #7573 disable dep collection when invoking lifecycle hooks
    pushTarget()
    
    • 临时关闭响应式系统的依赖收集功能
    • 防止钩子函数中对响应式数据的访问被误收集为依赖
    • 注释引用了 GitHub Issue #7573,说明这是特定问题的解决方案
  2. 获取并调用钩子函数:

    const handlers = vm.$options[hook]
    const info = `${hook} hook`
    if (handlers) {
      for (let i = 0, j = handlers.length; i < j; i++) {
        invokeWithErrorHandling(handlers[i], vm, null, vm, info)
      }
    }
    
    • 从组件选项中获取对应钩子函数(可能是数组)
    • 准备错误处理的信息描述
    • 依次调用每个钩子函数,用错误处理包装确保一个钩子的错误不会影响其他钩子
  3. 触发钩子事件:

    if (vm._hasHookEvent) {
      vm.$emit('hook:' + hook)
    }
    
    • 只在 _hasHookEvent 标志为 true 时触发事件
    • 事件名称格式为 hook:钩子名(如 hook:mounted
    • 允许父组件通过 @hook:mounted="handler" 监听子组件生命周期
  4. 恢复依赖收集:

    popTarget()
    

    恢复之前的依赖收集状态

这个函数体现了 Vue 生命周期系统的精妙设计:

  • 提供统一的钩子调用机制
  • 隔离钩子函数与响应式系统
  • 处理由 mixins 和继承产生的多钩子情况
  • 支持通过事件系统扩展组件通信
  • 提供可靠的错误处理保障

6. 激活/停用相关函数

export function activateChildComponent (vm: Component, direct?: boolean) {
  // 激活组件实现...
}

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  // 停用组件实现...
}

这两个函数用于处理 keep-alive 组件的激活和停用:

  • 递归地处理子组件的激活/停用
  • 触发 activated/deactivated 钩子
  • 管理 _inactive 标志状态

生命周期流程图解

Vue 实例生命周期流程可以概括为:

创建实例 → 初始化事件和生命周期 → 初始化数据 → 编译模板 → 挂载DOM → 数据更新 → 销毁实例

对应到代码调用流程:

new Vue() → initLifecycle() → mountComponent() → 
    beforeCreate → created → beforeMount → 
    (Watcher创建) → _render() → _update() → 
    mounted → ... → beforeUpdate → updated → ... → 
    beforeDestroy → destroyed

重要设计特点

1. 组件实例结构设计

Vue 实例通过层层递进的属性标志清晰地标记组件状态:

  • _isMounted:是否已挂载
  • _isDestroyed:是否已销毁
  • _isBeingDestroyed:是否正在销毁过程中

这种设计使得生命周期状态判断高效且清晰。

2. 父子组件关系管理

通过 $parent$children$root 建立组件树结构,同时处理了抽象组件的特殊情况,保证组件层次的正确性。

3. activeInstance 机制

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

这种设计创建了一个临时的"当前活动实例"上下文:

  • 在组件渲染过程中跟踪当前实例
  • 返回一个恢复函数,确保嵌套组件渲染完成后能正确恢复父级上下文
  • 实现了类似"作用域"的概念,保证多层组件嵌套时的正确性

4. 渲染 Watcher 机制

渲染 Watcher 是连接响应式系统和视图的桥梁:

  • 在数据变化时自动触发组件更新
  • 通过 before 钩子在更新前触发生命周期事件
  • 使用统一的渲染函数,简化内部实现

5. 钩子事件机制

除了直接调用选项中的钩子函数外,Vue 还提供了钩子事件机制:

if (vm._hasHookEvent) {
  vm.$emit('hook:' + hook)
}

这使得父组件可以监听子组件的生命周期事件:

<child-component @hook:mounted="onChildMounted" />

增强了组件间的联动能力。

总结

lifecycle.js 文件是 Vue 生命周期机制的核心实现,它通过精心设计的实例属性和方法,实现了组件从创建、挂载、更新到销毁的完整生命周期管理:

  1. 组件关系管理:建立清晰的父子组件树结构
  2. 状态标记系统:使用一系列标志位精确跟踪组件状态
  3. 渲染机制:通过 Watcher 连接数据变化与视图更新
  4. 钩子调用系统:确保生命周期钩子在正确的时机被调用
  5. 资源管理:完善的销毁机制防止内存泄漏

Vue 的生命周期实现体现了框架的设计哲学:简洁的 API 背后是精心设计的内部机制,使开发者能够轻松掌控组件的整个生命周期。