Vue2.x-源码分析(一)

26 阅读6分钟

Vue入口

首先,先来分析当执行如下代码的时候,内部vue是如何初始化的?

import Vue from 'vue'

node_modules 目录下找到vue的安装目录,查看 package.json 文件,scripts标签如下:

{
  "name": "vue",
  "version": "2.6.14",
  ...
  "scripts": {
    ...
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    ...
  },
  ...
}

查看 build 相关的命令,可以看出构建命令实际是执行的 scripts/build.js 文件,其他只是添加了不同的参数。

在 build.js 文件中,只是对构建过程和参数做了一些基本的过滤和判断,实际读取了 config.js 下的配置:

const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const rollup = require("rollup");
const terser = require("terser");
​
if (!fs.existsSync("dist")) {
  fs.mkdirSync("dist");
}
// 此处读取了所有配置内容
let builds = require("./config").getAllBuilds();
​
// filter builds via command line arg
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 {
  // filter out weex builds by default
  builds = builds.filter((b) => {
    return b.output.file.indexOf("weex") === -1;
  });
}
​
build(builds);

config.js 文件中:

const builds = {
  ...
  // Runtime+compiler CommonJS build (CommonJS)
  "web-full-cjs-dev": {
    entry: resolve("web/entry-runtime-with-compiler.js"),
    dest: resolve("dist/vue.common.dev.js"),
    format: "cjs",
    env: "development",
    alias: { he: "./entity-decoder" },
    banner,
  },
  "web-full-cjs-prod": {
    entry: resolve("web/entry-runtime-with-compiler.js"),
    dest: resolve("dist/vue.common.prod.js"),
    format: "cjs",
    env: "production",
    alias: { he: "./entity-decoder" },
    banner,
  },
  ...
};

config.js 文件中配置的是针对不同环境和场景的 Vue.js 构建的配置,遵循了 Rollup 构建规则,entry 是构建入口。运行时+编译时的配置项web/entry-runtime-with-compiler.js 并不是真正的文件路径,查看 resolve 函数是如何寻找真实地址的:

const aliases = require("./alias");
const resolve = (p) => {
  const base = p.split("/")[0]; // 按 / 分割传入的字符串,首个为 base
  // aliases 为 base 配置的别名,通过别名配置加载真实的文件地址
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1));
  } else {
    return path.resolve(__dirname, "../", p);
  }
};

alias.js 如下:

const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'), // web端别名配置
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

所以,web 端的真实源码位置在 src/platforms/web/ 下,即 web-full-cjs-dev 配置对应的入口文件就是 src/platforms/web/entry-runtime-with-compiler.js ,因此当执行 import Vue from 'vue' 时,就是从此文件开始的:

// src/platforms/web/entry-runtime-with-compiler.js/* @flow */import Vue from "./runtime/index";
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
...
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component { ... };
​
...
​
Vue.compile = compileToFunctions;
​
export default Vue;

可见,此文件并不是 Vue 定义的地方,只是对 Vue 对象的扩展,继续向下寻找,./runtime/index.js :

// src/platforms/web/runtime/index.js
/* @flow */
​
import Vue from "core/index";
import config from "core/config";
...
// install platform specific utils
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
​
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
​
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;
​
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};
...
export default Vue;

此文件依旧是对 Vue 对象的扩展,继续向下寻找,core/index.js :

// src/core/index.jsimport 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); // 初始化全局API
...
Vue.version = "__VERSION__";
​
export default Vue;

继续查看 ./instance/index.js :

// src/core/instance/index.jsimport { 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;

至此,找到了 Vue 定义的地方,是一个函数,并且发现,Vue 相关的像生命周期、初始化、状态、事件、渲染等功能都分散在不同的模块中,通过 xxxMixin 函数进行注册和初始化,这样做很好的分离了代码结构,从功能上看更加清晰和直观,方便管理和维护,这也是使用 Function 而非 Class 作为构造函数的原因。

另外,在 src/core/index.js 文件中,initGlobalAPI(Vue) 的执行,是对 Vue 全局API的扩展:

// src/core/global-api/index.js/* @flow */
...
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)
​
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
​
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
​
  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
​
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
​
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
​
  extend(Vue.options.components, builtInComponents)
​
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

此文件中注册的全局API,看上去就比较熟悉了,在官网的文档中都可以找到。至此,Vue 初始化过程就基本梳理完了。

接下来,我们看看 new Vue() 的过程发生了什么?

new Vue()

通过上面的分析,我们知道 Vue 本质是一个函数,但我们使用 Vue 的时候,只能通过 new Vue() 来使用,在 javascript 中类确实可以用函数来实现,但函数却不只用来声明类,通过上面的分析应该明白了其中的关键,如果还不清楚,请再看一遍上面的内容,哈哈哈。

new Vue()后,执行 Vue 函数,内部执行了 this._init()方法:

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); // 该函数为Vue的注入了_init()
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

initMixin(Vue) 在Vue的原型上注入了_init()方法:

// src/core/instance/init.js/* @flow */
...
export function initMixin(Vue: Class<Component>) {
  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;
    // 合并配置
    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;
    }
    // 一系列初始化
    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);
    }
  };
}

可以看出,_init()初始化过程主要包括合并配置和一系列初始化,包括生命周期、事件、渲染、data、props等,并且执行了 beforeCreatecreated 两个生命周期钩子。最后通过判断是否存在 el 属性,调用$mount 方法挂载 vm,挂载的过程实际就是把模版渲染成真实DOM的过程。

通过一系列初始化可以发现,在 beforeCreate 生命周期钩子内是无法访问到data、props、methods等配置的,因为还没有初始化,在 created 钩子中可以。

接下来看看 vm 的挂载流程都做了什么?

在构建的分析过程中,构建入口文件 src/platforms/web/entry-runtime-with-compiler.js 中,有对 mount 方法的定义:

// src/platforms/web/entry-runtime-with-compiler.js/* @flow */
...
// 先拿出原型链上的$mount方法,再自定义$mount方法
// 最后 return mount.call(this, el, hydrating); 再执行原$mount
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el);
​
  // 不可以挂载在body和html元素上
  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;
  // 这里的render是 new Vue({ render:(){} }) 时可用户自定义的
  // 如果没有定义render函数,会判断有没有template,最后通过 compileToFunctions 方法把template编译成render函数
  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");
      }
    }
  }
  return mount.call(this, el, hydrating);
};
...
export default Vue;

从代码中可以看出,mount方法并不是此处首次定义,在源码中也发现有多个位置定义了此方法,其中原因主要是挂载流程是跟平台和构建流程相关的,不同的平台下,有不同的挂载方法。这样不同的构建流程中,对挂载有不同的处理,处理完异同后,再合流到统一的挂载中去。所以,在当前代码中的构建入口,先缓冲原型链上的mount方法,在定义自己的mount方法,执行完自定义的mount方法后,再通过 call 方法执行缓存的mount方法。其中有几个关键点:

  • Vue 不能挂载在 body、html 节点上
  • 如果用户没有自定义 render 方法,判断有没有 template ,如果没有,再判断有没有 el ,最后都会拿到一个 template ,然后通过 compileToFunctions 方法把 template 编译成 render 。在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template属性,最终都会转换成 render 方法;

执行完自定义的mount方法, 会调用原型链上定义的原始mount方法,此方法是在 src/platform/web/runtime/index.js 中定义的:

// src/platform/web/runtime/index.js// 公共的mount方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

然后去调用 mountComponent 方法:

// src/core/instance/lifecycle.jsexport function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  
  // 执行 beforeMount 钩子
  callHook(vm, "beforeMount");
​
  let updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && config.performance && mark) {
    // updateComponent 声明
    updateComponent = () => {
      ...
      const vnode = vm._render(); // 关键:执行_render()生成vnode
      vm._update(vnode, hydrating); // 关键:根据vnode执行_update生成真实dom
      ...
    };
  } else {
    // updateComponent 声明
    updateComponent = () => {
      vm._update(vm._render(), hydrating); // 关键:同上
    };
  }
​
  // 创建渲染Watcher实例
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        // 根据_isMounted的不同执行beforeUpdate钩子
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  hydrating = false;
​
  // 根据vnode的状态,执行mounted钩子
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, "mounted");
  }
  return vm;
}

这就是挂载的流程,核心是先实例化一个渲染Watcher,然后Watcher的回调函数中会执行updateComponent方法,这个方法里执行的比较关键,是调用_render方法生成vnode,然后把vnode传入_update方法更新DOM。渲染Watcher顾名思义会监测数据的变化,然后执行回调函数,然后执行不同的生命周期钩子。在此阶段的挂载过程中,会有三个生命周期钩子 beforeMount、mounted、beforeUpdate 在不同时机执行。

接下来继续看 _render(生成VNode)和 _update(更新DOM)。

_render方法是Vue实例的一个私有方法,目的是生成虚拟dom,定义在 src/core/instance/render.js 文件中,其中最关键的是执行mount阶段生成的 render 方法:

Vue.prototype._render = function (): VNode {
  ...
  let vnode
  try {
    // 关键:调用mount阶段生成的render的函数,此render函数可用户自定义,或者通过template编译生成
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    ...
  }
  // 如果不是有效的vnode,则生成一个空vnode兜底
  if (!(vnode instanceof VNode)) {
    ...
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

从这里可以看出,我们的业务代码中:

new Vue({
  render: function (createElement) {
    return createElement('div', {
       attrs: {
          id: 'app'
        },
    }, this.message)
  }
})

render函数中传入的 createElement 参数就是 vm.$createElement 方法,此方法定义如下:

// 这个是通过template模版编译而成的render函数使用的
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 这个是用户自定义的render函数使用的
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

最终又调到了createElement 方法:

// src/core/vdom/create-element.js
​
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 对参数做一些规范处理后,实际调用_createElement()
  return _createElement(context, tag, data, children, normalizationType)
}
​
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 规范化children
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 内置元素,直接创建vnode
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } 
    // 创建组件vnode
    else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

此方法的核心流程包括两个:

  1. children的规范化:由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。children的规范化是指要把createElement函数传入的第四个参数 children (任意类型)规范为VNode类型。比如:render函数是用户自己手写的,当子节点children只有一个节点的时候,Vue允许使用基本类型创建单个文本节点,这种情况会调用 createTextVNode 创建一个文本节点的 VNode。
  2. VNode的构建:虚拟Node的构建会根据 tag 的不同做判断:
  • 如果 tag 是 string 类型,则接着判断,如果是内置的一些节点,则直接创建一个普通 VNode;
  • 如果 tag 是为已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode;
  • 如果 tag 是一个 Component 类型,则直接调用 createComponent 创建一个组件类型的 VNode 节点。

对于 createComponent 创建组件类型的 VNode 的过程,本质上它还是返回了一个 VNode。

因此,createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree。

VNode创建完成后,接下来就是把虚拟dom转换成真实dom,此过程是在 _update 方法中完成的:

// src/core/instance/lifecycle.js
​
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  const restoreActiveInstance = setActiveInstance(vm);
  vm._vnode = vnode;
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  restoreActiveInstance();
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null;
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm;
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el;
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
};

可以发现 _update 的核心就是调用 vm.__patch__ 方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js中:

// src/platforms/web/runtime/index.js
...
Vue.prototype.__patch__ = inBrowser ? patch : noop
...

其中,patch 方法是 createPatchFunction 方法的返回值,这里传入了一个对象backend,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现。

// src/core/vdom/patch.js
​
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
​
  const { modules, nodeOps } = backend
​
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
​
    let isInitialPatch = false
    const insertedVnodeQueue = []
​
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        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)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 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)
        )
​
        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }
​
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
​
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

patch 方法接收 4个参数:

  1. oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;
  2. vnode 表示执行 _render 后返回的 VNode 的节点;
  3. hydrating 表示是否是服务端渲染;
  4. removeOnly 是给 transition-group 用的;

patch 方法的目的是生成真实dom,是通过 createElm 方法实现的,并且完成的父子节点的连接,在createElm内部又会调用createChildren生成子元素,然后通过insert方法插入父节点。这样通过递归循环调用,完成dom树的构建。

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  ...
  // 创建组件节点
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
​
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    ... 
    // nodeOps封装的dom操作方法生成真实dom
    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)
  }
}

至此,new Vue() 完成了真实dom的创建。