【Vue源码】3.Vue 实例挂载的实现

1,258 阅读1分钟

Vue 实例挂载的实现

上一节 中,我们看到会调用 vm.$mount(vm.$options.el); 来挂载实例,那么 $mount 是什么? $mount 方法在多个文件中都有定义,分别对应不同构建方式和平台。因为我们这次主要看 runtime-with-compiler,所以先看一下 src/platform/web/entry-runtime-with-compiler.js 中的定义。

// 先保存之前通用的 $mount 方法,定义在 src/platform/web/runtime/index.js
// 此处定义的是针对于 entry-runtime-with-compiler 的 $mount方法
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // el 可以传入字符串或者DOM对象
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== "production" &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      );
    return this;
  }

  const options = this.$options;
  // resolve template/el and convert to render function
  // 生成 render 函数
  if (!options.render) {
    let template = options.template;
    if (template) {
      if (typeof template === "string") {
        // Vue.component('anchored-heading', {
        //   template: '#anchored-heading-template',
        // })
        if (template.charAt(0) === "#") {
          // 会寻找template的内容
          template = idToTemplate(template);
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            );
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        if (process.env.NODE_ENV !== "production") {
          warn("invalid template option:" + template, this);
        }
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
      // 此时,template为字符串
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
        mark("compile");
      }
      // 生成 render 函数
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments,
        },
        this
      );
      options.render = render;
      options.staticRenderFns = staticRenderFns;

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
        mark("compile end");
        measure(`vue ${this._name} compile`, "compile", "compile end");
      }
    }
  }
  return mount.call(this, el, hydrating);
};
  1. 在此处,先缓存了原型上的 $mount 保存在 mount 中。
  2. 重新定义$mount,传入参数为 el
  3. el 做限制,不能传入 htmlbody 这样的根节点。
  4. 如果没有 options.render 方法,则会获取 template 字符串(根据 template 或者 el)。
  5. template 作为参数传入 compileToFunctions 生成 render 方法。编译过程暂时不看。
  6. 最后调用 mount

原型上的$mount 定义在src/platform/web/runtime/index.js中。

// 通用的 $mount 函数
// 在不同的平台有可能被改写专用的 $mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

$mount 函数支持传入两个参数,第一个是 el,表示挂载的元素,可以为字符串或者 DOM,第二个参数和服务端渲染有关,我们不需要。 $mount 方法实际上会调用mountComponent方法,定义在src/core/instance/lifecycle.js

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== "production") {
      /* istanbul ignore if */
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== "#") ||
        vm.$options.el ||
        el
      ) {
        warn(
          "You are using the runtime-only build of Vue where the template " +
            "compiler is not available. Either pre-compile the templates into " +
            "render functions, or use the compiler-included build.",
          vm
        );
      } else {
        warn(
          "Failed to mount component: template or render function not defined.",
          vm
        );
      }
    }
  }
  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
  // 在此处为渲染 Watcher
  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;
}

mountComponent 方法首先会校验开发环境和 options,然后触发 beforeMount 生命周期,如果不是开发模式,则定义 updateComponent 函数。

核心是实例化一个渲染 Watcher,在他的回调中调用 updateComponent,在此方法中先调用 vm._render 生成虚拟 Node,最终调用vm._update 更新 DOMWatcher 在这里起到两个作用,一是初始化的时候执行回调函数,二是在监测到数据变化时执行回调函数。关于 _render_update 在后面介绍。

最后判断为根节点的时候设置 vm._isMountedtrue, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

总结

Vue 实例挂载的实现中,最核心的就是 mountComponent 方法,其逻辑也很清晰,他会完成整个渲染工作。接下来会着重看_render_update 这两个方法。

源码分析 GitHub 地址