Vue官网中的约束源码解释 -- 生命周期

2,467 阅读2分钟
原文链接: github.com

关于生命周期的源码执行

首先我们先来看一张官网的图:

然后我们来看一下源码里什么时候开始执行各个生命周期的:

1. beforeCreate、created

beforeCreatecreated钩子在core/instance/init.js_init方法中执行

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
nitProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

这里主要是初始化一些vm的属性,initState主要为定义的data属性进行obsever以及处理一些propswatchcomputed:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

2. beforMounted

在执行beforMounted的钩子的时候,会进行几部判断:

1. 判断存不存在$el属性
  if (vm.$options.el) {
      vm.$mount(vm.$options.el)
  }
2. 判断存不存在template属性:
    let template = options.template
    let template = options.template
    if (template) {
      // string
      if (typeof template === 'string') {
        // 如果第一个字符是#,则 template = query(id).innerHTML
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
     // dom 节点
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

3. mounted

这一步主要是经过了 render --> VNode --> path步骤后生成了一个真实的dom节点,并挂载到el上:

return function patch () {
  ...
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}
function invokeInsertHook (vnode, queue, initial) {
   ...
    for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
     }
 }

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

4. beforeUpdate

当我们执行dom更新之前,且已经经过mounted。会触发的钩子:

vm._update(vm._render(), hydrating)
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  ...
    if (vm._isMounted) {
       callHook(vm, 'beforeUpdate')
    }
  ...
}

5. updated

这个钩子函数主要是在异步更新队列中执行,也就是nextTick更新dom后会执行的钩子

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

function flushSchedulerQueue () {
   ...
   watcher.run()
  ...
  callUpdatedHooks(updatedQueue)
}

关于什么是nextTick?以及Event loop相关知识,有兴趣可以参考我的这两篇文章:

Vue nextTick 机制

Event loop 简介

6. beforeDestroy destroyed

$destroy函数被调用时,会首先触发beforeDestroy钩子:

 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
    }
  }
}

可以看到,destroy步骤如下:

  1. remove(parent.$children, vm)从父节点中先移除自己
  2. vm._watcher.teardown() 销毁watchers
  3. vm._data.__ob__.vmCount-- 从数据ob中删除引用
  4. vm.__patch__(vm._vnode, null) 调用当前渲染树上的销毁钩子
  5. callHook(vm, 'destroyed') 调用destroyed钩子
  6. vm.$off()销毁事件监听 ...

到这里差不多就执行完了销毁任务,从而触发了destroyed钩子

一些警告

不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch('a', newValue => this.myMethod())。因为箭头函数是和父级上下文绑定在一起的,this 不会是如你所预期的 Vue 实例,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。
我们可以看一下Vue是如何执行生命周期函数的:

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

比如我们执行beforeCreate钩子:callHook(vm, 'beforeCreate')。因为是箭头函数,所以可以先了解箭头函数的几个特性:

  1. 箭头函数作为函数的一种形式,对于this的处理和普通函数有所区别,其没有自己的this上下文,也就是说通过bind/call/apply函数方法设置this值时无效的,会被忽略
  2. 因为箭头函数没有自己的this上下文,那么当它作为对象的方法函数运行时,this并不指向这个对象
  3. 箭头函数的的函数体中出现的this在运行时绑定到最近的作用域上下文对象
  4. 你可以认为箭头函数的this和调用者无关,只和其定义时所在的上下文相关

说到这里,应该明白了为什么不要在选项属性或回调上使用箭头函数了吧...