Skip to content

Vue2 生命周期深度解析

Vue2的生命周期是组件从创建到销毁的完整过程,理解生命周期对于掌握Vue的运行机制至关重要。

🔄 生命周期概览

mermaid
graph TD
    A[new Vue] --> B[Init Events & Lifecycle]
    B --> C[beforeCreate]
    C --> D[Init injections & reactivity]
    D --> E[created]
    E --> F{Has 'el' option?}
    F -->|No| G[When vm.$mount is called]
    F -->|Yes| H[Has 'template' option?]
    G --> H
    H -->|No| I[Compile el's outerHTML as template]
    H -->|Yes| J[Compile template into render function]
    I --> J
    J --> K[beforeMount]
    K --> L[Create vm.$el and replace 'el' with it]
    L --> M[mounted]
    
    M --> N[When data changes]
    N --> O[beforeUpdate]
    O --> P[Virtual DOM re-render and patch]
    P --> Q[updated]
    Q --> N
    
    M --> R[When vm.$destroy is called]
    R --> S[beforeDestroy]
    S --> T[Teardown watchers, child components and event listeners]
    T --> U[destroyed]

🏗️ 生命周期钩子详解

1. beforeCreate

组件实例刚被创建,数据观测和事件配置都未开始:

javascript
export default {
  beforeCreate() {
    console.log('beforeCreate')
    console.log('data:', this.$data) // undefined
    console.log('methods:', this.myMethod) // undefined
    console.log('computed:', this.myComputed) // undefined
    console.log('el:', this.$el) // undefined
  },
  
  data() {
    return {
      message: 'Hello Vue'
    }
  },
  
  methods: {
    myMethod() {
      console.log('method called')
    }
  },
  
  computed: {
    myComputed() {
      return this.message + '!'
    }
  }
}

源码实现

javascript
function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    vm._uid = uid++
    vm._isVue = true
    
    // 合并选项
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
    vm._renderProxy = vm
    vm._self = vm
    
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    
    // 调用 beforeCreate 钩子
    callHook(vm, 'beforeCreate')
    
    initInjections(vm)
    initState(vm) // 初始化 data、props、computed、methods、watch
    initProvide(vm)
    
    // 调用 created 钩子
    callHook(vm, 'created')
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

2. created

实例创建完成,数据观测、属性和方法的运算已完成,但DOM未生成:

javascript
export default {
  created() {
    console.log('created')
    console.log('data:', this.$data) // 可访问
    console.log('methods:', this.myMethod) // 可访问
    console.log('computed:', this.myComputed) // 可访问
    console.log('el:', this.$el) // undefined
    
    // 适合进行数据初始化
    this.fetchData()
  },
  
  methods: {
    fetchData() {
      // 发起API请求
      axios.get('/api/data').then(response => {
        this.data = response.data
      })
    }
  }
}

3. beforeMount

模板编译完成,即将挂载到DOM,但还未挂载:

javascript
export default {
  beforeMount() {
    console.log('beforeMount')
    console.log('el:', this.$el) // undefined
    console.log('template compiled') // 模板已编译为渲染函数
  }
}

源码实现

javascript
Vue.prototype.$mount = function (el, hydrating) {
  el = el && query(el)
  
  const options = this.$options
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  
  return mount.call(this, el, hydrating)
}

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

function mountComponent(vm, el, hydrating) {
  vm.$el = el
  
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  
  callHook(vm, 'beforeMount')
  
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true)
  
  hydrating = false
  
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  
  return vm
}

4. mounted

组件挂载完成,DOM已生成,可以访问DOM元素:

javascript
export default {
  mounted() {
    console.log('mounted')
    console.log('el:', this.$el) // 可访问DOM元素
    
    // 适合进行DOM操作
    this.$nextTick(() => {
      // DOM更新完成后执行
      this.initChart()
    })
  },
  
  methods: {
    initChart() {
      // 初始化图表等需要DOM的操作
      const canvas = this.$refs.canvas
      const ctx = canvas.getContext('2d')
      // 绘制图表...
    }
  }
}

5. beforeUpdate

数据更新时调用,发生在虚拟DOM重新渲染之前:

javascript
export default {
  data() {
    return {
      count: 0
    }
  },
  
  beforeUpdate() {
    console.log('beforeUpdate')
    console.log('旧的count值:', this.count)
    console.log('DOM还未更新')
  },
  
  methods: {
    increment() {
      this.count++
    }
  }
}

6. updated

数据更新完成,DOM重新渲染完成:

javascript
export default {
  updated() {
    console.log('updated')
    console.log('新的count值:', this.count)
    console.log('DOM已更新')
    
    // 注意:避免在此钩子中修改数据,可能导致无限循环
  }
}

7. beforeDestroy

实例销毁之前调用,实例仍然完全可用:

javascript
export default {
  beforeDestroy() {
    console.log('beforeDestroy')
    
    // 清理工作
    clearInterval(this.timer)
    window.removeEventListener('resize', this.handleResize)
    
    // 取消网络请求
    if (this.cancelToken) {
      this.cancelToken.cancel('组件销毁')
    }
  },
  
  data() {
    return {
      timer: null
    }
  },
  
  mounted() {
    this.timer = setInterval(() => {
      console.log('定时器执行')
    }, 1000)
    
    window.addEventListener('resize', this.handleResize)
  },
  
  methods: {
    handleResize() {
      // 处理窗口大小变化
    }
  }
}

8. destroyed

实例销毁后调用,所有指令解绑,事件监听器移除:

javascript
export default {
  destroyed() {
    console.log('destroyed')
    console.log('组件已完全销毁')
    
    // 此时组件实例已不可用
    // 主要用于确认清理工作完成
  }
}

🔧 生命周期源码实现

callHook 函数

javascript
function callHook(vm, hook) {
  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()
}

function invokeWithErrorHandling(handler, context, args, vm, info) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

销毁过程

javascript
Vue.prototype.$destroy = function () {
  const vm = this
  if (vm._isBeingDestroyed) {
    return
  }
  
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  
  // 从父组件中移除
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  
  // 销毁watcher
  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
  
  // 调用当前渲染树上的销毁钩子
  vm.__patch__(vm._vnode, null)
  
  callHook(vm, 'destroyed')
  
  // 关闭所有实例监听器
  vm.$off()
  
  // 移除__vue__引用
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  
  // 释放循环引用
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

🎯 最佳实践

1. 合理选择生命周期钩子

javascript
export default {
  // 数据初始化
  created() {
    this.fetchUserData()
  },
  
  // DOM操作
  mounted() {
    this.initEcharts()
    this.bindEvents()
  },
  
  // 清理工作
  beforeDestroy() {
    this.cleanup()
  }
}

2. 避免在updated中修改数据

javascript
export default {
  updated() {
    // ❌ 错误:可能导致无限循环
    // this.count++
    
    // ✅ 正确:只进行DOM相关操作
    this.adjustLayout()
  }
}

3. 异步组件的生命周期

javascript
const AsyncComponent = () => ({
  component: import('./MyComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

Vue2的生命周期机制为开发者提供了在组件不同阶段执行代码的能力,是Vue组件系统的重要组成部分。