【Vue源码】11.组件化-生命周期

组件化-生命周期

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

lifecycle.png

相信使用 Vue 的我们,每天都要和生命周期打招呼,在各个生命周期处理不同的事情,比如我喜欢在 created 阶段调用接口,在 mounted 阶段操作 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 主要做了几件事:

  1. 根据传入的字符串 hook 名称,拿到对应的回调函数数组
  2. 循环回调函数数组,然后执行,执行的时候把 vm 作为函数执行上下文

在上一节 组件化-合并配置 中,我们介绍过在合并 options 的过程中,各个阶段的生命周期也被作为数组合并到了 vm.$options 中,因此调用 callHook 时就可以把里面的回调全部执行完。

在了解执行/触发方式之后,继续看一下每个生命周期的调用时机。

beforeCreate & created

beforeCreate & created 是在初始化实例 Vue 的阶段调用的,他定义在 _init 中:

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");
  // ...
};
复制代码

可以看到在 initState 前后分别调用了 beforeCreatecreated
initState 的作用是初始化 PropsMethodsDataComputedWatch 等,所以我们在 beforeCreate 时还拿不到 this.xxx 的值。
另外在这两个钩子执行的时候还没有 渲染 DOM,所以不能 操作 DOM

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;
}
复制代码

在执行 updateComponent 之前调用 beforeMount,在执行完 updateComponentVNode patch真实 DOM 后执行 mounted
注意在 mounted 时有一个判断,如果是组件的方式,则不会在这里触发钩子函数,只有手动调用:new Vue(options) 时才会调用。

组件的调用时机和代码在 invokeInsertHook 函数中,之前我们提到过,组件的 VNode patchDOM 后,会执行 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");
    }
    // ...
  },
};
复制代码

注意:insertedVnodeQueue 的添加顺序是先子后父,所以对于同步渲染的子组件来说,mounted 的执行顺序也是先子后父。

beforeUpdate & updated

beforeUpdate & updated 的触发时机是在数据更新的时候,这个涉及到数据双向绑定、更新,后面细讲。

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 */
  );
  // ...
}
复制代码

这时候生成一个渲染 Watcher,每次更新时触发 beforeUpdate

updated 的执行时机是在 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");
    }
  }
}
复制代码

在这里我们大概了解一下 flushSchedulerQueue,后面会细讲。
updatedQueue 是更新了的 watcher 数组,然后调用 callUpdatedHooks 进行遍历,只有满足 vm._watcher === watcher && vm._isMounted 才会触发 updated

我们之前提到在 mount 过程(调用 mountComponent)中会实例化一个 渲染Watcher

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 = true,则把当前 watcher 赋值给 vm._watcher

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

beforeDestroy & destroyed 是在组件销毁阶段触发的,组件最终会调用 $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,然后从 parent 中移除自身,删除 watcher,移除 observer 的引用,执行 vm.__patch__(vm._vnode, null) 触发子组件的销毁钩子函数,也是递归调用,和 mounted 一样,先子后父。

activated & deactivated

activated & deactivatedKeepAlive 的专属生命周期钩子。

总结

这里主要介绍了各个生命周期的钩子是在什么时机被触发的,通过这些知道在 created 时可以访问 propsdatamethod等。在 mounted 阶段能够进行 DOM 操作。在 destroy 阶段可以清除定时器、DOM 引用赋值为 null 等,了解这些可以让我们在不同的生命周期中调用方法和操作时游刃有余。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘

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