Vue.js一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作DOM,而是通过修改数据。当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为DOM变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触DOM,这样的代码非常利于维护。
在Vue.js中我们可以采用简洁的模板语法来声明式的将数据渲染为DOM:
{{ message }}
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
数据驱动还有一部分是数据更新驱动视图变化,我们的首要目标是弄清楚模板和数据如何渲染成最终的DOM。
new Vue发生了什么
从入口代码开始分析,我们先来分析new Vue
背后发生了哪些事情。Vue
实际上是一个类,源码在src/core/instance/index.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
中定义。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue
初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data、props、computed、watcher
等等。
在初始化的最后,检测到如果有el
属性,则调用vm.$mount
方法挂载vm
,挂载的目标就是把模板渲染成最终的DOM,那么接下来我们来分析Vue
的挂载过程。
Vue 实例挂载的实现
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
版本的$mount
实现,因为抛开webpack的vue-loader
,我们在纯前端浏览器环境分析Vue
的工作原理,有助于我们对原理理解的深入。
先来看一下src/platform/web/entry-runtime-with-compiler.js
文件中定义:
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 or - 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, {
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
直接使用的。
// 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对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。
$mount
方法实际上会去调用mountComponent
方法,这个方法定义在src/core/instance/lifecycle.js
文件中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
从上面的代码可以看到,mountComponent
核心就是先实例化一个渲染Watcher
,在它的回调函数中会调用updateComponent
方法,在此方法中调用vm._render
方法先生成虚拟Node
,最终调用vm._update
更新DOM。
Watcher
在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当vm
实例中的监测的数据发生变化的时候执行回调函数。
函数最后判断为根节点的时候设置vm._isMounted
为true
,表示这个实例已经挂载了,同时执行mounted
钩子函数。 这里注意vm.$vnode
表示Vue
实例的父虚拟Node
,所以它为Null
则表示当前是根Vue
的实例。
mountComponent
方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._render
和vm._update
。
render
Vue
的_render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟Node
。它的定义在src/core/instance/render.js
文件中:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// 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 {
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') {
if (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
}
} else {
vnode = vm._vnode
}
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof 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
方法。
render
函数的第一个参数是createElement
,那么结合之前的例子:
{{ message }}
相当于我们编写如下render
函数:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
再回到_render
函数中的render
方法的调用:
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到,render
函数中的createElement
方法就是vm.$createElement
方法:
export function initRender (vm: Component) {
// ...
// 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
方法。
vm._render
最终是通过执行createElement
方法并返回的是vnode
,它是一个虚拟Node
。