在上一节中我们知道,在Vue初始化的最后是挂载到DOM上的。
src/core/instance/init.js :
在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
下面我们来了解一下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
我们重点分析带 compiler 版本的 $mount 实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入。
src/platform/web/entry-runtime-with-compiler.js :
import Vue from './runtime/index'
import { query } from './util/index'
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)
}
我们会发现该js重新定义了Vue.prototype.$mount方法,而该方法来自于src/platform/web/runtime/index.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)
}
那是因为,src/platform/web/runtime/index.js 中的 Vue.prototype.$mount 方法是适用于 Runtime Only 版本的,而 src/platform/web/entry-runtime-with-compiler.js 中的是适用于 Runtime+Compiler 版本的。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
})
我们可以看到对于Vue.prototype.$mount参数是可以传递 字符串 和 DOM对象的。
我们来看一下 query 方法
src/platform/web/util/index.js:
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(el) 方法获得字符串代表的DOM对象,如果发现没有,就会抱一个错误并且返回一个空div。
所以 el = el && query(el) 代表的一定是一个DOM
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
}
上文说道el是DOM对象,如果el 是 body 或者 html元素的话就会报错,那是因为挂载是覆盖的,如果挂载在body或html上,那么整个HTML文档就不对了。
所以我们一般采用的都是挂载在div上的形式。
如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的,
if (!options.render) {
let template = options.template
if (template) {
} else if (el) {
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
}
}
如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。
compileToFunctions是对我们最后生成的模板进行解析,生成render函数。
该方法来自于:.src/platform/compiler/index.js,
如果我们的例子是:
<div id="app">
<p>{{message}}p>
div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: '第一个vue实例'
}
})
script>
步骤1:
解析template,生成ast。
{
type: 1,
tag: 'div',
plain: false,
parent: undefined,
attrs: [{name:'id', value: '"app"'}],
attrsList: [{name:'id', value: 'app'}],
attrsMap: {id: 'app'},
children: [{
type: 1,
tag: 'p',
plain: true,
parent: ast,
attrs: [],
attrsList: [],
attrsMap: {},
children: [{
expression: "_s(message)",
text: "{{message}}",
type: 2
}]
}
{
type: 1,
tag: 'div',
plain: false,
parent: undefined,
attrs: [{name:'id', value: '"app"'}],
attrsList: [{name:'id', value: 'app'}],
attrsMap: {id: 'app'},
static: false,
staticRoot: false,
children: [{
type: 1,
tag: 'p',
plain: true,
parent: ast,
attrs: [],
attrsList: [],
attrsMap: {},
static: false,
staticRoot: false,
children: [{
expression: "_s(message)",
text: "{{message}}",
type: 2,
static: false
}]
}
render = function () {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}
在src/core/instance/render.js中,我们曾经添加过如下多个函数,这里和render内返回值调用一一对应。
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = _toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
这里的staticRenderFns目前是一个空数组,其实它是用来保存template中,静态内容的render,比如我们把例子中的模板改为:
<div id="app">
<p>这是<span>静态内容span>p>
<p>{{message}}p>
div>
staticRenderFns就会变为:
staticRenderFns = function () {
with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
}
mount.call(this, el, hydrating)
我们知道该js保存了mount = Vue.prototype.mount,然后又重新定义了Vue.prototype.上的方法
该js的最后又调用了mount方法。该方法来自于:
src/platform/web/runtime/index.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 方法实际上会去调用 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
。。。
}
...
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent 核心就是先调用 vm._render 方法先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。
步骤1 :
做DOM对象的缓存
vm.$el = el
步骤2:
判断是否有render函数
如果用户没有写render函数,并且template也没有转化为render函数,就会生成一个VNode节点,并在生成环境报警告。
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {。。。}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
Watcher定义在以下文件
src/core/observer/watcher.js
constructor (
vm: Component,
// 表达式
expOrFn: string | Function,
// 回调
cb: Function,
// 配置对象
options?: ?Object,
// 是否渲染Watcher的标准位
isRenderWatcher?: boolean
) {
this.vm = vm
// 如果渲染Watcher为true,则在 vm中添加_watcher
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
...
} else {
...
}
this.cb = cb
...
// 如果是开发环境就将 expOrFn toString
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 将expOrFn函数转化为getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
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
)
}
}
//计算属性
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
// 调用this.get()
this.value = this.get()
}
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 调用this.getter,也就是调用expOrFn
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
}
我们会把expOrFn也就是updateComponent赋值给this.getter,并且在获取this.value的值时会调用this.get(),进而调用了updateComponent。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
对于compiler 版本
1. 对el(也就是挂载的DOM)进行处理
2. 看是否有render函数,没有的话将template转化为render函数
3. 调用以前定义好的Vue.prototype.$mount也就是去调用mountComponent方法
4. mountComponent定义了updateComponent这个函数
5. updateComponent是一个通过渲染Watcher实现的调用
(updateComponent这个函数其实是实现了一次真实的渲染,通过Watcher调用,可以实现初始化渲染和数据更新时重新渲染)