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

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

前言

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钩子函数执行之后的。

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

结尾

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享