Vue2.x源码分析(二):Vue实例挂载的实现

Vue2.x源码分析(二):Vue实例挂载的实现

Vue源码分析 + 逐行注释 Github地址

Vue实例挂载阶段,这里只分析web平台的实现,web平台的入口文件是src/platforms/web/entry-runtime-with-compiler.js,在此文件中Vue通过$mount方法进行实例挂载。

Vue挂载阶段都做了什么?

src/platforms/web/entry-runtime-with-compiler.js文件$mount方法定义之前,$mount已经在runtime(src/platforms/web/runtime/index.js文件)里定义了一遍,在该文件里刚开始就对·$mount做一个缓存,缓存为mount变量,方便之后使用。

entry-runtime-with-compiler.js文件做了什么?

entry-runtime-with-compiler.js文件中的$mount方法在Vue init时被调用,此方法接收两个参数,第一个参数elDOM元素,可以是stringElement,第二个参数hydrating是关于Vue服务端渲染的,可以忽略。

首先,将传入的el参数通过query方法转换DOM对象,接着判断el是否为body或者documentElement,如果是则报出错误(不允许为<html> 或者<body>) 并直接return当前实例,不再往下执行。

接下来,对实例上的$options进行一个缓存,缓存变量为options,因为Vue支持直接传入render函数,所以会进行判断如果不存在render函数,则判断是否有template,再把template转换成一个render函数,最终目的是调用缓存的mount方法进行DOM挂载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// $mount已经在runtime里定义了一遍,在这里对$mount做一个缓存
const mount = Vue.prototype.$mount
// 重新定义$mount
// 此$mount在init时被调用
// 最终目的是调用mount方法进行DOM挂载
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 通过query方法转换DOM对象
el = el && query(el)

/* istanbul ignore if */
// 判断是否为body或者documentElement,如果是则报出错误(不允许为<html> 或者 <body>)
// document.body:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/body
// document.documentElement:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/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
}
// options缓存
const options = this.$options
// resolve template/el and convert to render function
// Vue支持传入render函数
// 如果不存在render函数,则判断是否有template,再把template转换成一个render函数
// 所有template最终都会被转换成render
if (!options.render) {
// 缓存template
let template = options.template
// 如果存在template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果template是一个字符串,并且第一个字符为'#'
// 则此时的template是一个DOM的ID名
// 通过idToTemplate方法转换成真实DOM
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}r
}
} else if (template.nodeType) {
// 如果template是一个Node节点
// 则将template赋值为template.innerHTML(DOM字符串)
// nodeType:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
// innerHTML:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/innerHTML
template = template.innerHTML
} else {
// 如果以上都不是,则报错
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果el存在,则调用getOuterHTML获取outerHTML
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 通过compileToFunctions获取到render
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.
* 获取outerHTML
* outerHTML:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/outerHTML
* appendChild:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
* cloneNode:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/cloneNode
* polyfill:https://developer.mozilla.org/zh-CN/docs/Glossary/Polyfill
*/
function getOuterHTML (el: Element): string {
// 如果outerHTML存在,则直接return
if (el.outerHTML) {
return el.outerHTML
} else {
// 如果不存在,则创建一个空的div,将el深度克隆到新的div的尾部
// 做一个outerHTML的polyfill
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
// 返回当前innerHTML
return container.innerHTML
}
}

runtime/index.js $mount文件做了什么?

runtime里的$mount同样接收两个参数,与entry-runtime-with-compiler.js文件中无差。

runtime $mount方法首先会将传入来的el参数通过query方法转换DOM对象(如果是浏览器环境),然后调用mountComponent方法生成虚拟DOM,并将生成的虚拟DOMreturn出去。

1
2
3
4
5
6
7
8
9
10
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 通过query方法转换DOM对象
el = el && inBrowser ? query(el) : undefined
// 调用mountComponent生成虚拟DOM
return mountComponent(this, el, hydrating)
}

mountComponent方法做了什么?

mountComponent方法定义在src/core/instance/lifecycle.js下,此方法第一个第一个参数vm需传入一个Vue实例,第二个与第三个参数与$mount方法的参数意义一样。

mountComponent方法首先会将传入的el缓存挂载到Vue的实例上,名为$el,紧接着判断render函数是否存在,如果存在则会创建一个空的VNode,然后判断是否为production环境,因为在production环境中,runtime-only版本如果当前配置了templatetemplate传入的不是一个id名称或者存在el属性,则报错,此时的情况只能使用render函数进行DOM挂载。

判断完是否存在render函数后,则会调用beforeMount生命周期钩子,此时DOM正在挂载中。下一步则创建一个updateComponent方法,该方法调用render方法生成虚拟node,然后实例化一个渲染Watcher,通过Watcher监听数据改变,当数据改变时,调用第二个参数触发_update进行更新DOM。

所有任务进行完成后,将当前实例重新return出去。

mountComponent方法主要作用就是渲染DOM,然后监听数据改变,进行DOM更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 缓存$el
vm.$el = el
// 如果当前没有render函数
if (!vm.$options.render) {
// 创建一个空的VNode
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
// 在runtime-only版本如果当前配置了template且template传入的不是一个ID名称
// 或者存在el属性
// 则报错
// 此时只能使用render函数
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
)
}
}
}

// 调用beforeMount生命周期钩子
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 = () => {
// 调用render方法生成虚拟node
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
// 当数据改变时,调用第二个参数触发_update进行更新DOM
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
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×