Vue源码解析之组件化(二)

本文深入分析Vue.js的生命周期,从源码角度解释各生命周期钩子函数的执行时机和过程,包括beforeCreate、created、beforeMount、mounted等,帮助开发者更好地理解和运用Vue组件生命周期。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

合并配置

通过之前章节的源码分析我们知道,new Vue 的过程通常有 2 种场景,一种是外部我们的代码主动调用 new Vue(options) 的方式实例化一个 Vue 对象;另一种是我们上一节分析的组件过程中内部通过 new Vue(options) 实例化子组件。

无论哪种场景,都会执行实例的 _init(options) 方法,它首先会执行一个 merge options 的逻辑,相关的代码在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

可以看到不同场景对于 options 的合并逻辑是不一样的,并且传入的 options 值也有非常大的不同,接下来我会分开介绍 2 种场景的 options 合并过程。

为了更直观,我们可以举个简单的示例:

import Vue from 'vue'

let childComp = {
  template: '<div>{{msg}}</div>',
  created() {
    console.log('child created')
  },
  mounted() {
    console.log('child mounted')
  },
  data() {
    return {
      msg: 'Hello Vue'
    }
  }
}

Vue.mixin({
  created() {
    console.log('parent created')
  }
})

let app = new Vue({
  el: '#app',
  render: h => h(childComp)
})

外部调用场景

当执行 new Vue 的时候,在执行 this._init(options) 的时候,就会执行如下逻辑去合并 options

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

这里通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,resolveConstructorOptions 的实现先不考虑,在我们这个场景下,它还是简单返回 vm.constructor.options,相当于 Vue.options,那么这个值又是什么呢,其实在 initGlobalAPI(Vue) 的时候定义了这个值,代码在 src/core/global-api/index.js 中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)
  // ...
}

首先通过 Vue.options = Object.create(null) 创建一个空对象,然后遍历 ASSET_TYPESASSET_TYPES 的定义在 src/shared/constants.js 中:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以上面遍历 ASSET_TYPES 后的代码相当于:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}

接着执行了 Vue.options._base = Vue,它的作用在我们上节实例化子组件的时候介绍了。

最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前有 <keep-alive><transition> 和 <transition-group> 组件,这也就是为什么我们在其它组件中使用 <keep-alive> 组件不需要注册的原因,这块儿后续我们介绍 <keep-alive> 组件的时候会详细讲。

那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions 主要功能就是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 extends 和 mixixns 合并到 parent 上,然后遍历 parent,调用 mergeField,然后再遍历 child,如果 key 不在 perent 的自身属性上,则调用 mergeField

这里有意思的是 mergeField 函数,它对不同的 key 有着不同的合并策略。举例来说,对于生命周期函数,它的合并策略是这样的:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

这里定义了 Vue.js 所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 mergeHook 函数。这个函数的实现也非常有意思,用了一个多层 3 元运算符,逻辑就是如果不存在 childVal ,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。

关于其它属性的合并策略的定义都可以在 src/core/util/options.js 文件中看到,这里不一一介绍了,感兴趣的同学可以自己看。

通过执行 mergeField 函数,把合并后的结果保存到 options 对象中,最终返回它。

因此,在我们当前这个 case 下,执行完如下合并后:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

vm.$options 的值差不多是如下这样:

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created') 
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {  
    //...
  }
}

组件场景

由于组件的构造函数是通过 Vue.extend 继承自 Vue 的,先回顾一下这个过程,代码定义在 src/core/global-api/extend.js 中。

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )

  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // ...
  return Sub
}

我们只保留关键逻辑,这里的 extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。

接下来我们再回忆一下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js 中:

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}

这里的 vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub, 所以 执行 new vnode.componentOptions.Ctor(options) 接着执行 this._init(options),因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。先来看一下它的代码实现,在 src/core/instance/init.js 中:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

initInternalComponent 方法首先执行 const opts = vm.$options = Object.create(vm.constructor.options),这里的 vm.construction 就是子组件的构造函数 Sub,相当于 vm.$options = Sub.options

接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。

这么看来,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。

因此,在我们当前这个 case 下,执行完如下合并后:

initInternalComponent(vm, options)

vm.$options 的值差不多是如下这样:

vm.$options = {
  parent: Vue /*父Vue实例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode实例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created') 
      }, function created() {
        console.log('child created') 
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted') 
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}

生命周期

每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

<img src="../assets/lifecycle.png"/>

在我们实际项目开发过程中,会非常频繁地和 Vue 组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。

源码中最终执行生命周期的函数都是调用 callHook 方法,它的定义在 src/core/instance/lifecycle 中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

callHook 函数的逻辑很简单,根据传入的字符串 hook,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候把 vm 作为函数执行的上下文。

在上一节中,我们详细地介绍了 Vue.js 合并 options 的过程,各个阶段的生命周期的函数也被合并到 vm.$options 里,并且是一个数组。因此 callhook 函数的功能就是调用某个生命周期钩子注册的所有回调函数。

了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期函数它的调用时机。

beforeCreate & created

beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  // ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
  // ...
}

可以看到 beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 propsdatamethodswatchcomputed 等属性,之后我们会详细分析。那么显然 beforeCreate 的钩子函数中就不能获取到 propsdata 中定义的值,也不能调用 methods 中定义的函数。

在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 propsdata 等数据的话,就需要使用 created 钩子函数。之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreatd 钩子函数。

beforeMount & mounted

顾名思义,beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent 函数中,定义在 src/core/instance/lifecycle.js 中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  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)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mouted 钩子。注意,这里对 mouted 钩子函数执行有一个判断逻辑,vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程。那么对于组件,它的 mounted 时机在哪儿呢?

之前我们提到过,组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook 函数,把 insertedVnodeQueue 里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js 中:

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

该函数会执行 insert 这个钩子函数,对于组件而言,insert 钩子函数的定义在 src/core/vdom/create-component.js 中的 componentVNodeHooks 中:

const componentVNodeHooks = {
  // ...
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // ...
  },
}

我们可以看到,每个子组件都是在这个钩子函数中执行 mouted 钩子函数,并且我们之前分析过,insertedVnodeQueue 的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted 钩子函数的执行顺序也是先子后父。

beforeUpdate & updated

顾名思义,beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候,到目前为止,我们还没有分析 Vue 的数据双向绑定、更新相关,下一章我会详细介绍这个过程。

beforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中,我们刚才提到过:

xport function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

注意这里有个判断,也就是在组件已经 mounted 之后,才会去调用这个钩子函数。

update 的执行时机是在flushSchedulerQueue 函数调用的时候, 它的定义在 src/core/observer/scheduler.js 中:

function flushSchedulerQueue () {
  // ...
  // 获取到 updatedQueue
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

flushSchedulerQueue 函数我们之后会详细介绍,可以先大概了解一下,updatedQueue 是 更新了的 wathcer 数组,那么在 callUpdatedHooks 函数中,它对这些数组做遍历,只有满足当前 watcher 为 vm._watcher 以及组件已经 mounted 这两个条件,才会执行 updated 钩子函数。

我们之前提过,在组件 mount 的过程中,会实例化一个渲染的 Watcher 去监听 vm 上的数据变化重新渲染,这断逻辑发生在 mountComponent 函数执行的时候:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  // 这里是简写
  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

那么在实例化 Watcher 的过程中,在它的构造函数里会判断 isRenderWatcher,接着把当前 watcher 的实例赋值给 vm._watcher,定义在 src/core/observer/watcher.js 中:

export default class Watcher {
  // ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // ...
  }
}

同时,还把当前 wathcer 实例 push 到 vm._watchers 中,vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数。

beforeDestroy & destroyed

顾名思义,beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy 方法,它的定义在 src/core/instance/lifecycle.js 中:

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }

beforeDestroy 钩子函数的执行时机是在 $destroy 函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent 的 $children 中删掉自身,删除 watcher,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函数。

在 $destroy 的执行过程中,它又会执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。

activated & deactivated

activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子,我们会在介绍 keep-alive 组件的时候详细介绍,这里先留个悬念。

最后是官网提供的生命周期图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值