Vue2.0源码阅读计划(六)——生命周期

437 阅读3分钟

——最困难的事情就是认识自己

前言

Vue中,每个Vue实例从被创建出来到最终被销毁都会经历一个过程,就像人一样,从出生到死亡。在这一过程里会发生许许多多的事,例如设置数据监听,编译模板,组件挂载等。在Vue中,把Vue实例从被创建出来到最终被销毁的这一过程称为Vue实例的生命周期,同时,在Vue实例生命周期的不同阶段Vue还提供了不同的钩子函数,以方便用户在不同的生命周期阶段做一些额外的事情。

正文

image.png

先用官方的生命周期图解镇楼,接下来我们一一来看:

beforeCreate

在创建Vue实例的时候,内部会执行this._init(options)

vm.$options = mergeOptions( // 合并配置
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm) // 初始化 render
callHook(vm, 'beforeCreate')
mergeOptions合并配置将Vue、用户自定义、实例上的属性进行合并,钩子函数会合并为数组
initLifecycle(vm)初始化生命周期给Vue实例挂载了一些属性并设置默认值,$parent$root较为重要
initEvents(vm)初始化事件中心父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。
initRender(vm)初始化 render给Vue实例挂载$slots、$scopedSlots、_c、$createElement等属性,将$attrs、$listeners变为响应式

此阶段可以加一些loading效果,在created时进行移除

created

initInjections(vm) // 初始化 inject
initState(vm) // 初始化 state
initProvide(vm) // 初始化 provide
callHook(vm, 'created')

initProvide(vm)在当前组件注入属性,initInjections(vm)原理:自底向上查找上游父级组件所注入的对应的属性。initState(vm)按顺序初始化了5个选项:props、methods、data、computed、watch

initProps(vm, opts.props)初始化 props对 props 赋予响应式并代理到 Vue 实例上
initMethods(vm, opts.methods)初始化 methods将 methods 代理到 Vue 实例上
initData(vm)初始化 data对 data 赋予响应式并代理到 Vue 实例上
initComputed(vm, opts.computed)初始化 computed循环创建 computed watcher并收集至 handler 所依赖的响应式属性上,最后将 computed 代理到 Vue 实例上
initWatch(vm, opts.watch)初始化 watch循环创建 user watcher并收集至所监听的响应式属性上

此阶段可以执行异步请求数据的方法,完成数据的初始化

beforeMount

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

// src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating) {
  el = el && query(el)
  
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  
  if (!options.render) {
      if (template) {
          // 处理 # 开始的选择符、dom对象的情况
      } else if (el) { // 获取 el 的 outerHTML 作为模板
          template = getOuterHTML(el)
      }
      if (template) { // 编译生成 render 函数
          compileToFunctions(template, {...}
      }
  }
  return mount.call(this, el, hydrating)
}

// src\core\instance\lifecycle.js
export function mountComponent (vm, el, hydrating) {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')
  // ......
}

此阶段主要做了这么几件事:

  • 判断el是否存在,存在时才会去调用$mount去执行后续的挂载操作
  • query(el)拿到eldom元素引用
  • el进行判断,不能为body、html元素
  • 判断render函数是否定义,未定义时针对template进行处理并进行模板编译生成render函数
  • Vue实例上挂载$el属性
  • 判断render函数是否定义,未定义时设值为一个空的VNode(此处判断针对的是子组件挂载时的情况)

mounted

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)

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

此阶段创建render watchervm._render()生成VNode,vm._update()进行patch渲染视图。

该阶段DOM加载完成 ,完成挂载,可以进行DOM的相关操作。

beforeUpdate

// 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()
    }
    // ......
    callUpdatedHooks(updatedQueue)
}

上面mounted阶段创建render watcher时,第4参数给到了before的定义,当数据更新时,会执行至此处异步队列刷空的操作,会在每一个watcher执行run方法前执行beforeUpdate钩子函数。

updated

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

上面beforeUpdate阶段所有的watcher执行完run方法后会调用callUpdatedHooks,进行所有Vue实例updated钩子函数的循环执行。

此阶段数据更新完成,DOM也重新渲染,业务的统一处理可以在这里进行。

beforeDestroy

// src\core\instance\lifecycle.js
Vue.prototype.$destroy = function () {
    const vm: Component = 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)
    }
    
    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()
    
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

我们可以看到beforeDestroy钩子函数调用的时机是在this.$destroy()执行的一开始,this.$destroy()除我们手动调用外,会在什么时候执行?

🙋‍♂️:在组件销毁的时候执行

🙎‍♀️:组件什么时候会被销毁?

🙋‍♂️:在patch阶段,VNode渲染更新的时候,对比出oldVNode需要被移除时,组件被销毁

// src\core\vdom\create-component.js
var componentVNodeHooks = {
    destroy: function destroy (vnode) {
      var componentInstance = vnode.componentInstance;
      if (!componentInstance._isDestroyed) {
        if (!vnode.data.keepAlive) {
          componentInstance.$destroy();
        } else {
          deactivateChildComponent(componentInstance, true /* direct */);
        }
      }
    }
}

Vnode渲染更新的时候,它在执行相关操作的同时,还会在每个阶段触发相应的钩子函数。组件销毁时会触发destroy这个钩子函数,从而执行this.$destroy()

destroyed

destroyed阶段主要做了这么几件事:

  • 将当前的Vue实例从其父级实例中移除
  • 卸载当前实例上的watcher
  • vm.__patch__(vm._vnode, null)重新patch,这里针对手动调用this.$destroy()的情况

注意:所有自定义事件监听器的移除操作vm.$off()是在destroyed钩子函数执行之后的。

最后,再移除一些相关属性的引用,至此,组件销毁完毕。

结尾

没啥可说的,奥力给!!!