Vue源码中的生命周期

474 阅读4分钟

😶基础知识又得重新补习一下了

前情回顾

理解vdom的前提下,component,element的创建实际上对vnode的实例化。而创建组件的时候会将生命周期混入进去,一起来看下这个生命周期

生命周期

生命周期的变量定义在shared文件夹中的constant.js文件中。constant顾名思义,变量嘛。

// 生命周期钩子数组
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

同同时这个问价还定义了一个资源类型

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

定义好了生命周期名称以后,必然需要有个地方去解释执行这些命令。这个位置在core文件夹中的instance文件夹中的lifeCycle.js

这个文件夹定义了以下几个方法:

  • setActiveInstance设置激活的实例,这里的实例指的是我们写的vue组件或页面。
  • initLifecycle 初始化生命周期。
  • lifecycleMixin 在Vue实例的原型上混入生命周期方法。
  • mountComponent 挂载组件。
  • updateChildComponent 更新子组件。
  • isInInactiveTree 判断当前实例的激活状态。
  • activateChildComponent 激活子组件。
  • deactivateChildComponent 灭活子组件。
  • callHook 调用钩子方法。

方法细节

  • callHook。callHook的代码如下:
export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  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()
}

不得不说,这个源码整的越来越复杂了。其意图是从$options中取出钩子方法,然后遍历执行对应的方法。

在之前的版本中没有invokeWithErrorHandling这个方法。遍历handlers时直接交给vm调用。

export function callHook (vm: Component, hook: string) {
  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)
  }
}

而最新的代码时封装了一个invokeWithErrorHandling方法,执行的时候会包含错误处理。

这里有个地方我一直不太理解,就是这个_hasHookEvent。从上边的代码里看。如果$optons中的hook即handlers存在,则会调用对应的hook方法。或者如果vm实例的_hasHookEvent属性为true,也会调用$emit方法触发对应的hook方法。

然后我们看下event.js`。

core/instance/event.js

const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

当使用$on方法监听事件时,如果事件名称以hook:为前缀,那么这个事件就会被当做hoodkEvent,在将事件的回调push到实例对象的_event属性时,实例的_hasHookEvent属性会被设置为true。当使用$emit触发$emit('hoook:eventname')时,对应的回调函数就会被触发。

使用的方式大致如下:

<LoginComponent @hook:created="created"></LoginComponent>
  • initLifecycle。初始化生命周期这个方法,其实只是在实例上申明了几个私有属性,用作申明周期的标识。其代码如下:
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
 // 设置初始化状态
  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

可以看到,vm设置了$children,$ref,_watcher,_inactve为空,_directInactive,_isMounted,_isDestroyed,_isBeingDestroyed为false。

  • lifecycleMixin生命周期混入方法。

这个方法在vue的原型上添加了_update$forceupdate,$destroy三个方法。

Vue.prototype._update

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

_update方法会先判断实例是否已经挂载,如果实例已经挂载,则会先调用beforeUpdate钩子。

然后当前实例的_vnode及当前实例分别赋值给prevNodepreActiveInstance。如果没有preVode则说明是初次渲染,直接调用__pattch__方法进行处理。

// Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render 初次渲染
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } 

如果存在preVnode则直接调用__patch__进行更新

 // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
  • $forceUpdate方法。这个放个直接判断实例上是否存在_watcher属性,如果存在_watcher属性,则直接调用watcher的update方法。
Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
  • $destroy方法。这个方法会判断实例的_isBeingDestroyed属性,然后执行beforeDestroy钩子函数。然后依次清除watcher,解绑事件(调用$off),设置_isDestroyed属性为true。设置$vnode.parent为null。
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
    }
  }
  • mountComponent挂载组件。这个方法会先判断$options中是否有render方法。如果render不存在,则创建一个空节点。然后调用beforeMount钩子方法。之后,调用实例的_render方法进行更新。最后在实例上设置新的_watcher,以上的步骤完成后,则调用mounted钩子函数。
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  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)
    }
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  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
}

总结

生命周期其实是一个组件从加载到销毁的过程。如果组件初次加载,则会先创建一个空节点,然后调用beforeMount,之后就是使用实例的_render进行渲染,渲染后则调用mounted,设置实例的属性isMounted为true。组件更新的过程会先调用beforeUpdate钩子,之后使用__patch__进行更新操作。组件销毁时会先触发beforeDestroy钩子,然后设置对应的属性为false,对应的示例属性对象为null,最后调用destroyed钩子。

大致就是这么一个过程。

思考

今天也看了些css相关的内容。明天顺带着提一下吧。

最后说两句

  1. 动一动您发财的小手,「点个赞吧」
  2. 动一动您发财的小手,「点个在看」
  3. 都看到这里了,不妨 「加个关注」
  4. 不妨 「转发一下」,好东西要记得分享

javascript基础知识总结