Vue.js 源码学习 - 初始化

2022/7/7

对于Vue.js 技术揭秘与 Vue 源码的学习笔记

构建与入口

Vue.js 通过执行node scripts/build.js进行构建

if (process.argv[2]) {
  const filters = process.argv[2].split(",");
  builds = builds.filter((b) => {
    return filters.some((f) => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1);
  });
} else {
  builds = builds.filter((b) => {
    return b.output.file.indexOf("weex") === -1;
  });
}

build(builds);

这段代码针对构建命令npm run build -- web-runtime-cjs,web-server-renderer传入的参数不同进行过滤,向build函数中传入了不同的参数,这些参数都配置在scripts/config.js中,声明了编译的入口、出口、格式、环境

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  "web-runtime-cjs-dev": {
    entry: resolve("web/entry-runtime.js"),
    dest: resolve("dist/vue.runtime.common.dev.js"),
    format: "cjs",
    env: "development",
    banner,
  },
  "web-runtime-cjs-prod": {
    entry: resolve("web/entry-runtime.js"),
    dest: resolve("dist/vue.runtime.common.prod.js"),
    format: "cjs",
    env: "production",
    banner,
  },
  //...
};

Runtime Only

该版本的 Vue.js 需要借助 vue-loader 编译 .vue 文件

Runtime + Compiler

该版本的 Vue.js 带有编译器,能够在客户端将 template 编译成 render 函数,大致效果可以理解为下面的两段代码

new Vue({
  template: "<div>{{ hi }}</div>",
});

new Vue({
  render(h) {
    return h("div", this.hi);
  },
});

入口文件

之前提到config.js中声明了编译版本的入口文件,这样就很方便的找到了 Runtime + Compiler 版本的入口文件所在的位置src/platforms/web/entry-runtime-with-compiler.js

可以看到该文件的最底部有一个export default Vue,我们写 Vue 项目时的import Vue from 'vue就是从这里倒入的(这里是构建前的代码,实际上是从构建后的代码中倒入),不过一般情况下我们开发使用的是 Runtime Onlysrc/platforms/web/entry-runtime.js

这个入口文件中 Vue 的来源是import Vue from './runtime/index',而在src/platforms/web/runtime/index.js中可以看到,Vue 真正初始化的位置是src/core/index.js

import Vue from "./instance/index";
import { initGlobalAPI } from "./global-api/index";
import { isServerRendering } from "core/util/env";
import { FunctionalRenderContext } from "core/vdom/create-functional-component";

initGlobalAPI(Vue);

// ...

export default Vue;

在这里通过initGlobalAPI(Vue)初始化了 Vue 的全局 API,我们见到的Vue.setVue.nextTickVue.use等 API 都是在这里挂载到 Vue 上的

而 Vue 本身声明在src/core/instance/index.js

import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

export default Vue;

new Vue

当我们new Vue({ ... })初始化时,调用了this._init方法,该方法是通过initMixin(Vue)挂载到 Vue 上的,定义在src/core/instance/init.js中,而_init方法主要逻辑就是进行一些初始化行为:生命周期、data、props 等内容的初始化

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

vm.$mount

在 init 的最后会通过vm.$mount挂载 Vue 实例,而$mount在不同平台、构建方式下都有所不同。

首先是src/platforms/web/runtime/index.js中定义了一个通用的$mount,无论是在哪个平台、哪个构建方式下最终都会调用这个通用的$mount,只不过会有一些不同的前置操作

// public mount method
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

src/platforms/web/entry-runtime-with-compiler.js中就是实现缓存了该$mount之后重写了它

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  //...

  return mount.call(this, el, hydrating);
};

在该模式下,由于需要编译 template,$mount增加了一些前置操作

首先是禁止挂载到 html 或 body 上

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

然后通过判断传入的是 template 还是 render 方法,如果是 template 则需要转化为 render 方法,转化所调用的compileToFunctions引入自src/compiler/index.js,compiler 目录下包含了 Vue.js 所有的编译相关的代码

if (!options.render) {
  let template = options.template;
  if (template) {
    if (typeof template === "string") {
      if (template.charAt(0) === "#") {
        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) {
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      mark("compile");
    }

    const { render, staticRenderFns } = compileToFunctions(
      template,
      {
        outputSourceRange: process.env.NODE_ENV !== "production",
        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");
    }
  }
}

最后就是调用缓存的mount,在这个方法中调用了mountComponent,也就是说真正挂载 Vue 实例的函数是src/core/instance/lifecycle.js中的mountComponent

mountComponet的核心逻辑就是通过vm._render生成 VNode 以及通过vm._update更新 DOM,在初始化时会通过new Watcher(Watcher 的构造函数)执行一遍vm._updatevm._render

render

vm._render声明在src/core/instance/render.js中,该方法最关键的逻辑就是对于用户声明的 render 或是通过 template 编译出的 render 的调用

vnode = render.call(vm._renderProxy, vm.$createElement);

这个vm._renderProxy实际上就是 this,在初始化的逻辑中可以找到,vm.$createElementsrc/core/vdom/create-element.js中的createElement函数,用户手写的 render 时的入参就是该函数。createElement最终会创建 VNode 并返回。

new VNode之前会进行 children 的规范化操作,将 children 变为VNode[]

if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children);
}

update

vm._update会将vm.render返回的 vnode 传入vm.__patch__中,渲染为真实 DOM,vm.__patch__在 web 和 weex 上实现方法是不同的,因此分别定义在了src/platforms的 web 和 weex 中

vm.__patch__src/core/vdom/patch.jscreatePatchFunction创建出来的patch函数。

createPatchFunction的入参是一些定义好的 DOM 操作函数和一些钩子函数,采用这样的做法而不是在src/core/vdom/patch.js中直接 import 的原因是 web 和 weex 的这些 DOM 操作和钩子不同,但是创建vm.__patch__使用的是同一个createPatchFunction

初始化的 patch

new Vue({
  el: "#app",
  render: function (createElement) {
    return createElement(
      "div",
      {
        attrs: {
          id: "app",
        },
      },
      this.message
    );
  },
  data: {
    message: "Hello Vue!",
  },
});
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

入参中的vm.$el是在$mount时获取到的<div id="app">hydrating在非服务端渲染时都是false

patch会将vm.$el转为 VNode,之后通过createElm将 VNode 创建为真实 DOM 并插入父节点

if (isRealElement) {
  // mounting to a real element
  // check if this is server-rendered content and if we can perform
  // a successful hydration.
  if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    oldVnode.removeAttribute(SSR_ATTR);
    hydrating = true;
  }
  if (isTrue(hydrating)) {
    // ...
  }
  // either not server-rendered, or hydration failed.
  // create an empty node and replace it
  oldVnode = emptyNodeAt(oldVnode);
}

// replacing existing element
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);

// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  // extremely rare edge case: do not insert if old element is in a
  // leaving transition. Only happens when combining transition +
  // keep-alive + HOCs. (#4590)
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
);
// ...
// function createElm

vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode);

// ...

if (isDef(tag)) {
  if (process.env.NODE_ENV !== "production") {
    if (data && data.pre) {
      creatingElmInVPre++;
    }
    if (isUnknownElement(vnode, creatingElmInVPre)) {
      warn(
        "Unknown custom element: <" +
          tag +
          "> - did you " +
          "register the component correctly? For recursive components, " +
          'make sure to provide the "name" option.',
        vnode.context
      );
    }
  }

  vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode);
  setScope(vnode);

  /* istanbul ignore if */
  if (__WEEX__) {
    // ...
  } else {
    createChildren(vnode, children, insertedVnodeQueue);
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    insert(parentElm, vnode.elm, refElm);
  }

  if (process.env.NODE_ENV !== "production" && data && data.pre) {
    creatingElmInVPre--;
  }
} else if (isTrue(vnode.isComment)) {
  // 注释
  vnode.elm = nodeOps.createComment(vnode.text);
  insert(parentElm, vnode.elm, refElm);
} else {
  // 文本
  vnode.elm = nodeOps.createTextNode(vnode.text);
  insert(parentElm, vnode.elm, refElm);
}

其中的createChildren是对于createElm的递归调用,遍历所有的子节点,它的入参insertedVnodeQueue是通过invokeCreateHooks创建的,它会将 vnode 放到insertedVnodeQueue中。

最终patch就会将完整的 DOM 树创建出来