Cosen95 / blog

关注行业前沿,分享所见所学。持续输出优质文章 :rocket:
212 stars 15 forks source link

Vue源码探秘(生命周期) #32

Open Cosen95 opened 4 years ago

Cosen95 commented 4 years ago

引言

在创建一个 Vue 实例的时候需要经过一系列的初始化过程,比如设置数据监听编译模板挂载实例到 DOM、在数据变化时更新 DOM 等。

同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

下面引用官网的一张图,这张图展示了Vue 实例的生命周期以及在它生命周期的各个阶段分别调用的钩子函数: vue-lifecycle

除了上图中展示的之外,还有activateddeactivated ,这两个是和 keep-alive 相关的函数。

callHook

回顾 _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");

  // ...
};

这里调用了两次 callHook 函数,分别执行了生命周期钩子函数 beforeCreatecreated 。来看 callHook 函数的定义:

// src/core/instance/lifecycle.js

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

callHook 函数接收两个参数,一个是 vm 实例,一个是要执行的钩子函数名。这里通过 vm.$options[hook] 拿到对应的函数数组,然后遍历这个数组调用 invokeWithErrorHandling 函数。 invokeWithErrorHandling 函数定义如下:

export function invokeWithErrorHandling(
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res;
  try {
    res = args ? handler.apply(context, args) : handler.call(context);
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`));
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true;
    }
  } catch (e) {
    handleError(e, vm, info);
  }
  return res;
}

invokeWithErrorHandling 函数主要逻辑就是执行传入的 handler 函数。在调用 invokeWithErrorHandling 函数的时候传入 vm 作为 context 参数,也就是说生命周期函数的 this 会指向当前实例 vm 。另外这里设置一个标识符 _handled 保证函数只被调用一次,避免递归调用。

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

beforeCreate & created

beforeCreatecreated 这两个钩子函数的调用时机前面也提到过了,在执行 _init 函数时被调用:

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");

可以看到,在完成初始化生命周期事件render 后调用了 beforeCreate 。在调用 beforeCreate 之后才调用 initState 。也就是说在 beforeCreate 函数中是访问不到 dataprops 等属性的,因为这个时候还没有初始化。

created 是在初始化 dataprops 后才被调用,因此在 created 中可以访问这些属性。

beforeMount & mounted

beforeMountmounted 这两个的调用时机是什么时候呢?

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

// src/core/instance/lifecycle.js

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  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;
}

可以看到,在组件挂载前就会调用 beforeMount 函数,然后在执行了一系列挂载操作后,在最后的 if 语句判断这个 vm 是外部 new Vue 的实例还是内部的组件实例

组件实例会有一个 $vnode 属性,指向组件的占位符 VNode

如果是外部实例则执行 mounted 函数。

因此组件实例的 mounted 函数调用时机不在 mountComponent 函数中,那是在什么地方呢?

回顾 patch 函数:

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // ...

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
}

组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook 函数,把 insertedVnodeQueue 里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js 中:

// 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 中:

// src/core/vdom/create-component.js

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 */);
      }
    }
  }
};

可以看到,组件的 mounted 就是在这里通过 callHook 调用的。

beforeUpdate & updated

beforeUpdateupdated 是和数据更新相关的,数据更新这一部分会在下一章详细讲解。

beforeUpdate 的调用时机在 mountComponent 创建 Watcher 实例时:

// src/core/instance/lifecycle.js

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...

  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      }
    },
    true /* isRenderWatcher */
  );
  hydrating = false;

  // ...
}

Watcher 的参数中有一个对象,对象中有一个 before 函数,这个函数判断如果组件已经 mounted 并且还没有 destroyed ,就调用 callHook 执行 beforeUpdate

before 函数的执行时机是在 flushSchedulerQueue 函数调用的时候,它被定义在 src/core/observer/scheduler.js 中:

// src/core/observer/scheduler.js

function flushSchedulerQueue() {
  // ...

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== "production" && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          "You may have an infinite update loop " +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        );
        break;
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice();
  const 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");
  }
}

现在我们只需要知道这里的 queue 是一个个 WatcherflushSchedulerQueue 函数会遍历 queue 然后执行每一个 Watcherbefore 方法。

flushSchedulerQueue 函数中还调用了 callUpdatedHooks 函数:

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

可以看到 updated 是在这里被调用的。

beforeDestroy & destroyed

beforeDestroydestroyed 都在执行 $destroy 函数时被调用。$destroy 函数是定义在 Vue.prototype 上的一个方法,在 src/core/instance/lifecycle.js 文件中:

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

可以看到在 $destroy 函数一开始就调用了 beforeDestroy ,然后执行一系列销毁操作后再调用 destroyed ,这些销毁操作会在后面章节再来具体分析。

这里调用了我们之前介绍过的__pacth__ 函数,实际上调用__pacth__函数后会触发子组件的 $destroy 函数,然后又执行__pacth__ 函数。

也就是说会通过递归调用先父后子的顺序把组件一层一层地销毁掉。因此 beforeDestroy 的调用顺序是先父后子,因为它会随着递归被调用;而 destroyed 是递归结束后执行,因此执行顺序是先子后父

总结

这一小节我们学习了生命周期函数的调用时机以及执行顺序。大概整理一下就是: