两种模式
首先需要知道Vue的很多版本中的其中两种开发用的版本
- runtime only
- runtime with compiler
在使用vue cli初始化项目时,其中一步会提示要用哪种 Build 方式,给出了上面这两种方式,简单来说,区别就是
runtime only
需要使用webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,使用el 或者 <template> 标签或者 render 函数来开发的话,就用这个模式
new Vue({
render (h) {
return h('div', this.vinsea)
}
})
new Vue({
el: '#app'
})2
3
4
5
6
7
8
9
runtime with compiler
如果是直接用 template 属性来做模板,那就需要用compiler来编译。
new Vue({
template: '<p>{{ vinsea }}</p>'
})2
3
最终Vue编译认的都是 render 函数
runtime+compiler
上一章里说到的 vm.$mount() 实际调用的是这个模式下的代码
这个模式的入口文件在:\src\platforms\web\entry-runtime-with-compiler.js
// ... 省略一些代码 ...
// 获取dom字符串
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 缓存mount方法
const mount = Vue.prototype.$mount
// 重写mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 处理el参数,如果是字符串就用querySelector获取dom对象
el = el && query(el)
/* istanbul ignore if */
// 如果el是文档标签就报警告,因为这个el最终会把原来的标签内容替换,如果替换了文档标签就改变了文档结构
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
}
const options = this.$options
// 解析 template/el 转换成 render 函数
if (!options.render) {
// template是dom字符串
let template = options.template
// 如果有template属性
if (template) {
if (typeof template === 'string') {
// 井号开头的话会当做选择器,通过上面提到的query函数转为dom字符串
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) {
// 如果已经是dom对象,就直接获取innerHTML
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果没有template属性,但是有el,就获取outerHTML
template = getOuterHTML(el)
}
// ========== 下面都是跟【编译】相关 ==========
if (template) {
// ... 省略代码 ...
}
}
// 调用runtime only模式的mount方法
return mount.call(this, el, hydrating)
}
/**
* 获取元素的outerHTML,同时处理IE中的SVG元素。
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
// 为了解决某些元素比如SVG获取不到outerHTML
// 或者可以理解为outerHTML的polyfill
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue2
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
runtime+compiler 模式的文件做的事情,就是将el、template 转成 render函数,最终再调用 runtime only模式的 mount 方法
runtime only
\src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 因为runtime only版本是直接使用这个文件,所以在这里再做个容错判断
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}2
3
4
5
6
7
8
找到 mountComponent() 方法:\src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 缓存 el
vm.$el = el
if (!vm.$options.render) {
// 如果没有render函数就创建一个空的vnode
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
// 开发时用的是runtime only版本,但是写了template,没写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 {
// template和render都没写
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
// config.performance && mark是Vue做性能埋点用的,方便使用者查看性能
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ... 省略代码 ...
} else {
updateComponent = () => {
// 渲染
vm._update(vm._render(), hydrating)
}
}
// 【渲染watcher】 和响应式原理强相关的类,可以理解为观察者模式
// 第1个参数:vm,当前实例
// 第2个参数:上面代码定义的updateComponent函数
// 第3个参数:noop,空函数,在Watcher类中是回调函数,所以这里回调函数是空函数
// 第4个参数:一个配置对象
// 第5个参数:Boolean,是否是渲染watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 手动挂载实例,调用挂载在self上
// 安装在其插入的钩子中用于渲染创建的子组件
if (vm.$vnode == null) {
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
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
mountComponent 方法的核心是 updateComponent(),它的作用就是渲染,不仅需要在第一次加载的时候渲染,在后续改变数据的时候也要动态渲染,所以需要用 Watcher 来监听并执行,从而实现数据驱动。
这里的 vm._update(vm._render(), hydrating) 在下一节讲。
可以粗略看一下 new Watcher():\src\core\observer\watcher.js
export default class Watcher {
// ... 省略代码 ...
constructor (
vm: Component, // 实例
expOrFn: string | Function, // 表达式
cb: Function, // 回调
options?: ?Object, // 配置对象
isRenderWatcher?: boolean // 是否是渲染watcher
) {
this.vm = vm
if (isRenderWatcher) {
// 是渲染watcher就加一个标识
vm._watcher = this
}
vm._watchers.push(this)
// ... 省略代码 ...
// 转换第二个参数(表达式)为一个函数,赋值给 this.getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 计算getter,然后重新收集依赖项
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 调用expOrFn
value = this.getter.call(vm, vm)
} catch (e) {
// ... 省略代码 ...
}
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
53
54
55
56
57
58
59
Wathcer 是通过class关键字创建的一个类,构造函数接受五个参数,结合上面mountComponent 中的代码,可以知道,在实例化Watcher时,第二个参数传入了 updateComponent函数,Watcher会调用自身的get()方法,这个方法会调用 this.getter(),而这个getter是通过构造函数的第二个参数传入的。
这里我们先不管Watcher监听的原理,得出的结论就是,Watcher在监听到变化时,就会执行 updateComponent,也就会执行 vm._update(vm._render(), hydrating)