vue2.x源码解析六——组件化--5.生命周期

1.生命周期

Vue实例从创建到销毁的过程,就是生命周期。详细来说也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、卸载等一系列过程。
首先我们来看一下官网的生命周期图(我自己做了一点点注释):
也可以看我之前的博客 vue生命周期的理解
这里写图片描述
Vue提供给我们的钩子为上图的红色的文字。

生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。
我们现在用源码来分析生命周期的钩子函数是如何被执行的。

2.了解生命周期的执行方式

源码中最终执行生命周期的函数都是调用 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()
}

参数:
1:Vue实例(组件类型的)
2:hook,String类型的, 比入我们可以传入 created mounted等生命周期

步骤1:

 const handlers = vm.$options[hook]

Vue.js 初始化合并 options 的过程,各个阶段的生命周期的函数也被合并到 vm. o p t i o n s h o o k v m . options[hook] 对应的回调函数数组

步骤2:

for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }

遍历执传入的生命周期中所有的所对应的函数,执行的时候把 vm 作为函数执行的上下文。

总结

callhook 函数的功能就是在当前vue组件实例中,调用某个生命周期钩子注册的所有回调函数。

3. beforeCreate & created

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

Vue.prototype._init = function (options?: Object) {
  .
  .
  .
  //主要就是给vm对象添加了$parent、$root、$children属性,以及一些其它的生命周期相关的标识。
  initLifecycle(vm)
  // 初始化事件相关的属性
  initEvents(vm)
  // vm添加了一些虚拟dom、slot等相关的属性和方法
  initRender(vm)

  callHook(vm, 'beforeCreate')

  //下面initInjections(vm)和 initProvide(vm) 两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察
  initInjections(vm) 
   //主要就是操作数据了,props、methods、data、computed、watch,从这里开始就涉及到了Observer、Dep和Watcher
  initState(vm)
  initProvide(vm) 

  callHook(vm, 'created')
  //
  .
  .
  .
}

_init 函数分为3步
1:合并options
2. 调用初始化函数
3. 挂载到DOM — $mount

beforeCreate
是拿不到数据的比如定义在,props、methods、data、computed、watch中的,因为他 initState(vm)之前执行

created
是可以拿到数据的的,因为他在initState(vm)之后执行

总结:
在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数。

4.beforeMount & mounted

在我们初始化最后回去执行$mount,也就是去执行挂载。

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

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

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      .
      .
      .
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }


  new Watcher(vm, updateComponent, noop, {
   .
   .
   .
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    // 执行mounted
    callHook(vm, 'mounted')
  }
  return vm
}

过程是

  1. 渲染watcher监听updateComponent方法,在数据初始化和变更的时候调用。
  2. updateComponent也就是vm._update(将虚拟DOM映射到真实DM)的函数。
  3. vm._update之前会先调用 vm._render() 函数渲染 VNode

4.1 beforeMount

在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数

4.2 mouted

在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mouted 钩子。

注意:

这是通过外部 new Vue 初始化过程。
因为这里对 mouted 钩子函数执行有一个判断逻辑,vm.$vnode 为 null,因为vm.$vnode的意思是父VNode,如果vue实力没有父VNode,说明他只有根VNode。则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程。

那么对于组件,它的 mounted 时机在哪儿呢?

组件 mounted 时机

组件的 VNode patch 到 DOM ,patch的最后会执行 invokeInsertHook 函数,
它的定义在 src/core/vdom/patch.js 中:

 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)

invokeInsertHook也定义在该 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])
    }
  }
}

可以看到把 queue就是insertedVnodeQueueinvokeInsertHook是将保存在insertedVnodeQueue的钩子函数依次执行一遍

insertedVnodeQueue是一个数组,在patch的过程中,会将子组件的子VNode掺入到子组件,这是也会将子组件pushinsertedVnodeQueue数组,子组件插入到父组件的时候也会将父组件pushinsertedVnodeQueue数组,所以是子组件先插入,父组件后插入

我们可以看到遍历insertedVnodeQueue数组(里面放着组件)的时候,会调用组件的data中的insert 这个钩子函数

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

const componentVNodeHooks = {
  // insert函数
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      // 组件调用mounted方法
      callHook(componentInstance, 'mounted')
    }
    // 
  },
}

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

4.3总结

  1. 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,也就是说beforeMount 函数中是拿不到DOM的
  2. 对于组件beforeMount 钩子,是先父后子。
    1. (因为在组件的的虚拟DOM映射到真实DOM的过程中,显示父组件映射,这是会走beforeMount 钩子,再往后发现父组件有子组件就会再执行一遍patch,子组件会再执行beforeMount 钩子,依次类推,最后mounted后分别插入父辈组件)
  3. mounted 之后虚拟DOM映射完成,可以拿到DOM
  4. 对于组件的mounted ,mounted 钩子函数的执行顺序是先子后父。

5.beforeUpdate & updated

顾名思义,beforeUpdateupdated 的钩子函数执行时机都应该是在数据更新的时候。

5.1 beforeUpdate

beforeUpdate 的执行时机是在渲染 Watcherbefore 函数中,我们刚才提到过:
mouted的时候调用的mountComponent函数中, src/core/instance/lifecycle.js

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

  new Watcher(vm, updateComponent, noop, {
    before () {
    // 先判断是否mouted完成
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 
}

我们看到有一个before函数,这个函数是在数据变化的时候调用flushSchedulerQueue 函数中执行, 它的定义在 src/core/observer/scheduler.js 中:

function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }
}

queue是一个包含所有watcher的数组,遍历queue,如果watcher有before函数就会先执行before函数

注意在调用before这里有个判断,也就是在组件已经 mounted 之后,才会去调用这个钩子函数。也就是说初始化的时候不会调用该方法。

我们知道watcher监听update方法,在变更的时候调用。
也就是说会在数据变化前调用beforeUpdate这个钩子

5.2 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')
    }
  }
}

updatedQueue 是 更新了的 wathcer 数组,那么在 callUpdatedHooks 函数中,它对这些数组做遍历,只有满足当前 watchervm._watcher(也就是当前的渲染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 实例 pushvm._watchers 中,vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数。

过程:

  1. 会实例化一个渲染的 Watcher 去监听 vm 上的数据变化
  2. 实例化 Watcher 的过程中,在它的构造函数里会判断 isRenderWatcher,接着把当前 watcher 的实例赋值给 vm._watchervm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
  3. 当前 wathcer 实例 pushvm._watchers
  4. flushSchedulerQueue 函数在数据变化的时候调用,再去调用callUpdatedHooks 函数,并在其中遍历更新了的 ,只有满足当前 watchervm._watcher(也就是当前的渲染watcher) 以及组件已经 mounted 这两个条件,才会执行 updated 钩子函数。
  5. (只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数。)

5.2 总结

  1. beforeUpdate发生在数据变化的前,初始化数据并不会触发
  2. update 发生在数据变化的后,初始化数据并不会触发
  3. beforeUpdateupdate 都只会在mounted以后调用

6.beforeDestroy & destroyed

顾名思义,beforeDestroydestroyed 钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy 方法,它的定义在 src/core/instance/lifecycle.js 中:

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    // 调用beforeDestroy钩子
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true

    // 一些销毁工作
    const parent = vm.$parent
    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
    // 发现子组件,会先去销毁子组件
    vm.__patch__(vm._vnode, null)


    // 执行destroyed钩子
    callHook(vm, 'destroyed')

    vm.$off(
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
  1. beforeDestroy 钩子函数的执行时机是在 $destroy 函数执行最开始的地方
  2. 接着执行了一系列的销毁动作,包括从 parent$children 中删掉自身,删除 watcher,当前渲染的VNode 执行销毁钩子函数等,
  3. 会执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用
  4. 执行完毕后再调用 destroyed 钩子函数。

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

总结

beforeDestroy钩子函数的执行时机在组件销毁的阶段前调用
destroyed 钩子函数的执行时机在组件销毁的阶段后调用

7.总结

  • Vue的生命周期函数就是在初始化及数据更新过程中的不同阶段调用不通的函数
  • created钩子函数可以访问数据
  • mounted钩子函数可以访问DOM
  • destroyed 钩子函数可以做一些定时器销毁工作

猜你喜欢

转载自blog.csdn.net/haochangdi123/article/details/80943829