Vue 组件 mount
# $mount 实现
Vue 实例化的最后一步就是 Vue 实例的挂载,这里有一个判断:当 vm.$options.el
这个选项存在的时候 就会去执行 $mount 的操作。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
2
3
还有一种场景,那就是组件的挂载过程,组件的构造不用传递 el 属性,所以不会走到这里的逻辑。
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
2
3
4
5
$mount 这个方法在 web 平台,针对不同的场景有不同的实现,
# runtime
runtime
版本的 Vue,这个方法的实现比较简单,内部调用了一个 mountComponent
的方法,并把 当前的 组件实例(this)
、要挂载的 dom 元素(el)
传入了进去。参数 hydrating
是用来判断是否是服务端渲染的,这个做 ssr 的时候会用到。
Vue.prototype.$mount = function (el, hydrating): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
2
3
4
# runtime + compiler
而加了 编译器版本
的 Vue,这个方法的实现就相对复杂了一些。首先采用了一种面向切面编程(AOP)
的思想,首先缓存了原始的函数实现,然后在重写的方法中加入针对这种场景的处理逻辑后,最终又去调用了原始的实现。
此外这里有几个判断逻辑需要关注下:
首先是判断了要挂载的元素是否是 body 或者 html,vue 不允许这样做,因为最终渲染的结果会替换掉被挂载的元素,而不是插入到被挂载节点的内部
然后就是,如果在实例化的时候,同时传入了 template 以及 render 函数,这两个的优先级
render
>template
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating): Component {
el = el && query(el)
// 不能挂载到 body 或者 html 元素上
if (el === document.body || el === document.documentElement) return this
const options = this.$options
// 首先判断了组件的选项对象上是否定义了 render 方法
if (!options.render) {
let template = options.template
// 如果没有 render 方法,就看下是否定义了 template 属性
if (template) {
// template 是字符串模板
if (typeof template === 'string') {
// 选择符匹配元素的 innerHTML 模板
if (template.charAt(0) === '#') {
// 则当做 id 选择器,获取元素
template = idToTemplate(template)
}
} else if (template.nodeType) {
// template 为 dom 元素匹配元素的 innerHTML 模板
template = template.innerHTML
} else {
return this
}
} else if (el) {
// 如果没有定义 template,就去拿 el 节点的HTML
template = getOuterHTML(el)
}
// 如果得到了 template, 就转换成 render 函数
if (template) {
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
)
// 将 render 函数挂到了 options 上,接下来 mount 的流程中会用到它
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后调用原始的 mount 函数进行组件挂载
return mount.call(this, el, hydrating)
}
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
从这两种实现上可以看出,
compiler
版本的 vue,模板的编译操作是在执行 $mount 方法时,通过compileToFunctions
方法实现的,也就是所说的在运行时进行的编译
。runtime
版本的 vue,则是借助于像 webpack 这样的构建工具加上 vue-loader 等相关插件,在打包构建时将 template 编译成 render 函数的
具体的,在 vue-loader 的实现中,该部分的功能其实是借助的 vue 源码中的
vue-template-compiler
包进行处理的,这个包是维护在源码的packages
目录下。他的版本是跟 vue 的版本一致的,这也是为什么官方要求编译器的版本必须和基本的 vue 包保持同步
的原因,因为这样才能保证组件的 template 能够被编译成正确的 render 函数。要不然假如某一天 vue 支持了一个新的模板语法,而编译器还用的老版本,这就意味着新的语法无法被正确编译成 render 函数,从而导致错误
# template 的三种形式
- 字符串模板
var vm = new Vue({
el: '#app',
template: '<div>模板字符串</div>',
})
2
3
4
- dom 元素匹配元素的 innerHTML 模板
<div id="app">
<div>test1</div>
<span id="test"><div class="test2">test2</div></span>
</div>
<script>
var vm = new Vue({
el: '#app',
template: document.querySelector('#test'),
})
</script>
2
3
4
5
6
7
8
9
10
- 选择符匹配元素的 innerHTML 模板
<div id="app">
<div>test1</div>
<script type="x-template" id="test">
<p>test</p>
</script>
</div>
<script>
var vm = new Vue({
el: '#app',
template: '#test',
})
</script>
2
3
4
5
6
7
8
9
10
11
12
# mountComponent
过程
两种版本的 vue,最终的 mount 逻辑中都是走到了 mountComponent 这个方法
export function mountComponent(vm, el, hydrating): Component {
vm.$el = el
// 1. 这里又校验了 render 方法是不是存在,不存在的话给默认值兜底,开发环境下给警告
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
}
// 2. beforeMount 钩子触发
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 3. 渲染 watcher
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
hydrating = false
if (vm.$vnode == null) {
// 4. 挂载完成,添加标记并触发 mounted 钩子
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
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
这里主要分四步,
- 对 render 方法做兜底处理
- 调用 beforeMount 钩子
- 创建 渲染 Watcher 实例
- 组件挂载完成,添加挂载完成标记并触发 mounted 钩子
其中最重要的一步,就是在创建渲染 Watcher 实例这一步
# 渲染 Watcher
这部分代码单独拿出来看下
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
},
true /* isRenderWatcher */
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
先解释下,这里的渲染 Watcher 具体做了哪些事情,然后再去看实现细节。
- 需要被渲染的组件都会经历 mount 过程,所以它们都会拥有一个渲染 Watcher
- 渲染 Watcher 的第二个参数是
updateComponent
这个方法,这个方法调用的时机有两个:- 一个就是现在,即
组件挂载
的时候 - 另外一个就是组件内部依赖的
响应式数据发生改变
的时候,这个场景下还会调用第四个参数中的before
方法,触发beforeUpdate
钩子
- 一个就是现在,即
- 在
updateComponent
这个方法的内部,vm._render
方法的作用就是调用组件的 render 方法(用户手写的 render
或者编译生成的 render
),调用 render 方法得到的结果就是组件的 VNodevm_update
方法的作用就是将 render 生成的 VNode 渲染成真实的 DOM 元素,插入到页面中去,也就是常说的 Patch 过程
# Watcher
下面的代码只保留了关键部分,其他部分先省略,这里的参数再说明一下
- vm 是当前组件实例
- expOrFn 就是上面的 updateComponent
- cb 这里是一个空函数,可以忽略
- options 这里有一个 before 属性
- isRenderWatcher 标记当前 watcher 是否是 渲染 Watcher,这里是 true
export default class Watcher {
constructor (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm
// 如果是 渲染Watcher
if (isRenderWatcher) {
vm._watcher = this
}
// 组件实例的 _watchers 属性,用于存储组件内的所有 watcher,包括渲染watcher 以及组件内定义的 watcher
vm._watchers.push(this)
// 将 before 函数挂载 当前 watcher 实例上
if (options) {
this.before = options.before
}
// 将 updateComponent 定义为当前的 watcher 实例的 getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
// lazy 代表的是惰性求值,这里是false,所以会立即调用 watcher 实例的 get 方法
this.value = this.lazy
? undefined
: this.get()
}
/**
* get方法会立即调用 this.getter
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 调用 this.getter,这里就是 updateComponent 方法
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
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
下面看 Watcher 实例化过程,
- 首先如果当前 watcher 是渲染 watcher 的话,会将当前 watcher 实例挂在 当前组件实例
vm._watcher
属性上 - 并在当前组件实例的
_watchers
数组中,压入当前 watcher - 然后将
options.before
挂在了当前 watcher 的before
属性上 - expOrFn 如果是一个 function 的话,就挂在当前 watcher 的
getter
属性上 - 最后就是判断如果当前 watcher 不是
惰性求值
的话,就通过执行 watcher 实例的 get 方法,求取 watcher 实例上value
属性的值,这个时候就会触发上面的getter
当前的 渲染 watcher 实例化 的场景中,get 方法会立即执行,也就意味着 updateComponent
这个方法会被立即执行。
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
2
3
上面已经提到,这个方法执行的实质就是:
vm._render
内部通过调用组件的 render 方法,生成 VNodevm._update
通过调用组件实例的vm._patch
方法,将 VNode 渲染成真实的 DOM,并插入到页面中,完成渲染。这也就是常说的 patch 过程
到这里,组件 mount 的大体流程就说完了,下一篇说一下 VNode 的生成过程