前言
我们看源码,我觉得最好带着问题去看源码,这样我们会专注于一个点去看源码,不会被源码的一些其他功能,把我们带离最初想去的地方。本章主要的目的是,弄明白 vue 是如何生成虚拟 DOM 的。
从入口开始
我们从入口文件一步一步慢慢的分析。先看入口文件。
-
入口文件:
web/entry-runtime-with-compiler.js。
import Vue from './runtime/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element, // 根元素,可以是字符串或者是 DOM 元素
hydrating?: boolean // 服务端渲染相关,服务端渲染时为 true
): Component {
...
const options = this.$options
// 没有手写 render 方法时,获取 template(template 会被编译成 render 方法)
if (!options.render) {
// 先获取模板
let template = options.template
...
if (template) {
...
// render 方法会生成 vnode, template => render 方法 => vnode
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 将 render 方法添加到 this.$options 上
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 调用保存的 mount 函数,此时已获得由模板编译过来的 render 函数
return mount.call(this, el, hydrating)
}
-
runtime 文件:./runtime/index
import Vue from 'core/index'
import { mountComponent } from 'core/instance/lifecycle'
...
// 在原型上,添加 $mount 方法,这个方法会返回 mountComponent 方法。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
...
-
core 文件: core/index
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue) // 初始化全局 API,如 nextTick
...
-
instance 文件:./instance/index
// vue 的构造函数,代码模块化,方便维护
...
function Vue(options) {
...
this._init(options)
}
// 在 vue prototype 上添加方法
initMixin(Vue) // _init 方法等
stateMixin(Vue) // 数据相关,如 $watch 方法等
eventsMixin(Vue) // 事件相关,如 $emit 方法等
lifecycleMixin(Vue) // _update 方法等
renderMixin(Vue) // _render 方法等
new Vue 时发生什么
由上面的代码,我们可以看到,当我们new Vue() 时,会触发一系列的初始化,然后调用 _init() 方法。
生成虚拟 DOM
-
_init()
既然 new Vue() 时,调用的是 _init() 方法 ,我们就先看看 _init() 方法主要做了什么事情。它是在 initMixin() 函数执行时,添加到原型上的方法。在 init 方法的最后,我们看到如下代码。它调用了vm.$mount方法。
Vue.prototype._init = function (options?: Object) {
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
-
$mount 方法
在上面的分析中,我们知道 Vue 中有两个$mount 方法。定义如下:
// 第一个 $mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 第二个 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element, // 根元素,可以是字符串或者是 DOM 元素
hydrating?: boolean // 服务端渲染相关,服务端渲染时为 true
): Component {
...
if (!options.render) {
...
if (template) {
...
// render 方法会生成 vnode, template => render 方法 => vnode
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 将 render 方法添加到 this.$options 上
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 调用保存的 mount 函数,此时已获得由模板编译过来的 render 函数
return mount.call(this, el, hydrating)
}
我们可以看到:
-
第一个 $mount方法:是给第二个 $mount方法调用用的,它会返回mountComponent方法。 -
第二个 $mount方法:将template编译成render方法,保存render方法到$options上。最后调用第一个 $mount方法。
接下来,我们看下mountComponent方法。
-
mountComponent 方法
mountComponent 方法的主要代码如下:
// mountComponent
export function mountComponent(
vm: Component, // 组件实例
el: ?Element, // 挂载的元素
hydrating?: boolean // 服务端渲染相关
): Component {
...
updateComponent = () => {
// vm._render(),生成 vnode,在 instance/render.js 中
// vm._update(),更新 dom
vm._update(vm._render(), hydrating)
}
// watcher 会调用 updateComponent,先生成 vnode ,然后调用 update 更新 dom;
new Watcher(vm, updateComponent, noop, {
before() { ... }
}, true /* isRenderWatcher */)
return vm
}
// watcher
export default class Watcher {
constructor(
vm: Component, // 组件实例
expOrFn: string | Function,
cb: Function, // 当监听的数据变化时,会触发该回调
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// expOrFn 是 `updateComponent` 方法
this.getter = expOrFn
this.value = this.lazy
? undefined
: this.get()
}
get() {
...
try {
// 相当于执行 updateComponent()
value = this.getter.call(vm, vm)
} catch (e) {
...
}
}
mountComponent方法的作用是实例化一个Watcher,同时也创建了一个updateComponent方法。Watcher:作用是监听数据的变化,实例化时会执行updateComponent方法。它会执行下面这行代码。
vm._update(vm._render(), hydrating)
-
vm._render()
这个才是正主。前期的准备工作已做完,下面到了生成虚拟DOM的时候了。
Vue.prototype._render = function (): VNode {
...
// render 方法是由模板编译过来的
const { render, _parentVnode } = vm.$options
// render 生成 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
...
}
可以看到,vue 会调用由 template 编译过来的 render 方法生成 虚拟 DOM。需要注意的是:
- 当使用编译而来的 render 方法时,执行的是下面的
createElement生成虚拟DOM。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
- 当使用手写的 render 方法时,执行的是下面的
createElement生成虚拟DOM。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
然后 vue 具体是如何生成虚拟 DOM 的呢,且听下回分解。
总结
- 在
new Vue()时,会合并相关的配置项、执行一系列的初始化、以及调用_init()。 - 在
_init()方法中,会执行vm.$mount(vm.$options.el) $mount方法有两个,第一个返回mountComponent方法。第二个$mount方法是将模板编译成render方法,然后调用第一个mount方法。mountComponent方法中,实例化Watcher,在此过程中,会执行updateComponent方法,相当于调用vm._update(vm._render(), hydrating)- 在执行
vm._render()时,会调用由模板编译而来的render方法,最后生成了 虚拟 DOM。