Skip to content
On this page

Vue 源码解析

准备工作

Flow

Flow 是 facebook 出品的 JavaScript 静态类型检查工具。

Vue.js 的源码利用了 Flow 做了静态类型检查,所以了解 Flow 有助于我们阅读源码。

为什么使用 Flow

JavaScript 是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至看上去都不会报错,但在运行阶段就可能出现各种奇怪的 bug。

类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。

项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性。 Vue.js 在做 2.0 重构的时候,在 ES2015 的基础上,除了 ESLint 保证代码风格之外,也引入了 Flow 做静态类型检查。之所以选择 Flow,主要是因为 Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力。

Flow 的工作方式

通常类型检查分成 2 种方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

安装 Flow

js
npm i flow-bin -g

初始化 Flow

js
flow init

类型判断

它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型。

这就是所谓的类型推断,Flow 最重要的特性之一。

通过一个简单例子说明一下:

js
// @flow

function split (str) {
  return str.split(' ')
}

split(11);

Flow 检查上述代码后会报错,因为函数 split 期待的参数是字符串,而我们输入了数字。

类型注释

如上所述,类型推断是 Flow 最有用的特性之一,不需要编写类型注释就能获取有用的反馈。但在某些特定的场景下,添加类型注释可以提供更好更明确的检查依据。

考虑如下代码:

js
// @flow

function add (x, y) {
  return x + y
}

add('Hello', 11);

Flow 检查上述代码时检查不出任何错误,因为从语法层面考虑, + 即可以用在字符串上,也可以用在数字上,我们并没有明确指出 add() 的参数必须为数字。

在这种情况下,我们可以借助类型注释来指明期望的类型。类型注释是以冒号 : 开头,可以在函数参数,返回值,变量声明中使用。

如果我们在上段代码中添加类型注释,就会变成如下:

js
// @flow

function add (x: number, y: number): number {
  return x + y
}

add('Hello', 11);

现在 Flow 就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。

上面的例子是针对函数的类型注释。接下来我们来看看 Flow 能支持的一些常见的类型注释。

Flow 类型

数组

js
// @flow

var arr: Array<number> = [1, 2, 3];

arr.push('Hello');

数组类型注释的格式是 Array<T>,T 表示数组中每项的数据类型。在上述代码中,arr 是每项均为数字的数组。

如果我们给这个数组添加了一个字符串,Flow 能检查出错误。

类和对象

js
// @flow

class Bar {
  x: string;           // x 是字符串
  y: string | number;  // y 可以是字符串或者数字
  z: boolean;

  constructor (x: string, y: string | number) {
    this.x = x;
    this.y = y;
    this.z = false;
  }
}

var bar: Bar = new Bar('hello', 4);
js
// @flow

var obj: { a: string, b: number, c: Array<string>, d: Bar } = {
  a: 'hello',
  b: 11,
  c: ['hello', 'world'],
  d: new Bar('hello', 3)
}

类的类型注释格式如上,可以对类自身的属性做类型检查,也可以对构造函数的参数做类型检查。

这里需要注意的是,属性 y 的类型中间用 | 做间隔,表示 y 的类型即可以是字符串也可以是数字。

对象的注释类型类似于类,需要指定对象属性的类型。

Null

想任意类型 T 可以为 null 或者 undefined,只需类似如下写成 ?T 的格式即可。

若想任意类型 T 可以为 null 或者 undefined,只需类似如下写成 ?T 的格式即可。

js
// @flow

var foo: ?string = null;

此时,foo 可以为字符串,也可以为 null。

目前我们只列举了 Flow 的一些常见的类型注释。如果想了解所有类型注释,请移步 Flow 的官方文档

Flow 在 Vue 源码中的应用

有时候我们想引用第三方库,或者自定义一些类型,但 Flow 并不认识,因此检查的时候会报错。为了解决这类问题,Flow 提出了一个 libdef 的概念,可以用来识别这些第三方库或者是自定义类型,而 Vue.js 也利用了这一特性。

在 Vue.js 的主目录下有 .flowconfig 文件, 它是 Flow 的配置文件,感兴趣的同学可以看官方文档。这其中的 [libs] 部分用来描述包含指定库定义的目录,默认是名为 flow-typed 的目录。

这里 [libs] 配置的是 flow,表示指定的库定义都在 flow 文件夹内。我们打开这个目录,会发现文件如下:

vue.js 配置如下:

js
[ignore]
.*/node_modules/.*
.*/test/.*
.*/scripts/.*
.*/examples/.*
.*/benchmarks/.*

[include]

[libs]
flow

[options]
unsafe.enable_getters_and_setters=true
module.name_mapper='^compiler/\(.*\)$' -> '<PROJECT_ROOT>/src/compiler/\1'
module.name_mapper='^core/\(.*\)$' -> '<PROJECT_ROOT>/src/core/\1'
module.name_mapper='^shared/\(.*\)$' -> '<PROJECT_ROOT>/src/shared/\1'
module.name_mapper='^web/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/web/\1'
module.name_mapper='^weex/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/weex/\1'
module.name_mapper='^server/\(.*\)$' -> '<PROJECT_ROOT>/src/server/\1'
module.name_mapper='^entries/\(.*\)$' -> '<PROJECT_ROOT>/src/entries/\1'
module.name_mapper='^sfc/\(.*\)$' -> '<PROJECT_ROOT>/src/sfc/\1'
suppress_comment= \\(.\\|\n\\)*\\$flow-disable-line

flow 目录

  • compiler.js 编译相关
  • component.js 组件数据结构
  • global-api.js Global API 结构
  • modules.js 第三方库定义
  • options.js 选项相关
  • ssr.js 服务端渲染相关
  • vnode.js 虚拟 node 相关

可以看到,Vue.js 有很多自定义类型的定义,在阅读源码的时候,如果遇到某个类型并想了解它完整的数据结构的时候,可以回来翻阅这些数据结构的定义。

总结

通过对 Flow 的认识,有助于我们阅读 Vue 的源码,并且这种静态类型检查的方式非常有利于大型项目源码的开发和维护。

类似 Flow 的工具还有如 TypeScript,感兴趣的同学也可以自行去了解一下。

Vue 源码目录设计

vue-dir

Vue.js 的源码都在 src 目录下,其目录结构如下。

  • compiler 编译相关

    compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。

    编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。

    显然,编译是一项耗性能的工作,所以更推荐前者——离线编译。

  • core 核心代码

    core 目录包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。

    这里的代码可谓是 Vue.js 的灵魂,也是我们之后需要重点分析的地方。

  • platforms 不同平台的支持

    Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客户端上。

    platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

    我们会重点分析 web 入口打包后的 Vue.js,对于 weex 入口打包的 Vue.js,感兴趣的同学可以自行研究。

  • server 服务端渲染

    Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。

    服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

  • sfc .vue 文件解析

    通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件的编写组件。

    这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

  • shared 共享代码

    Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

总结

从 Vue.js 的目录设计可以看到,作者把功能模块拆分的非常清楚,相关的逻辑放在一个独立的目录下维护,并且把复用的代码也抽成一个独立目录。这样的目录设计让代码的阅读性和可维护性都变强,是非常值得学习和推敲的。

Vue.js 源码构建

Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下。

Rollup 和 Webpack 都是构建工具,Webpack 更强大一些,会把 图片,JS 等静态资源通通编译成 JavaScript。

Rollup 更适合 JavaScript 库的编译,只处理 JS 部分,不处理其他资源。相对于 Webpack 更轻量,编译出的代码也更友好。

构建脚本

通常一个基于 NPM 托管的项目都会有一个 package.json 文件,它是对项目的描述文件,它的内容实际上是一个标准的 JSON 对象。

js
{
"name": "vue",
"version": "2.6.12",
"description": "Reactive, component-oriented view layer for modern web interfaces.",
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
"unpkg": "dist/vue.js",
"jsdelivr": "dist/vue.js",
"typings": "types/index.d.ts",
"files": [
	"src",
	"dist/*.js",
	"types/*.d.ts"
],
  "sideEffects": false,
  "scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  },
  "gitHooks": {
    "pre-commit": "lint-staged",
    "commit-msg": "node scripts/verify-commit-msg.js"
  },
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "git add"
    ]
  },
  "author": "Evan You",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/vuejs/vue/issues"
  },
  "homepage": "https://github.com/vuejs/vue#readme",
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}

main 字段是 npm 包的入口,import vue 会通过 main 查找入口。

module 和 main 是类似的,在 webpack2 以上,会把 module 作为默认入口。

我们通常会配置 script 字段作为 NPM 的执行脚本,Vue.js 源码构建的脚本如下:

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

这里总共有 3 条命令,作用都是构建 Vue.js,后面 2 条是在第一条命令的基础上,添加一些环境参数。

当在命令行运行 npm run build 的时候,实际上就会执行 node scripts/build.js,接下来我们来看看它实际是怎么构建的。

构建过程

在 scripts/build.js 中:

js
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)

这段代码逻辑非常简单,先从配置文件读取配置,再通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的 Vue.js 。

在 scripts/config.js 中:

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+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
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
    format: 'es',
    transpile: false,
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
    format: 'es',
    transpile: false,
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // ...
}

这里列举了一些 Vue.js 构建的配置,关于还有一些服务端渲染 webpack 插件以及 weex 的打包配置就不列举了。

对于单个配置,它是遵循 Rollup 的构建规则的。其中 entry 属性表示构建的入口 JS 文件地址,dest 属性表示构建后的 JS 文件地址。format 属性表示构建的格式,cjs 表示构建出来的文件遵循 CommonJS 规范,es 表示构建出来的文件遵循 ES Module 规范。 umd 表示构建出来的文件遵循 UMD 规范。

以 web-runtime-cjs 配置为例,它的 entry 是 resolve('web/entry-runtime.js'),先来看一下 resolve 函数的定义。

js
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

这里的 resolve 函数实现非常简单,它先把 resolve 函数传入的参数 p 通过 / 做了分割成数组,然后取数组第一个元素设置为 base。在我们这个例子中,参数 p 是 web/entry-runtime.js,那么 base 则为 web。base 并不是实际的路径,它的真实路径借助了别名的配置,我们来看一下别名配置的代码,在 scripts/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'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

很显然,这里 web 对应的真实的路径是 path.resolve(__dirname, '../src/platforms/web'),这个路径就找到了 Vue.js 源码的 web 目录。然后 resolve 函数通过 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最终路径,它就是 Vue.js 源码 web 目录下的 entry-runtime.js。因此,web-runtime-cjs 配置对应的入口文件就找到了。

它经过 Rollup 的构建打包后,最终会在 dist 目录下生成 vue.runtime.common.js。

Runtime Only VS Runtime+Compiler

通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候会询问我们用 Runtime Only 版本的还是 Runtime+Compiler 版本。

下面我们来对比这两个版本。

  • Runtime Only

    我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。

  • Runtime+Compiler

    我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板,如下所示:

    js
    // 需要编译器的版本
    new Vue({
      template: '<div>{{ hi }}</div>'
    });
    
    // 这种情况不需要
    new Vue({
      render (h) {
        return h('div', this.hi)
      }
    });
    

因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用 Runtime-Only 的 Vue.js。

总结

通过这一节的分析,我们可以了解到 Vue.js 的构建打包过程,也知道了不同作用和功能的 Vue.js 它们对应的入口以及最终编译生成的 JS 文件。尽管在实际开发过程中我们会用 Runtime Only 版本开发比较多,但为了分析 Vue 的编译过程,我们这门课重点分析的源码是 Runtime+Compiler 的 Vue.js。

从入口开始

我们之前提到过 Vue.js 构建过程,在 web 应用下,我们来分析 Runtime + Compiler 构建出来的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js:

js
/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  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
  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)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

那么,当我们的代码执行 import Vue from 'vue' 的时候,就是从这个入口执行代码来初始化 Vue, 那么 Vue 到底是什么,它是怎么初始化的,我们来一探究竟。

Vue 的入口

在这个入口 JS 的上方我们可以找到 Vue 的来源:import Vue from './runtime/index',我们先来看一下这块儿的实现,它定义在 src/platforms/web/runtime/index.js 中:

js
/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// 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)
}
// ...

这里关键的代码是 import Vue from 'core/index',之后的逻辑都是对 Vue 这个对象做一些扩展,可以先不用看,我们来看一下真正初始化 Vue 的地方,在 src/core/index.js 中:

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)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

这里有 2 处关键的代码,import Vue from './instance/index' 和 initGlobalAPI(Vue),初始化全局 Vue API(我们稍后介绍),我们先来看第一部分,在 src/core/instance/index.js 中:

Vue 的定义

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

在这里,我们终于看到了 Vue 的庐山真面目,它实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它。

有些同学看到这不禁想问,为何 Vue 不用 ES6 的 Class 去实现呢?我们往后看这里有很多 xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法(这里具体的细节会在之后的文章介绍,这里不展开),Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。这么做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。

全局静态方法

Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法,它的定义在 src/core/global-api/index.js 中:

js
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

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

这里就是在 Vue 上扩展的一些全局方法的定义,Vue 官网中关于全局 API 都可以在这里找到,这里不会介绍细节,会在之后的章节我们具体介绍到某个 API 的时候会详细介绍。有一点要注意的是,Vue.util 暴露的方法最好不要依赖,因为它可能经常会发生变化,是不稳定的。

总结

那么至此,Vue 的初始化过程基本介绍完毕。这一节的目的是让同学们对 Vue 是什么有一个直观的认识,它本质上就是一个用 Function 实现的 Class,然后它的原型 prototype 以及它本身都扩展了一系列的方法和属性,那么 Vue 能做什么,它是怎么做的,我们会在后面的章节一层层帮大家揭开 Vue 的神秘面纱。

一、数据驱动

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。

在 Vue.js 中我们可以采用简洁的模板语法来声明式的将数据渲染为 DOM:

js
<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});

最终它会在页面上渲染出 Hello Vue。接下来,我们会从源码角度来分析 Vue 是如何实现的,分析过程会以主线代码为主,重要的分支逻辑会放在之后单独分析。数据驱动还有一部分是数据更新驱动视图变化,这一块内容我们也会在之后的章节分析,这一章我们的目标是弄清楚模板和数据如何渲染成最终的 DOM

new Vue 发生了什么

Vue 初始化

从入口代码开始分析,我们先来分析 new Vue 背后发生了哪些事情。我们都知道,new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的,来看一下源码,在src/core/instance/index.js 中。

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

可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义。

js
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) {
        // $mount 挂载
		vm.$mount(vm.$options.el)
	}
}

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Data 初始化

数据初始化发生在 initState 方法中。该方法在 src/core/instance/statejs 中定义。

initState
js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState 方法,用来初始化 data、props、computed、watcher 等等。

initData
js
function initData (vm: Component) {
  let data = vm.$options.data
  // 获取数据赋值给 data 和 vm._data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  // 如果 data 中定义过某项值,在 methods 和 props 中就不能使用,否则会报出警告
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 数据代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data 
  // 响应式处理
  observe(data, true /* asRootData */)
}
proxy
js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
js
vm.key => vm[source][key] // 数据代理
vm.key => vm['_data'][key] // 数据代理

虽然使用 vm._data 也能访问到属性,不推荐使用,__[property] 在编程界意味着私有属性,不可访问。

总结

Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,这样的编程思想是非常值得借鉴和学习的。

由于我们这一章的目标是弄清楚模板和数据如何渲染成最终的 DOM,所以各种初始化逻辑我们先不看。在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM,那么接下来我们来分析 Vue 的挂载过程。

Vue 实例挂载的实现

重写 $mount 方法

Vue 中我们是通过 $mount 实例⽅法去挂载 vm 的, $mount ⽅法在多个⽂件中都有定义,如

src/platform/web/entry-runtime-with- compiler.js 、 src/platform/web/runtime/index.js 、 src/platform/weex/runtime/index.js 。

因为 $mount 这个⽅法的实现是和平台、构建⽅式都相关的。接下来我们重点分析带 compiler 版本的 $monut 实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的⼯作原 理,有助于我们对原理理解的深⼊。

compiler 版本的 $monut 实现⾮常有意思,先来看⼀下 src/platform/web/entry-runtime- with-compiler.js ⽂件中定义:

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

  /* istanbul ignore if */
  // vue 不可以挂载到 body 和 documentElement 元素上
  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
  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)
}

这段代码⾸先缓存了原型上的 $mount ⽅法,再重新定义该⽅法,我们先来分析这段代码。⾸先,它 对 el 做了限制,Vue 不能挂载在 body 、 html 这样的根节点上。接下来的是很关键的逻辑 。

如果没有定义 render ⽅法,则会把 el 或者 template 字符串转换成 render ⽅法。这⾥我们 要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render ⽅法,⽆论我们是⽤单⽂件 .vue ⽅式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render ⽅法,那么这个 过程是 Vue 的⼀个“在线编译”的过程,它是调⽤ compileToFunctions ⽅法实现的,编译过程我们之后会介绍。

最后,调⽤原先原型上的 $mount ⽅法挂载。 原先原型上的 $mount ⽅法在 src/platform/web/runtime/index.js 中定义,之所以这么设计完 全是为了复⽤,因为它是可以被 runtime only 版本的 Vue 直接使⽤的。

query
js
/**
 * Query an element selector if it's not an element already.
 */
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

调用 document.querySelector 方法获取页面元素。如果不存在元素,创建 div 元素返回。

getOutHtml
js
/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

如果存在 outerHTML 直接返回。如果不存在,外面包裹一层 div,返回其 innerHTML。

原型 $mount 方法

这个⽅法在 src/platform/web/runtime/index.js 中定义。

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

$mount ⽅法⽀持传⼊ 2 个参数,第⼀个是 el ,它表⽰挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调⽤ query ⽅法转换成 DOM 对象的。第⼆个参数是和 服务端渲染相关,在浏览器环境下我们不需要传第⼆个参数。

mountComponent

$mount ⽅法实际上会去调⽤ mountComponent ⽅法,这个⽅法定义在 src/core/instance/lifecycle.js ⽂件中:

js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 如果不存在 render 或者 template 并没有正确转化 render 函数
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    // 开发环境报警告
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
       // 如果使用了 runtime-only 版本,但是有没有写 render 函数,写了 template
      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 {
        // 既没有 template 和 render 
        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 && !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
}

从上⾯的代码可以看到, mountComponent 核⼼就是先调⽤ vm._render ⽅法先⽣成虚拟 Node,再 实例化⼀个渲染 Watcher ,在它的回调函数中会调⽤ updateComponent ⽅法,最终调⽤ vm._update 更新 DOM。

Watcher 在这⾥起到两个作⽤,⼀个是初始化的时候会执⾏回调函数,另⼀个是当 vm 实例中的监测 的数据发⽣变化的时候执⾏回调函数,这块⼉我们会在之后的章节中介绍。

函数最后判断为根节点的时候设置 vm._isMounted 为 true , 表⽰这个实例已经挂载了,同时执⾏ mounted 钩⼦函数。 这⾥注意 vm.$vnode 表⽰ Vue 实例的⽗虚拟 Node,所以它为 Null 则表⽰ 当前是根 Vue 的实例。

Watcher
js
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 如果是渲染 watcher, vm._watcher = this。
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    // 处理 options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    // 如果 expOrFn 是 function,直接赋值给 getter。
    // 否则调用 parsePath 进行转化赋值给 getter。
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 依赖收集相关
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 对于首次渲染来说,getter 就是 updateComponent 。
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  // ...
}

总结

mountComponent ⽅法的逻辑也是⾮常清晰的,它会完成整个渲染⼯作,接下来我们要重点分析其中 的细节,也就是最核⼼的 2 个⽅法: vm._render 和 vm._update 。

vm.render

_render

Vue 的 _render ⽅法是实例的⼀个私有⽅法,它⽤来把实例渲染成⼀个虚拟 Node。

它的定义在 src/core/instance/render.js ⽂件中:

js
Vue.prototype._render = function (): VNode {
	const vm: Component = this
	const { render, _parentVnode } = vm.$options

	if (_parentVnode) {
		vm.$scopedSlots = normalizeScopedSlots(
			_parentVnode.data.scopedSlots,
			vm.$slots,
			vm.$scopedSlots
		)
	}

	// set parent vnode. this allows render functions to have access
	// to the data on the placeholder node.
	vm.$vnode = _parentVnode
	// render self
	let vnode
	try {
		// There's no need to maintain a stack because all render fns are called
		// separately from one another. Nested component's render fns are called
		// when parent component is patched.
		currentRenderingInstance = vm
        // vm._renderProxy 在生产环境下就是 vm,this 本身
        // 开发环境是一个 proxy 对象
		vnode = render.call(vm._renderProxy, vm.$createElement)
	} catch (e) {
		handleError(e, vm, `render`)
		// return error render result,
		// or previous vnode to prevent render error causing blank component
		/* istanbul ignore else */
		if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
			try {
				vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
			} catch (e) {
				handleError(e, vm, `renderError`)
				vnode = vm._vnode
			}
		} else {
			vnode = vm._vnode
		}
	} finally {
		currentRenderingInstance = null
	}
	// if the returned array contains only a single node, allow it
	if (Array.isArray(vnode) && vnode.length === 1) {
		vnode = vnode[0]
	}
	// return empty vnode in case the render function errored out
    // 如果 vnode 不属于 VNode
	if (!(vnode instanceof VNode)) {
        // 如果 vnode 是一个数组,说明模板存在多个根节点
		if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
			warn(
				'Multiple root nodes returned from render function. Render function ' +
				'should return a single root node.',
				vm
			)
		}
		vnode = createEmptyVNode()
	}
	// set parent
	vnode.parent = _parentVnode
	return vnode
}

这段代码最关键的是 render ⽅法的调⽤,我们在平时的开发⼯作中⼿写 render ⽅法的场景⽐较 少,⽽写的⽐较多的是 template 模板,在之前的 mounted ⽅法的实现中,会把 template 编译 成 render ⽅法,但这个编译过程是⾮常复杂的,我们不打算在这⾥展开讲,之后会专门花⼀个章节 来分析 Vue 的编译过程。

在 Vue 的官⽅⽂档中介绍了 render 函数的第⼀个参数是 createElement ,那么结合之前的例⼦:

html
<div id="app">
  {{ message }}
</div>

相当于我们编写如下 render 函数:

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

再回到 _render 函数中的 render ⽅法的调⽤:

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

可以看到, render 函数中的 createElement ⽅法就是 vm.$createElement ⽅法:

js
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // ...
}

实际上, vm.$createElement ⽅法定义是在执⾏ initRender ⽅法的时候,可以看到除了 vm.$createElement ⽅法,

还有⼀个 vm._c ⽅法,它是被模板编译成的 render 函数使⽤,⽽ vm.$createElement 是⽤户⼿写 render ⽅法使⽤的,

这俩个⽅法⽀持的参数相同,并且内部都调⽤了 createElement ⽅法。

示例代码

html
<div id="app"></div>
js
new Vue({
  el: '#app',
  render (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app1'
      }
    }, this.message);
  },
  data () {
    return {
      message: 'Hello World'
    }
  }
})

手写 render 函数,不会走模板编译转换为 render 的过程,和使用 template 模板是一样的效果。

实际挂载的 #app1的元素 会把原 id 为 #app 的元素替换掉。这也是为什么不能使用 body 或者 html 作为根元素的原因。

_renderProxy

继续看这部分代码,在调用 render 时,还传递了 vm._renderProxy 参数。

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

它的定义在 src/instance/init.js 文件中:

js
 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
    }
  	// ...
  }

可以看到 vm._renderProxy 在生产环境下就是 vm,指向 this 本身。

在开发环境下,则调用 initProxy 方法。该方法定义在 src/instance/proxy.js 文件中:

js
let initProxy

if (process.env.NODE_ENV !== 'production') {
  const allowedGlobals = makeMap(
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    'require' // for Webpack/Browserify
  )

  const warnNonPresent = (target, key) => {
    warn(
      `Property or method "${key}" is not defined on the instance but ` +
      'referenced during render. Make sure that this property is reactive, ' +
      'either in the data option, or for class-based components, by ' +
      'initializing the property. ' +
      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
      target
    )
  }

  const warnReservedPrefix = (target, key) => {
    warn(
      `Property "${key}" must be accessed with "$data.${key}" because ` +
      'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
      'prevent conflicts with Vue internals. ' +
      'See: https://vuejs.org/v2/api/#data',
      target
    )
  }

  const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)

  if (hasProxy) {
    const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
    config.keyCodes = new Proxy(config.keyCodes, {
      set (target, key, value) {
        if (isBuiltInModifier(key)) {
          warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
          return false
        } else {
          target[key] = value
          return true
        }
      }
    })
  }

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }

  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}

export { initProxy }

可以看到,当浏览器存在 Proxy 时,创建 Proxy 对象赋值给 vm._renderProxy。正因如此,在开发环境时,当你使用未被定义的变量时,Proxy 会优先拦截属性,如果发现属性未定义,这时控制台就会报警告。

示例代码

html
<div id="app">{{ message1 }}</div>
js
new Vue({
  el: '#app',
  data () {
    return {
      message: 'Hello World'
    }
  }
});
warn

总结

vm._render 最终是通过执⾏ createElement ⽅法并返回的是 vnode ,它是⼀个虚拟 Node。

Vue 2.0 相⽐ Vue 1.0 最⼤的升级就是利⽤了 Virtual DOM。

因此在分析 createElement 的实现前,我们 先了解⼀下 Virtual DOM 的概念。

Virtual DOM

Virtual DOM 这个概念相信⼤部分⼈都不会陌⽣,它产⽣的前提是浏览器中的 DOM 是很“昂贵"的,

为了更直观的感受,我们可以简单的把⼀个简单的 div 元素的属性都打印出来,如图所⽰:

origin-dom

可以看到,真正的 DOM 元素是⾮常庞⼤的,因为浏览器的标准就把 DOM 设计的⾮常复杂。

当我们频繁的去做 DOM 更新,会产⽣⼀定的性能问题。

⽽ Virtual DOM 就是⽤⼀个原⽣的 JS 对象去描述⼀个 DOM 节点,所以它⽐创建⼀个 DOM 的代价要⼩很多。

在 Vue.js 中,Virtual DOM 是⽤ VNode 这么⼀个 Class 去描述,它是定义在 src/core/vdom/vnode.js 中的。

js
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

可以看到 Vue.js 中的 Virtual DOM 的定义还是略微复杂⼀些的,因为它这⾥包含了很多 Vue.js 的特 性。

这⾥千万不要被这些茫茫多的属性吓到,实际上 Vue.js 中 Virtual DOM 是借鉴了⼀个开源库 snabbdom 的实现,然后加⼊了⼀些 Vue.js 特⾊的东⻄。我建议⼤家如果想深⼊了解 Vue.js 的 Virtual DOM 前不妨先阅读这个库的源码,因为它更加简单和纯粹。

VNode 相关类型定义在 flow/vnode.js 文件中。

tsx
declare type VNodeChildren = Array<?VNode | string | VNodeChildren> | string;

declare type VNodeComponentOptions = {
  Ctor: Class<Component>;
  propsData: ?Object;
  listeners: ?Object;
  children: ?Array<VNode>;
  tag?: string;
};

declare type MountedComponentVNode = {
  context: Component;
  componentOptions: VNodeComponentOptions;
  componentInstance: Component;
  parent: VNode;
  data: VNodeData;
};

// interface for vnodes in update modules
declare type VNodeWithData = {
  tag: string;
  data: VNodeData;
  children: ?Array<VNode>;
  text: void;
  elm: any;
  ns: string | void;
  context: Component;
  key: string | number | void;
  parent?: VNodeWithData;
  componentOptions?: VNodeComponentOptions;
  componentInstance?: Component;
  isRootInsert: boolean;
};

declare interface VNodeData {
  key?: string | number;
  slot?: string;
  ref?: string;
  is?: string;
  pre?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | Array<Object> | Object;
  normalizedStyle?: Object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: string };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: ?{ [key: string]: Function | Array<Function> };
  nativeOn?: { [key: string]: Function | Array<Function> };
  transition?: Object;
  show?: boolean; // marker for v-show
  inlineTemplate?: {
    render: Function;
    staticRenderFns: Array<Function>;
  };
  directives?: Array<VNodeDirective>;
  keepAlive?: boolean;
  scopedSlots?: { [key: string]: Function };
  model?: {
    value: any;
    callback: Function;
  };
};

declare type VNodeDirective = {
  name: string;
  rawName: string;
  value?: any;
  oldValue?: any;
  arg?: string;
  oldArg?: string;
  modifiers?: ASTModifiers;
  def?: Object;
};

declare type ScopedSlotsData = Array<{ key: string, fn: Function } | ScopedSlotsData>;

总结

其实 VNode 是对真实 DOM 的⼀种抽象描述,它的核⼼定义⽆⾮就⼏个关键属性,标签名、数据、⼦节点、键值等,其它属性都是都是⽤来扩展 VNode 的灵活性以及实现⼀些特殊 feature 的。由于 VNode 只是⽤来映射到真实 DOM 的渲染,不需要包含操作 DOM 的⽅法,因此它是⾮常轻量和简单的。

Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、 patch 等过程。那么在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement ⽅法创建的,我们接下来分析这部分的实现。

createElement

Vue.js 利⽤ createElement ⽅法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:

js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果满足条件,说明 data 就是 children
  // 对参数个数不一致进行处理
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement ⽅法实际上是对 _createElement ⽅法的封装,它允许传⼊的参数更加灵活,

在处理这些参数后,调⽤真正创建 VNode 的函数 _createElement :

ts
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // data 不能是响应式对象
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    // 创建注释 VNode
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // 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
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 对 children 进行 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)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        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
      )
    } 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()
  }
}

_createElement ⽅法有 5 个参数。

  • context 表⽰ VNode 的上下⽂环境,它是 Component 类型;
  • tag 表⽰标签,它可以是⼀个字符串,也可以是⼀个 Component ;
  • data 表⽰ VNode 的数据,它是⼀个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义,这⾥先不展开说;
  • children 表⽰当前 VNode 的⼦节点,它是任意类型的,它接下来需要被规范为标准的 VNode 数组;
  • normalizationType 表⽰⼦节点规范的类型,类型不同规范的⽅法也就不⼀样,它主要是参考 render 函数是编译⽣成的还是⽤户⼿写的。

createElement 函数的流程略微有点多,我们接下来主要分析 2 个重点的流程 —— children 的规范化以及 VNode 的创建。

children 的规范化

由于 Virtual DOM 实际上是⼀个树状结构,每⼀个 VNode 可能会有若⼲个⼦节点,这些⼦节点应该也是 VNode 的类型。 _createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型。

这⾥根据 normalizationType 的不同,调⽤了 normalizeChildren(children)simpleNormalizeChildren(children) ⽅法,

它们的定义都在 src/core/vdom/helpers/normalzie-children.js 中:

js
// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

simpleNormalizeChildren ⽅法调⽤场景是 render 函数当函数是编译⽣成的。理论上编译⽣成的 children 都已经是 VNode 类型的,但这⾥有⼀个例外,就是 functional component 函数式组件返回的是⼀个数组⽽不是⼀个根节点,所以会通过 Array.prototype.concat ⽅法把整个 children 数组打平,让它的深度只有⼀层。

js
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

normalizeChildren ⽅法的调⽤场景有 2 种。

⼀个场景是 render 函数是⽤户⼿写的,当 children 只有⼀个节点的时候,Vue.js 从接⼝层⾯允许⽤户把 children 写成基础类型⽤来创建单个简单的⽂本节点,这种情况会调⽤ createTextVNode 创建⼀个⽂本节点的 VNode;

另⼀个场景是当编译 slot 、 v-for 的时候会产⽣嵌套数组的情况,会调⽤ normalizeArrayChildren ⽅法,接下来看⼀下它的实现:

js
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        // 优化:合并文本节点
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        // 优化:合并文本节点
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildren 接收 2 个参数, children 表⽰要规范的⼦节点, nestedIndex 表⽰嵌套的索引,因为单个 child 可能是⼀个数组类型。 normalizeArrayChildren 主要的逻辑就是遍历 children ,获得单个节点 c ,然后对 c 的类型判断。

如果是⼀个数组类型,则递归调⽤ normalizeArrayChildren ;

如果是基础类型,则通过 createTextVNode ⽅法转换成 VNode 类型;

否则就已经是 VNode 类型了,如果 children 是⼀个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key。

这⾥需要注意⼀点,在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text 节点,会把它们合并成⼀个 text 节点。经过对 children 的规范化, children 变成了⼀个类型为 VNode 的 Array。

VNode 的创建

回到 createElement 函数,规范化 children 后,接下来会去创建⼀个 VNode 的实例:

js
let vnode, ns
if (typeof tag === 'string') {
	let Ctor
	ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判断标签是不是 HTML 保留标签
	if (config.isReservedTag(tag)) {
		// platform built-in elements
		if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
			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
		)
	} 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)
}

这⾥先对 tag 做判断。

如果是 string 类型,则接着判断如果是内置的⼀些节点,则直接创建⼀个 普通 VNode,如果是为已注册的组件名,则通过 createComponent 创建⼀个组件类型的 VNode;否则创建⼀个未知的标签的 VNode。

如果是 tag ⼀个 Component 类型,则直接调⽤ createComponent 创建⼀个组件类型的 VNode 节点。

对于 createComponent 创建组件类型的 VNode 的过程,我们之后会去介绍,本质上它还是返回了⼀个 VNode。

总结

那么⾄此,我们⼤致了解了 createElement 创建 VNode 的过程,每个 VNode 有 children , children 每个元素也是⼀个 VNode,这样就形成了⼀个 VNode Tree,它很好的描述了我们的 DOM Tree。

回到 mountComponent 函数的过程,我们已经知道 vm._render 是如何创建了⼀个 VNode,接下来就是要把这个 VNode 渲染成⼀个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的, 接下来分析⼀下这个过程。

update

_update

Vue 的 _update 是实例的⼀个私有⽅法,它被调⽤的时机有 2 个,⼀个是⾸次渲染,⼀个是数据更新的时候;

由于我们这⼀章节只分析⾸次渲染部分,数据更新部分会在之后分析响应式原理的时候涉及。

_update ⽅法的作⽤是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:

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

js
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

可以看到,甚⾄在 web 平台上,是否是服务端渲染也会对这个⽅法产⽣影响。因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是⼀个空函数,⽽在浏览器端渲染中,它指向了 patch ⽅法,它的定义在 src/platforms/web/runtime/patch.js 中:

js
/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

该⽅法的定义是调⽤ createPatchFunction ⽅法的返回值,这⾥传⼊了⼀个对象,包含 nodeOps 参数和 modules 参数。

其中, nodeOps 封装了⼀系列 DOM 操作的⽅法, modules 定义了⼀些模块的钩⼦函数的实现,我们这⾥先不详细介绍。

patch

来看⼀下 createPatchFunction 的实现,它定义在 src/core/vdom/patch.js 中:

js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

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

createPatchFunction 内部定义了⼀系列的辅助⽅法,最终返回了⼀个 patch ⽅法,这个⽅法就赋值给了 vm._update 函数⾥调⽤的 vm.__patch__

在介绍 patch 的⽅法实现之前,我们可以思考⼀下为何 Vue.js 源码绕了这么⼀⼤圈,把相关代码分散到各个⽬录。

因为前⾯介绍过, patch 是平台相关的,在 Web 和 Weex 环境,它们把虚拟 DOM 映射到 “平台 DOM” 的⽅法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。

因此每个因此每个平台都有各⾃的 nodeOps 和 modules ,它们的代码需要托管在 src/platforms 这个⼤⽬录下。

⽽不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共的部分托管在 core 这个⼤⽬录下。

差异化部分只需要通过参数来区别,这⾥⽤到了⼀个函数柯⾥化的技巧,通过 createPatchFunction 把差异化参数提前固化,这样不⽤每次调⽤ patch 的时候都传递 nodeOps 和 modules 了,这种编程技巧也⾮常值得学习。

在这⾥, nodeOps 表⽰对 “平台 DOM” 的⼀些操作⽅法, modules 表⽰平台的⼀些模块,它们会在整个 patch 过程的不同阶段执⾏相应的钩⼦函数。这些代码的具体实现会在之后的章节介绍。回到 patch ⽅法本⾝,它接收 4个参数, oldVnode 表⽰旧的 VNode 节点,它也可以不存在或者是⼀个 DOM 对象; vnode 表⽰执⾏ _render 后返回的 VNode 的节点; hydrating 表⽰是否是服务端渲染; removeOnly 是给 transition-group ⽤的,之后会介绍。

patch 的逻辑看上去相对复杂,因为它有着⾮常多的分⽀逻辑,为了⽅便理解,我们并不会在这⾥介绍所有的逻辑,仅会针对我们之前的例⼦分析它的执⾏逻辑。之后我们对其它场景做源码分析的时候会再次回顾 patch ⽅法。

先来回顾我们的例⼦:

js
new Vue({
  el: '#app',
  render (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      }
    }, this.message);
  },
  data () {
    return {
      message: 'Hello World'
    }
  }
})

然后我们在 vm._update 的⽅法⾥是这么调⽤ patch ⽅法的(src/core/instance/lifecycle.js 文件中):

js
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

结合我们的例⼦,我们的场景是⾸次渲染,所以在执⾏ patch 函数的时候,传⼊的 vm.$el 对应的是例⼦中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的 <div id="app"> vm.$el 的赋值是在之前 mountComponent 函数做的, vnode 对应的是调⽤ render 函数的返回值, hydrating 在⾮服务端渲染情况下为 false, removeOnly 为 false。

确定了这些⼊参后,我们回到 patch 函数的执⾏过程,看⼏个关键步骤。

js
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
    // 将真实 DOM 转换为 VNode
	oldVnode = emptyNodeAt(oldVnode)
}

// replacing existing element
const oldElm = oldVnode.elm // <div id="app"></div>
const parentElm = nodeOps.parentNode(oldElm) // <body></body>

// create new node
// 将 vnode 挂载到真实 DOM
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)
)

createElem

由于我们传⼊的 oldVnode 实际上是⼀个 DOM container,所以 isRealElement 为 true。接下来⼜通过 emptyNodeAt ⽅法把 oldVnode 转换成 VNode 对象,然后再调⽤ createElm ⽅法,这个⽅法在这⾥⾮常重要,来看⼀下它的实现:

js
function createElm (
	vnode,
	insertedVnodeQueue,
	parentElm,
	refElm,
	nested,
	ownerArray,
	index
) {
	if (isDef(vnode.elm) && isDef(ownerArray)) {
		// This vnode was used in a previous render!
		// now it's used as a new node, overwriting its elm would cause
		// potential patch errors down the road when it's used as an insertion
		// reference node. Instead, we clone the node on-demand before creating
		// associated DOM element for it.
		vnode = ownerArray[index] = cloneVNode(vnode)
	}

	vnode.isRootInsert = !nested // for transition enter check
    // 尝试创建组件节点,首次肯定是返回 false
	if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
		return
	}

	const data = vnode.data
	const children = vnode.children
	const tag = vnode.tag
	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)
	}
}

createElm 的作⽤是通过虚拟节点创建真实的 DOM 并插⼊到它的⽗节点中。

我们来看⼀下它的⼀些关键逻辑, createComponent ⽅法⽬的是尝试创建⼦组件,这个逻辑在之后组件的章节会详细介绍,在当前这个 case 下它的返回值为 false;接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在⾮⽣产环境下做校验,看是否是⼀个合法标签;然后再去调⽤平台 DOM 的操作去创建 ⼀个占位符元素。

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

createChildren

接下来调⽤ createChildren ⽅法去创建⼦元素:

js
createChildren(vnode, children, insertedVnodeQueue)
js
function createChildren (vnode, children, insertedVnodeQueue) {
	if (Array.isArray(children)) {
		if (process.env.NODE_ENV !== 'production') {
			checkDuplicateKeys(children)
		}
		for (let i = 0; i < children.length; ++i) {
			createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
		}
	} else if (isPrimitive(vnode.text)) {
		nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
	}
}

createChildren 的逻辑很简单,实际上是遍历⼦虚拟节点,递归调⽤ createElm ,这是⼀种常⽤的深度优先的遍历算法,

这⾥要注意的⼀点是在遍历过程中会把 vnode.elm 作为⽗容器的 DOM 节点占位符传⼊。

接着再调⽤ invokeCreateHooks ⽅法执⾏所有的 create 的钩⼦并把 vnode push 到 insertedVnodeQueue 中。

js
invokeCreateHooks(vnode, insertedVnodeQueue)
js
function invokeCreateHooks (vnode, insertedVnodeQueue) {
	for (let i = 0; i < cbs.create.length; ++i) {
		cbs.create[i](emptyNode, vnode)
	}
	i = vnode.data.hook // Reuse variable
	if (isDef(i)) {
		if (isDef(i.create)) i.create(emptyNode, vnode)
		if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
	}
}

最后调⽤ insert ⽅法把 DOM 插⼊到⽗节点中,因为是递归调⽤,⼦元素会优先调⽤ insert ,所以整个 vnode 树节点的插⼊顺序是先⼦后⽗。来看⼀下 insert ⽅法,它的定义在 src/core/vdom/patch.js 上。

js
insert(parentElm, vnode.elm, refElm)
js
function insert (parent, elm, ref) {
	if (isDef(parent)) {
		if (isDef(ref)) {
			if (nodeOps.parentNode(ref) === parent) {
				nodeOps.insertBefore(parent, elm, ref)
			}
		} else {
			nodeOps.appendChild(parent, elm)
		}
	}
}

insert 逻辑很简单,调⽤⼀些 nodeOps 把⼦节点插⼊到⽗节点中,这些辅助⽅法定义在 src/platforms/web/runtime/node-ops.js 中:

js
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
js
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

其实就是调⽤原⽣ DOM 的 API 进⾏ DOM 操作,看到这⾥,很多同学恍然⼤悟,原来 Vue 是这样动态创建的 DOM。

在 createElm 过程中,如果 vnode 节点如果不包含 tag ,则它有可能是⼀个注释或者纯⽂本节点,可以直接插⼊到⽗元素中。

在我们这个例⼦中,最内层就是⼀个⽂本 vnode ,它的 text 值取的就是之前的 this.message 的值 Hello World! 。

再回到 patch ⽅法,⾸次渲染我们调⽤了 createElm ⽅法,这⾥传⼊的 parentElm 是 oldVnode.elm 的⽗元素,在我们的例⼦是 id 为 #app div 的⽗元素,也就是 Body;实际上整个过程就是递归创建了⼀个完整的 DOM 树并插⼊到 Body 上。

最后,我们根据之前递归 createElm ⽣成的 vnode 插⼊顺序队列,执⾏相关的 insert 钩⼦函数,这部分内容我们之后会详细介绍。

总结

那么⾄此我们从主线上把模板和数据如何渲染成最终的 DOM 的过程分析完毕了,

我们可以通过下图更直观地看到从初始化 Vue 到最终渲染的整个过程。

data-dirven

我们这⾥只是分析了最简单和最基础的场景,在实际项⽬中,我们是把⻚⾯拆成很多组件的,Vue 另⼀个核⼼思想就是组件化。

那么下⼀章我们就来分析 Vue 的组件化过程。

二、组件化

Vue.js 另⼀个核⼼思想是组件化。所谓组件化,就是把⻚⾯拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图⽚等资源放在⼀起开发和维护。组件是资源独⽴的,组件在系统内部可复⽤,组件和组件之间可以嵌套。

我们在⽤ Vue.js 开发实际项⽬的时候,就是像搭积⽊⼀样,编写⼀堆组件拼装⽣成⻚⾯。在 Vue.js 的官⽹中,也是花了⼤篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。

那么在这⼀章节,我们将从源码的⾓度来分析 Vue 的组件内部是如何⼯作的,只有了解了内部的⼯作原理,才能让我们使⽤它的时候更加得⼼应⼿。接下来我们会⽤ Vue-cli 初始化的代码为例,来分析⼀下 Vue 组件初始化的⼀个过程。

js
import Vue from 'vue';
import App from './App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
})

这段代码相信很多同学都很熟悉,它和我们上⼀章相同的点也是通过 render 函数去渲染的,不同的这次通过 createElement 传的参数是⼀个组件⽽不是⼀个原⽣的标签,那么接下来我们就开始分析这⼀过程。

createComponent

上⼀章我们在分析 createElement 的实现的时候,它最终会调⽤ _createElement ⽅法,其中有⼀段逻辑是对参数 tag 的判断,如果是⼀个普通的 html 标签,像上⼀章的例⼦那样是⼀个普通的 div,则会实例化⼀个普通 VNode 节点,否则通过 createComponent ⽅法创建⼀个组件 VNode。

js
if (typeof tag === 'string') {
	let Ctor
	ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
	if (config.isReservedTag(tag)) {
		// platform built-in elements
		if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
			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
		)
	} 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)
}

在我们这⼀章传⼊的是⼀个 App 对象,它本质上是⼀个 Component 类型,那么它会⾛到上述代码的 else 逻辑,直接通过 createComponent ⽅法来创建 vnode 。

所以接下来我们来看⼀下 createComponent ⽅法的实现,它定义在 src/core/vdom/create-component.js ⽂件中:

jsx
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  // Vue.extend 
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  // 异步组件处理
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 安装组件钩子函数
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

可以看到, createComponent 的逻辑也会有⼀些复杂,但是分析源码⽐较推荐的是只分析核⼼流程,分⽀流程可以之后针对性的看,所以这⾥针对组件渲染这个 case 主要就 3 个关键步骤: 构造⼦类构造函数,安装组件钩⼦函数和实例化 vnode 。

构造⼦类构造函数

js
const baseCtor = context.$options._base

// plain options object: turn it into a constructor
// Vue.extend 
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
}

我们在编写⼀个组件的时候,通常都是创建⼀个普通对象,还是以我们的 App.vue 为例,代码如下:

js
import HelloWorld from './components/HelloWorld';

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}

这⾥ export 的是⼀个对象,所以 createComponent ⾥的代码逻辑会执⾏到 baseCtor.extend(Ctor) ,在这⾥ baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么⼀段逻辑:

js
// 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

细⼼的同学会发现,这⾥定义的是 Vue.option ,⽽我们的 createComponent 取的是 context.$options ,实际上在 src/core/instance/init.js ⾥ Vue 原型上的 _init 函数中有这么⼀段逻辑:

js
vm.$options = mergeOptions(
	resolveConstructorOptions(vm.constructor),
	options || {},
	vm
)

这样就把 Vue 上的⼀些 option 扩展到了 vm.$option 上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。 mergeOptions 的实现我们会在后续章节中具体分析,现在只需要理解它的功能是把 Vue 构造函数的 options 和⽤户传⼊的 options 做⼀层合并,到 vm.$options 上。

在了解了 baseCtor 指向了 Vue 之后,我们来看⼀下 Vue.extend 函数的定义,在 src/core/global-api/extend.js 中。

js
/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
	extendOptions = extendOptions || {}
	const Super = this
	const SuperId = Super.cid
	const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    // 多组件使用同一个组件时,组件只会创建一次
	if (cachedCtors[SuperId]) {
		return cachedCtors[SuperId]
	}

	const name = extendOptions.name || Super.options.name
	if (process.env.NODE_ENV !== 'production' && name) {
        // 校验组件名称
		validateComponentName(name)
	}

    // 定义子构造函数,其内部调用 init 方法
	const Sub = function VueComponent (options) {
		this._init(options)
	}
    // 子构造器的原型指向父的原型(原型继承)
	Sub.prototype = Object.create(Super.prototype)
	Sub.prototype.constructor = Sub
	Sub.cid = cid++
    // 配置合并,将自身配置与 Vue 配置进行合并
	Sub.options = mergeOptions(
		Super.options,
		extendOptions
	)
	Sub['super'] = Super

	// For props and computed properties, we define the proxy getters on
	// the Vue instances at extension time, on the extended prototype. This
	// avoids Object.defineProperty calls for each instance created.
    // 初始化 props
	if (Sub.options.props) {
		initProps(Sub)
	}
    // 初始化 computed
	if (Sub.options.computed) {
		initComputed(Sub)
	}

	// allow further extension/mixin/plugin usage
	Sub.extend = Super.extend
	Sub.mixin = Super.mixin 
	Sub.use = Super.use

	// create asset registers, so extended classes
	// can have their private assets too.
	ASSET_TYPES.forEach(function (type) {
		Sub[type] = Super[type]
	})
	// enable recursive self-lookup
	if (name) {
		Sub.options.components[name] = Sub
	}

	// keep a reference to the super options at extension time.
	// later at instantiation we can check if Super's options have
	// been updated.
	Sub.superOptions = Super.options
	Sub.extendOptions = extendOptions
	Sub.sealedOptions = extend({}, Sub.options)

	// cache constructor
    // 缓存构造器
	cachedCtors[SuperId] = Sub
	return Sub
}
js
export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

Vue.extend 的作⽤就是构造⼀个 Vue 的⼦类,它使⽤⼀种⾮常经典的原型继承的⽅式把⼀个纯对象转换⼀个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本⾝扩展了⼀些属性,如扩展 options 、添加全局 API 等;

并且对配置中的 props 和 computed 做了初始化⼯作;最后对于这个 Sub 构造函数做了缓存,避免多次执⾏ Vue.extend 的时候对同⼀个⼦组件重复构造。这样当我们去实例化 Sub 的时候,就会执⾏ this._init 逻辑再次⾛到了 Vue 实例的初始化逻辑,实例化⼦组件的逻辑在之后的章节会介绍。

js
const Sub = function VueComponent (options) {
	this._init(options)
}

####安装组件钩⼦函数

js
// install component management hooks onto the placeholder node
installComponentHooks(data)

我们之前提到 Vue.js 使⽤的 Virtual DOM 参考的是开源库 snabbdom,它的⼀个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩⼦函数,⽅便我们做⼀些额外的事情,Vue.js 也是充分利⽤这⼀点,在初始化⼀个 Component 类型的 VNode 的过程中实现了⼏个钩⼦函数:

js
// 每个组件都存在的钩子函数
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

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

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
js
const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩⼦函数合并到 data.hook 中,在 VNode 执⾏ patch 的过程中执⾏相关的钩⼦函数,具体的执⾏我们稍后在介绍 patch 过程中会详细介绍。

这⾥要注意的是合并策略,在合并过程中,如果某个时机的钩⼦已经存在 data.hook 中,那么通过执⾏ mergeHook 函数做合并,这个逻辑很简单,就是在最终执⾏的时候,依次执⾏这两个钩⼦函数即可。

####实例化 vnode

js
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
	`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
	data, undefined, undefined, undefined, context,
	{ Ctor, propsData, listeners, tag, children },
	asyncFactory
)

最后⼀步⾮常简单,通过 new VNode 实例化⼀个 vnode 并返回。

需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的,这点很关键,在之后的 patch 过程中我们会再提。

组件 VNode 没有 children、text、elm,但是存在 componentOptions 属性。

总结

这⼀节我们分析了 createComponent 的实现,了解到它在渲染⼀个组件的时候的 3 个关键逻辑:构造⼦类构造函数,安装组件钩⼦函数和实例化 vnode 。 createComponent 后返回的是组件 vnode ,它也⼀样⾛到 vm._update ⽅法,进⽽执⾏了 patch 函数,我们在上⼀章对 patch 函数做了简单的分析,那么下⼀节我们会对它做进⼀步的分析。

patch

目标

了解组件 patch 的整体流程。

了解组件 patch 流程中的 activeInstance、vm.$vnode、vm._vnode 等。(父子关系)

了解嵌套组件的插入顺序。(先子后父)

引言

通过前⼀章的分析我们知道,当我们通过 createComponent 创建了组件 VNode,接下来会⾛到 vm._update ,执⾏ vm.__patch__ 去把 VNode 转换成真正的 DOM 节点。

这个过程我们在前⼀章已经分析过了,但是针对⼀个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不⼀样的地⽅。

patch 的过程会调⽤ createElm 创建元素节点,回顾⼀下 createElm 的实现,它的定义在 src/core/vdom/patch.js 中:

js
function createElm (
	vnode,
	insertedVnodeQueue,
	parentElm,
	refElm,
	nested,
	ownerArray,
	index
) {
	// ...
	if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
		return
	}
	// ...
}

createComponent

我们删掉多余的代码,只保留关键的逻辑,这⾥会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果为 true 则直接结束,那么接下来看⼀下 createComponent ⽅法的实现:

js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
	let i = vnode.data
	if (isDef(i)) {
        // keep-alive 逻辑,先忽略
		const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
        
        // render 过程中已安装组件钩子函数 init
		if (isDef(i = i.hook) && isDef(i = i.init)) {
			i(vnode, false /* hydrating */)
		}
		// after calling the init hook, if the vnode is a child component
		// it should've created a child instance and mounted it. the child
		// component also has set the placeholder vnode's elm.
		// in that case we can just return the element and be done.
		if (isDef(vnode.componentInstance)) {
			initComponent(vnode, insertedVnodeQueue)
			insert(parentElm, vnode.elm, refElm)
			if (isTrue(isReactivated)) {
				reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
			}
			return true
		}
	}
}

createComponent 函数中,⾸先对 vnode.data 做了⼀些判断:

js
let i = vnode.data
if (isDef(i)) {
	// ...
	if (isDef(i = i.hook) && isDef(i = i.init)) {
		i(vnode, false /* hydrating */)
	}
	// ...
}

如果 vnode 是⼀个组件 VNode,那么条件会满⾜,并且得到 i 就是 init 钩⼦函数,

回顾上节我们在创建组件 VNode 的时候合并钩⼦函数中就包含 init 钩⼦函数,定义在 src/core/vdom/create-component.js 中:

js
import { activeInstance } from '../instance/lifecycle'

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
	if (
		vnode.componentInstance &&
		!vnode.componentInstance._isDestroyed &&
		vnode.data.keepAlive
	) {
		// kept-alive components, treat as a patch
		const mountedNode: any = vnode // work around flow
		componentVNodeHooks.prepatch(mountedNode, mountedNode)
	} else {
		const child = vnode.componentInstance = createComponentInstanceForVnode(
			vnode,
			activeInstance
		)
		child.$mount(hydrating ? vnode.elm : undefined, hydrating)
	}
}

init 钩⼦函数执⾏也很简单,我们先不考虑 keepAlive 的情况,它是通过 createComponentInstanceForVnode 创建⼀个 Vue 的实例,然后调⽤ $mount ⽅法挂载⼦组件,先来看⼀下 createComponentInstanceForVnode 的实现:

js
export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, // 占位符 VNode,占位节点
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode 函数构造的⼀个内部组件的参数,然后执⾏ new vnode.componentOptions.Ctor(options) 。

这⾥的 vnode.componentOptions.Ctor 对应的就是⼦组件的构造函数,我们上⼀节分析了它实际上是继承于 Vue 的⼀个构造器 Sub ,相当于 new Sub(options) 这⾥有⼏个关键参数要注意⼏个点, _isComponent 为 true 表⽰它是⼀个组件, parent 表⽰当前激活的组件实例。注意,这⾥⽐较有意思的是如何拿到组件实例,后⾯会介绍。

所以⼦组件的实例化实际上就是在这个时机执⾏的,并且它会执⾏实例的 _init ⽅法,这个过程有⼀些和之前不同的地⽅需要挑出来说,代码在 src/core/instance/init.js 中:

js
Vue.prototype._init = function (options?: Object) {
	const vm: Component = this
	// ...
	// 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
		)
	}
	// ...
	if (vm.$options.el) {
		vm.$mount(vm.$options.el)
	}
}

这⾥⾸先是合并 options 的过程有变化, _isComponent 为 true,所以⾛到了 initInternalComponent 过程,这个函数的实现也简单看⼀下:

js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent // 父 VNode 实例
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

这个过程我们重点记住以下⼏个点即可:

opts.parent = options.parent 、 opts._parentVnode = parentVnode ,

它们是把之前我们通过 createComponentInstanceForVnode 函数传⼊的⼏个参数合并到内部的选项 $options ⾥了。

再来看⼀下 _init 函数最后执⾏的代码:

js
if (vm.$options.el) {
	vm.$mount(vm.$options.el)
}

由于组件初始化的时候是不传 el 的,因此组件是⾃⼰接管了 $mount 的过程,这个过程的主要流程在上⼀章介绍过了,回到组件 init 的过程,componentVNodeHooks 的 init 钩⼦函数,在完成实例化的 _init 后,接着会执⾏ child.$mount(hydrating ? vnode.elm : undefined, hydrating) 。这⾥ hydrating 为 true ⼀般是服务端渲染的情况,我们只考虑客户端渲染,所以这⾥$mount 相当于执⾏ child.$mount(undefined, false) ,它最终会调⽤ mountComponent ⽅法,进⽽执⾏ vm._render() ⽅法

js
Vue.prototype._render = function (): VNode {
	const vm: Component = this
	const { render, _parentVnode } = vm.$options
    // ... 
	// set parent vnode. this allows render functions to have access
	// to the data on the placeholder node.
	vm.$vnode = _parentVnode
	// render self
	let vnode
	try {
		// There's no need to maintain a stack because all render fns are called
		// separately from one another. Nested component's render fns are called
		// when parent component is patched.
		currentRenderingInstance = vm
		vnode = render.call(vm._renderProxy, vm.$createElement)
	} catch (e) {
		// ...
	}
    // ...
	// set parent
	vnode.parent = _parentVnode
	return vnode
}

我们只保留关键部分的代码,这⾥的 _parentVnode 就是当前组件的⽗ VNode,⽽ render 函数⽣成的 vnode 当前组件的渲染 vnode , vnode 的 parent 指向了 _parentVnode ,也就是vm.$vnode ,它们是⼀种⽗⼦的关系。

我们知道在执⾏完 vm._render ⽣成 VNode 后,接下来就要执⾏ vm._update 去渲染 VNode 了。

来看⼀下组件渲染的过程中有哪些需要注意的, vm._update 的定义在src/core/instance/lifecycle.js 中:

js
export let activeInstance: any = null

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._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,vm._vnodevm.$vnode 的关系就是⼀种⽗⼦关系,⽤代码表达就是 vm._vnode.parent === vm.$vnode

还有⼀段⽐较有意思的代码:

js
export let activeInstance: any = null

export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

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

这个 activeInstance 作⽤就是保持当前上下⽂的 Vue 实例,它是在 lifecycle 模块的全局变量,定义是 export let activeInstance: any = null ,并且在之前我们调⽤ createComponentInstanceForVnode ⽅法的时候从 lifecycle 模块获取,并且作为参数传⼊的。

因为实际上 JavaScript 是⼀个单线程,Vue 整个初始化是⼀个深度遍历的过程,在实例化⼦组件的过程中,它需要知道当前上下⽂的 Vue 实例是什么,并把它作为⼦组件的⽗ Vue 实例。之前我们提到过对⼦组件的实例化过程先会调⽤ initInternalComponent(vm, options) 合并 options ,把parent 存储在 vm.$options 中,在 $mount 之前会调⽤ initLifecycle(vm) ⽅法:

js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

可以看到 vm.$parent 就是⽤来保留当前 vm 的⽗实例,并且通过 parent.$children.push(vm) 来把当前的 vm 存储到⽗实例的 $children 中。 在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance ,同时通过 const prevActiveInstance = activeInstance ⽤ prevActiveInstance 保留上⼀次的 activeInstance 。

实际上, prevActiveInstance 和当前的 vm 是⼀个⽗⼦关系,当⼀个 vm 实例完成它的所有⼦树的 patch 或者 update 过程后, activeInstance 会回到它的⽗实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化⼦组件的时候能传⼊当前⼦组件的⽗ Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个⽗⼦关系保留。

那么回到 _update ,最后就是调⽤ __patch__ 渲染 VNode 了。

js
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

return function patch (oldVnode, vnode, hydrating, removeOnly) {
	// ...
	let isInitialPatch = false
	const insertedVnodeQueue = []

	if (isUndef(oldVnode)) {
		// empty mount (likely as component), create new root element
		isInitialPatch = true
		createElm(vnode, insertedVnodeQueue)
	} else {
		// ...
	}
	// ...
}

这⾥⼜回到了本节开始的过程,之前分析过负责渲染成 DOM 的函数是 createElm ,注意这⾥我们只传了 2 个参数,所以对应的 parentElm 是 undefined 。我们再来看看它的定义:

js
function createElm (
	vnode,
	insertedVnodeQueue,
	parentElm,
	refElm,
	nested,
	ownerArray,
	index
) {
	if (isDef(vnode.elm) && isDef(ownerArray)) {
		// This vnode was used in a previous render!
		// now it's used as a new node, overwriting its elm would cause
		// potential patch errors down the road when it's used as an insertion
		// reference node. Instead, we clone the node on-demand before creating
		// associated DOM element for it.
		vnode = ownerArray[index] = cloneVNode(vnode)
	}

	vnode.isRootInsert = !nested // for transition enter check
	if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
		return
	}

	const data = vnode.data
	const children = vnode.children
	const tag = vnode.tag
	if (isDef(tag)) {
		// ...
        
		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)
	}
}

注意,这⾥我们传⼊的 vnode 是组件渲染的 vnode ,也就是我们之前说的 vm._vnode ,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode ,这⾥ createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。

接下来的过程就和我们上⼀章⼀样了,先创建⼀个⽗节点占位符,然后再遍历所有⼦ VNode 递归调⽤ createElm ,在遍历的过程中, 如果遇到⼦ VNode 是⼀个组件的 VNode,则重复本节开始的过程,这样通过⼀个递归的⽅式就可以完整地构建了整个组件树。

由于我们这个时候传⼊的 parentElm 是空,所以对组件的插⼊,在 createComponent 有这么⼀段逻辑:

js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
	let i = vnode.data
	if (isDef(i)) {
		const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
		if (isDef(i = i.hook) && isDef(i = i.init)) {
			i(vnode, false /* hydrating */)
		}
		// after calling the init hook, if the vnode is a child component
		// it should've created a child instance and mounted it. the child
		// component also has set the placeholder vnode's elm.
		// in that case we can just return the element and be done.
		if (isDef(vnode.componentInstance)) {
			initComponent(vnode, insertedVnodeQueue)
			insert(parentElm, vnode.elm, refElm)
			if (isTrue(isReactivated)) {
				reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
			}
			return true
		}
	}
}

在完成组件的整个 patch 过程后,最后执⾏ insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插⼊,如果组件 patch 过程中⼜创建了⼦组件,那么DOM 的插⼊顺序是先⼦后⽗。

总结

  • patch 的整体流程:createComponent -> 子组件初始化 -> 子组件 render ->子组件 patch 。

  • activeInstance 为当前激活的的 vm 实例;vm.$vnode 为组件的占位 vnode;vm._vnode 为组件的渲染 vnode。

  • 嵌套组件的插入顺序是先子后父。

那么到此,⼀个组件的 VNode 是如何创建、初始化、渲染的过程也就介绍完毕了。在对组件化的实现有⼀个⼤概了解后,接下来我们来介绍⼀下这其中的⼀些细节。我们知道编写⼀个组件实际上是编写⼀个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在 _init 的最初阶段执⾏的就是 merge options 的逻辑,那么下⼀节我们从源码⾓度来分析合并配置的过程。

合并配置

当全局 mixin 调用时,会进行合并配置。相关的代码在 src/core/global-api/mixins 文件中。

js
Vue.mixin = function (mixin: Object) {
	this.options = mergeOptions(this.options, mixin)
	return this
}

它会把传入的 mixin 合并到 Vue.options 中。通过之前章节的源码分析我们知道, new Vue 的过程通常有 2 种场景。

⼀种是外部我们的代码主动调⽤ new Vue(options) 的⽅式实例化⼀个 Vue 对象;

另⼀种是我们上⼀节分析的组件过程中内部通过 new Vue(options) 实例化⼦组件。

⽆论哪种场景,都会执⾏实例的 _init(options) ⽅法,它⾸先会执⾏⼀个 merge options 的逻辑,

相关的代码在 src/core/instance/init.js 中:

js
Vue.prototype._init = function (options?: Object) {
	// ...
	// 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
		)
	}
	// ...
}

可以看到不同场景对于 options 的合并逻辑是不⼀样的,并且传⼊的 options 值也有⾮常⼤的不同,接下来我会分开介绍 2 种场景的 options 合并过程。为了更直观,我们可以举个简单的⽰例:

js
import Vue from 'vue';

const childComp = {
  template: '<div>{{ msg }}</div>',
  created () {
    console.log('child created');
  },
  mounted () {
    console.log('child mounted');
  },
  data () {
    return {
      msg: 'Hello World.'
    }
  }
}

Vue.mixin({
  created () {
    console.log('parent created');
  },
})

new Vue({
  el: '#app',
  render: h => h(childComp)
})

外部调用场景

当执⾏ new Vue 的时候,在执⾏ this._init(options) 的时候,就会执⾏如下逻辑去合并 options :

js
vm.$options = mergeOptions(
	resolveConstructorOptions(vm.constructor),
	options || {},
	vm
)

这⾥通过调⽤ mergeOptions ⽅法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合 并, resolveConstructorOptions 的实现先不考虑,在我们这个场景下,它还是简单返回 vm.constructor.options ,相当于 Vue.options ,那么这个值⼜是什么呢,其实在 initGlobalAPI(Vue) 的时候定义了这个值,代码在 src/core/global-api/index.js 中:

js
export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  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)
  // ...
}

⾸先通过 Vue.options = Object.create(null) 创建⼀个空对象,然后遍历 ASSET_TYPES , ASSET_TYPES 的定义在 src/shared/constants.js 中:

js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以上⾯遍历 ASSET_TYPES 后的代码相当于:

js
Vue.options.components = {};
Vue.options.directives = {};
Vue.options.filters = {}

接着执⾏了 Vue.options._base = Vue ,它的作⽤在我们上节实例化⼦组件的时候介绍了。

最后通过 extend(Vue.options.components, builtInComponents) 把⼀些内置组件扩展到 Vue.options.components 上,Vue 的内置组件⽬前有 <keep-alive><transition><transition-group> 组件,这也就是为什么我们在其它组件中使⽤ <keep-alive> 组件不需要注册的原因,这块⼉后续我们介绍 <keep-alive> 组件的时候会详细讲。那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中:

js
/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }
	
  // 定义空对象,作为最后返回的值
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions 主要功能就是把 parent 和 child 这两个对象根据⼀些合并策略,合并成⼀个新对象并返回。

⽐较核⼼的⼏步,先递归把 extends 和 mixixns 合并到 parent 上,然后遍历 parent ,调⽤ mergeField ,然后再遍历 child ,如果 key 不在 perent 的⾃⾝属性上,则调⽤ mergeField 。

这⾥有意思的是 mergeField 函数,它对不同的 key 有着不同的合并策略。举例来说,对于⽣命周期函数,它的合并策略是这样的:

js
/**
 * Hooks and props are merged as arrays.
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:

js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

这⾥定义了 Vue.js 所有的钩⼦函数名称,所以对于钩⼦函数,他们的合并策略都是 mergeHook 函数。

这个函数的实现也⾮常有意思,⽤了⼀个多层 3 元运算符,逻辑就是如果不存在 childVal ,就返回 parentVal ;

否则再判断是否存在 parentVal ,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,⼀旦 parent 和 child 都定义了相同的钩⼦函数,那么它们会把 2 个钩⼦函数合并成⼀个数组。

关于其它属性的合并策略的定义都可以在 src/core/util/options.js ⽂件中看到,这⾥不⼀⼀介绍了,感兴趣的同学可以⾃⼰看。

通过执⾏ mergeField 函数,把合并后的结果保存到 options 对象中,最终返回它。

因此,在我们当前这个 case 下,执⾏完如下合并后:

js
vm.$options = mergeOptions(
	resolveConstructorOptions(vm.constructor),
	options || {},
	vm
)

vm.$options 的值差不多是如下这样:

js
vm.$options = {
  components: {},
  create: [
    function created () {
      console.log('parent created');
    }
  ],
  directives: {},
  filters: {},
  _base: function Vue (options) {
    // ...
  },
  el: '#app',
  render: function (h) {
    // ...
  }
}

组件场景

由于组件的构造函数是通过 Vue.extend 继承⾃ Vue 的,先回顾⼀下这个过程,代码定义在 src/core/global-api/extend.js 中。

js
/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
	// ...
	Sub.options = mergeOptions(
		Super.options,
		extendOptions
	)
    
	// ...
	// keep a reference to the super options at extension time.
	// later at instantiation we can check if Super's options have
	// been updated.
	Sub.superOptions = Super.options
	Sub.extendOptions = extendOptions
	Sub.sealedOptions = extend({}, Sub.options)

	// ...
	return Sub
}

我们只保留关键逻辑,这⾥的 extendOptions 对应的就是前⾯定义的组件对象,它会和 Vue.options(Super.options) 合并到 Sub.opitons 中。

接下来我们再回忆⼀下⼦组件的初始化过程,代码定义在 src/core/vdom/create-component.js 中:

js
export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

这⾥的 vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub , 所以 执⾏ new vnode.componentOptions.Ctor(options) 接着执⾏ this._init(options) ,因为 options._isComponent 为 true,那么合并 options 的过程⾛到了 initInternalComponent(vm, options) 逻辑。先来看⼀下它的代码实现,在 src/core/instance/init.js 中:

js
Vue.prototype._init = function (options?: Object) {
	// ...
	// 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)
	}
	// ...
}

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

initInternalComponent ⽅法⾸先执⾏ const opts = vm.$options = Object.create(vm.constructor.options) ,这⾥的 vm.construction 就是⼦组件的构造函数 Sub ,相当于 vm.$options = Sub.options

接着⼜把实例化⼦组件传⼊的⼦组件⽗ VNode 实例 parentVnode 、⼦组件的⽗ Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。这么看来, initInternalComponent 只是做了简单⼀层对象赋值,并不涉及到递归、合并策略等复杂逻辑。因此,在我们当前这个 case 下,执⾏完如下合并后:

js
initInternalComponent(vm, options)

vm.$options 的值差不多是如下这样:

js
vm.$options = {
  parent: Vue, // 父 Vue 实例
  propsData: undefined,
  _compoentTag: undefined,
  _parentVnode: VNode, // 父 VNode 实例
  _renderChildren: undefined,
  _proto__: {
    components: {},
    directives: {},
    filters: {},
    _base: function Vue (options) {
      // ...
    },
    _Ctor: {},
    create: [
      function created () {
        console.log('parent created');
      },
      function created () {
        console.log('child created');
      },
    ],
    mounted: [
      function mounted () {
        console.log('child mounted');
      },
    ],
    data () {
      return {
        msg: 'Hello World.'
      }
    },
    template: '<div>{{ msg }}</div>'
  }
}

总结

那么⾄此,Vue 初始化阶段对于 options 的合并过程就介绍完了,我们需要知道对于 options 的合并有 2 种⽅式,⼦组件初始化过程通过 initInternalComponent ⽅式要⽐外部初始化 Vue 通过 mergeOptions(遵循一定的合并策略) 的过程要快,合并完的结果保留在 vm.$options 中。

纵观⼀些库、框架的设计⼏乎都是类似的,⾃⾝定义了⼀些默认配置,同时⼜可以在初始化阶段传⼊⼀些定义配置,然后去 merge 默认配置,来达到定制化不同需求的⽬的。只不过在 Vue 的场景下,会对 merge 的过程做⼀些精细化控制,虽然我们在开发⾃⼰的 JSSDK 的时候并没有 Vue 这么复杂,但这个设计思想是值得我们借鉴的。

生命周期钩子函数

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

vue-lifecycle

在我们实际项⽬开发过程中,会⾮常频繁地和 Vue 组件的⽣命周期打交道,接下来我们就从源码的⾓度来看⼀下这些⽣命周期的钩⼦函数是如何被执⾏的。源码中最终执⾏⽣命周期的函数都是调⽤ 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 函数的逻辑很简单,根据传⼊的字符串 hook ,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执⾏,执⾏的时候把 vm 作为函数执⾏的上下⽂。在上⼀节中,我们详细地介绍了 Vue.js 合并 options 的过程,各个阶段的⽣命周期的函数也被合并

到 vm.$options ⾥,并且是⼀个数组。因此 callhook 函数的功能就是调⽤某个⽣命周期钩⼦注册的所有回调函数。

了解了⽣命周期的执⾏⽅式后,接下来我们会具体介绍每⼀个⽣命周期函数它的调⽤时机。

beforeCreate & created

beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init ⽅法中执⾏的,它的定义在 src/core/instance/init.js 中:

js
Vue.prototype._init = function (options?: Object) {
	// ...
	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')
	// ...
}

可以看到 beforeCreate 和 created 的钩⼦调⽤是在 initState 的前后, initState 的作⽤是初始化 props 、 data 、 methods 、 watch 、 computed 等属性,之后我们会详细分析。那么显然 beforeCreate 的钩⼦函数中就不能获取到 props 、 data 中定义的值,也不能调⽤ methods 中定义的函数。

在这俩个钩⼦函数执⾏的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,⼀般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩⼦函数执⾏都可以,如果是需要访问 props 、 data 等数据的话,就需要使⽤ created 钩⼦函数。

之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreatd 钩⼦函数。

beforeMount & mounted

顾名思义, beforeMount 钩⼦函数发⽣在 mount ,也就是 DOM 挂载之前,它的调⽤时机是在 mountComponent 函数中,

定义在 src/core/instance/lifecycle.js 中:

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