Vue源码-Vue mount方法分析

mount挂载过程

1
2
3
4
5
6
7
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
....
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

运行时版本中存在两个 $mount 方法,Vue内部首先会将公开版本赋值给 mount 变量,然后重写 $mount方法,并在该方法中进行调用 mount 方法,mount 方法主要调用 mountComponent 方法,用于加载组件。

1
2
3
4
5
6
7
8
9
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
console.log('invoke mountComponent')
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
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
const mount = Vue.prototype.$mount

Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
console.log('$mount: 加载组件')
el = el && query(el) // 查询el,如果不存在就创建一个div

/* istanbul ignore if */
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
// 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, {
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)
}
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)

return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn

// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}

// compile
const compiled = compile(template, options)

// check compilation errors/tips
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}

// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})

// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}

return (cache[key] = res)
}
}


export function createCompilerCreator (baseCompile: Function): Function {
console.log('创建compilerCreator')
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []

let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}

if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length

warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}

finalOptions.warn = warn

const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}

return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}

export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
console.log('解析template, 生成ast语法树')
const ast = parse(template.trim(), options)
console.log('对语法树进行优化')
if (options.optimize !== false) {
optimize(ast, options)
}
console.log('生成render方法')
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})


const { compile, compileToFunctions } = createCompiler(baseOptions)

compileToFunctions(template, {})

总结

有一点像插槽的感觉,后面如果要替换编译方案,直接修改 createCompilerCreator 就可以了,而内部的代码基本不需要改变。
先编写好内部的代码,留出一个插槽,随时可以替换。
有个两参数需要被设置,所以利用柯里化,则需要调用两次。此处利用了自执行方案,两次设定预定的基本参数,包括 baseOptionbaseCompile。 而对于在 baseCompiler 包裹之后的 complier 方法,又利用了 createCompileToFunctionFn 在进行包裹一层,对于compiler出来的内容,利用new Function 转换成function代码。这里有一点像装饰模式。
那么为什么不直接一次就搞定,要分两次去设置baseOption和baseCompile?
1 baseOption 是一定不会有改变的,而 baseCompile 是后面可能被调整的,他们两个是没有必要被耦合在一起。
2 分开设置可以降低耦合性。
3 compile 会被传递到后面的函数进一步进行处理,而 baseOption 只是被设置一次就可以
4 如果要放在一起的话,那么其实也可以直接放到一个对象中就可以了,这应该是关于纯函数的划分问题。
5 另外 baseOption 会被用到,需要和用户填入的option进行合并,而 baseCompiler 则是只需要存储一份就好了,保存在闭包中即可。后面需要用到编译,那么都可以用同一个 baseCompile 的包装function。