vue生命周期底层实现

643 阅读2分钟

高频面试题:vue中的生命周期是怎么实现的?

首先我们知道生命周期钩子函数共11个,分别为beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedactivateddeactivatedbeforeDestroydestroyederrorCaptured

通过例子来分别分析生命周期的底层实现。

// 当前代码在main.js文件中
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
// 组件定义,用于演示beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated和deactivated
const comA = {
  data() {
    return {
      count: 1
    };
  },
  template: `<div>
    <button @click='changeCount'>递增</button>
    <span>{{count}}</span>
  </div>`,
  methods: {
    changeCount() {
      this.count++;
    }
  },
  beforeCreate() {
    console.log("----beforeCreate----");
  },
  created() {
    console.log("----created----");
  },
  beforeMount() {
    console.log("----beforeMount----");
  },
  mounted() {
    console.log("----mounted----");
  },
  beforeUpdate() {
    console.log("----beforeUpdate----");
  },
  updated() {
    console.log("----updated----");
  },
  activated() {
    console.log("----activated----");
  },
  deactivated() {
    console.log("----deactivated----");
  }
};
const comB = {
  template: `<div>
    <router-link to="/routerA">Go to routerA</router-link>
    <router-link to="/routerB">Go to routerB</router-link>
    <router-view></router-view>
  </div>`
};
Vue.component("comA", comA);
Vue.component("comB", comB);
// 路由定义,用于演示声明周期beforeDestroy和destroyed
const routerA = {
  template: "<div>routerA</div>",
  beforeDestroy() {
    console.log("----beforeDestroy----");
  },
  destroyed() {
    console.log("----destroyed----");
  }
};
const routerB = { template: "<div>routerB</div>" };
const routes = [
  { path: "/routerA", component: routerA },
  { path: "/routerB", component: routerB }
];
const router = new VueRouter({
  routes
});
// 错误组件定义,演示errorCaptured
const comErr = {
  template: `<div></div>`,
  render: {},
}
Vue.component("comErr", comErr);
// 演示入口:
new Vue({
  el: "#app",
  router,
  data() {
    return {
      currentComponet: "comA"
    };
  },
  methods: {
    changeComponent() {
      this.currentComponet = this.currentComponet === "comA" ? "comB" : "comA";
    }
  },
  errorCaptured(err, vm, info) {
    console.log("----errorCaptured----");
    console.log(err, vm, info);
  },
  template: `<div>
    <button @click='changeComponent'>当前组件为:{{currentComponet}}</button>
    <keep-alive>
      <component v-bind:is="currentComponet"></component>
    </keep-alive>
    <comErr></comErr>
  </div>`,
});

1、beforeCreatecreated

在实例化Vue的过程中会执行初始化方法this._init

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

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

在执行callHook(vm, 'beforeCreate')之前完成了Vue的实例化、生命周期、事件和渲染函数的初始化。

在执行callHook(vm, 'created')之前完成了provideinject的处理,并且对methodsdatapropscomputedwatch等进行配置化处理。

2、beforeMountmounted

在初始化this._init最后执行vm.$mount(vm.$options.el),通过挂载节点的处理以及render的获取以后,会执行到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 && !vm._isDestroyed) {
        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
}

在执行callHook(vm, 'beforeMount')时,已完成了vm.$el的赋值和render函数的获取。

在执行callHook(vm, 'mounted')时,已通过vm._update(vm._render(), hydrating)的方式完成了render函数的渲染,视图进行展示。如果需要等到所有的子组件都完成挂载后再执行业务逻辑,可以借助this.$nextTick

3、beforeUpdateupdated

当数据发生变化时会执行发布者depnotify,最后会执行到flushSchedulerQueue

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;
  
  queue.sort(function (a, b) { return a.id - b.id; });

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // ...

  // keep copies of post queues before resetting state
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}
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')
    }
  }
}

在执行watcher.before()时,就执行了渲染Watcher中的before

before () {
  if (vm._isMounted && !vm._isDestroyed) {
    callHook(vm, 'beforeUpdate')
  }
}

beforeUpdate钩子函数在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。

当视图进行更新后,会执行到callUpdatedHooks(updatedQueue),即通过while循环的方式,依次执行其中的updated函数。当这个钩子被调用时,组件 DOM 已经更新,可以执行依赖于DOM的操作,如果需要等到所有的子组件都完成挂载后再执行业务逻辑,可以借助this.$nextTick

4、activateddeactivated

当前生命周期主要是针对动态组件在keepAlive组件中激活或者失活的场景。

(1)activated过程:

组件在patch过程中都会执行到invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)

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

其中的hook.insert指的是:

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
}

激活函数activateChildComponent

export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

执行到callHook(vm, 'activated')时,指的被keep-alive缓存的组件激活。

(2)deactivated过程:

当前例子中,当切换this.currentComponet的时候,会让组件comA失活,当执行到removeVnodes时会执行invokeDestroyHook方法:

function invokeDestroyHook (vnode) {
    var i, j;
    var data = vnode.data;
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
      for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j]);
      }
    }
  }

其中的i = data.hook && i = i.destroycomponentVNodeHooks中的destroy赋值给i:

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

失活函数deactivateChildComponent

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}

执行到callHook(vm, 'deactivated')时,指的被keep-alive缓存的组件失活。

5、beforeDestroydestroyed

// 在文件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
    }
  }
}

在执行callHook(vm, 'beforeDestroy')时,实例仍然完全可用。

在执行callHook(vm, 'destroyed')时,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

6、errorCaptured

在执行_render函数获取虚拟DOM的过程中,会对可能遇到的错误进行捕捉:

try {
  currentRenderingInstance = vm;
  vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
  handleError(e, vm, "render");
  // return error render result,
  // or previous vnode to prevent render error causing blank component
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
    try {
      vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
    } catch (e) {
      handleError(e, vm, "renderError");
      vnode = vm._vnode;
    }
  } else {
    vnode = vm._vnode;
  }
} finally {
  currentRenderingInstance = null;
}

这里因为定义的错误组件中的render不是函数,所以会执行到handleError(e, vm, "render")

export function handleError (err: Error, vm: any, info: string) {
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}

这里通过const hooks = cur.$options.errorCaptured的方式获取到errorCaptured函数,即例子中定义的errorCaptured函数,达到错误捕捉的目的。

总结

生命周期的目的是在实例化Vue的过程中,函数的不同阶段可以调用不同的钩子函数,根据不同阶段的不同特点进行业务逻辑的处理。

后记

如有纰漏,请贵手留言~