版本:2.5.17。
我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。
学习文档:https://ustbhuangyi.github.io/vue-analysis/data-driven/mounted.html
src/core/instance/init.js : 在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
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
这个方法的实现是和平台、构建方式都相关的。
先来看一下 src/platform/web/entry-runtime-with-compiler.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 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)
}
/**
* 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
这段代码首先缓存了原型上的 $mount
方法,再重新定义该方法,我们先来分析这段代码。首先,它对 el
做了限制,Vue 不能挂载在 body
、html
这样的根节点上。
我们会发现该js重新定义了Vue.prototype.$mount方法,而该方法来自于src/platform/web/runtime/index.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)
}
那是因为,src/platform/web/entry-runtime-with-compiler.js 中的是适用于 Runtime+Compiler 版本的。而src/platform/web/runtime/index.js 中的 Vue.prototype.$mount 方法是适用于 Runtime Only 版本的,
我们可以看到对于Vue.prototype.$mount参数是可以传递 字符串 和 DOM对象的。
我们来看一下 query 方法 ,src/platform/web/util/index.js:
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
/* 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
}
如果没有定义 render
方法,则会把 el
或者 template
字符串转换成 render
方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render
方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el
或者 template
属性,最终都会转换成 render
方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions
方法实现的
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')
}
}
}
上文说道el是DOM对象,如果el 是 body 或者 html元素的话就会报错,那是因为挂载是覆盖的,如果挂载在body或html上, 那么整个HTML文档就不对了。 所以我们一般采用的都是挂载在div上的形式。
如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。 compileToFunctions是对我们最后生成的模板进行解析,生成render函数。
该方法来自于:.src/platform/compiler/index.js,如果我们的例子是:
{{message}}
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
}]
}
2.对ast进行优化,分析出静态不变的内容部分,增加了部分属性:
因为我们这里只有一个动态的{{message}},所以static和staticRoot都是false。
{
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
}]
}
3.ast生成render函数和staticRenderFns数组。
render = function () {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}
4.在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,比如我们把例子中的模板改为:
这是静态内容
{{message}}
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方法。原先原型上的 $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 核心就是先调用 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') {...}
}
3.实例化一个渲染Watcher。
Watcher 在这里起到两个作用, 一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。.new Watcher传的参数1.vue实例,2.updateComponent函数,3.空函数, 4.对象,5布尔值。
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。
4.通过watcher回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
5.函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm