美文网首页
Vue源码分析—组件化(四)

Vue源码分析—组件化(四)

作者: oWSQo | 来源:发表于2019-07-17 13:20 被阅读0次

    生命周期

    每个Vue实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到DOM、在数据变化时更新DOM等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

    在我们实际项目开发过程中,会非常频繁地和Vue组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。

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

    callHook函数的逻辑很简单,根据传入的字符串hook,去拿到vm.$options[hook]对应的回调函数数组,然后遍历执行,执行的时候把vm作为函数执行的上下文。

    Vue.js合并options的过程,各个阶段的生命周期的函数也被合并到vm.$options里,并且是一个数组。因此callhook函数的功能就是调用某个生命周期钩子注册的所有回调函数。

    了解了生命周期的执行方式后,接下来我们会具体介绍每一个生命周期函数它的调用时机。

    beforeCreate & created

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

    Vue.prototype._init = function (options?: Object) {
      // ...
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
      // ...
    }
    

    可以看到beforeCreatecreated的钩子调用是在initState的前后,initState的作用是初始化propsdatamethodswatchcomputed等属性。那么显然beforeCreate的钩子函数中就不能获取到propsdata中定义的值,也不能调用methods中定义的函数。

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

    beforeMount & mounted

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

    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      // ...
      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)
        }
      }
    
      // we set this to vm._watcher inside the watcher's constructor
      // since the watcher's initial patch may call $forceUpdate (e.g. inside child
      // component's mounted hook), which relies on vm._watcher being already defined
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      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
    }
    

    在执行vm._render()函数渲染VNode之前,执行了beforeMount钩子函数,在执行完vm._update()VNode patch到真实DOM后,执行mounted钩子。注意,这里对mounted钩子函数执行有一个判断逻辑,vm.$vnode如果为null,则表明这不是一次组件的初始化过程,而是我们通过外部new Vue初始化过程。那么对于组件,它的mounted时机在哪儿呢?

    组件的VNode patch到DOM后,会执行invokeInsertHook 函数,把insertedVnodeQueue里保存的钩子函数依次执行一遍,它的定义在src/core/vdom/patch.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])
        }
      }
    }
    

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

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

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

    beforeUpdate & updated

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

    beforeUpdate的执行时机是在渲染Watcherbefore函数中:

    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      // ...
    
      // we set this to vm._watcher inside the watcher's constructor
      // since the watcher's initial patch may call $forceUpdate (e.g. inside child
      // component's mounted hook), which relies on vm._watcher being already defined
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      // ...
    }
    

    注意这里有个判断,也就是在组件已经mounted之后,才会去调用这个钩子函数。

    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以及组件已经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钩子函数。

    beforeDestroy & destroyed

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

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

    beforeDestroy钩子函数的执行时机是在$destroy函数执行最开始的地方,接着执行了一系列的销毁动作,包括从parent$children中删掉自身,删除watcher,当前渲染的VNode执行销毁钩子函数等,执行完毕后再调用destroy钩子函数。

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

    activated & deactivated

    activateddeactivated钩子函数是专门为keep-alive组件定制的钩子。

    总结

    通过分析我们知道了Vue生命周期中各个钩子函数的执行时机以及顺序,如在created钩子函数中可以访问到数据,在mounted钩子函数中可以访问到DOM,在destroy钩子函数中可以做一些定时器销毁工作,了解它们有利于我们在合适的生命周期去做不同的事情。

    相关文章

      网友评论

          本文标题:Vue源码分析—组件化(四)

          本文链接:https://www.haomeiwen.com/subject/eztshctx.html