当我们初始化Vue的时候,我们有如下几种方式:
- 第一种
new Vue({
data () {
return {
name: '请叫我张先森'
}
},
el: '#app'
})
- 第二种
new Vue({
data () {
return {
name: '请叫我张先森'
}
},
el: '#app',
template: '<div>{{ name }}</div>'
})
- 第三种
new Vue({
data () {
return {
name: '请叫我张先森'
}
},
el: '#app',
render (h) {
return h('div', null, [this.name])
}
})
那么,Vue是如何处理这些不同情况呢? 这就涉及到 Vue挂载中的模板编译, 今天我们就看看 Vue是如何编译模板的;
源码文件定位: src/platforms/web/entry-runtime-with-compiler.js
先提供一个流程图,然后再 进行源码解析
graph TD
queryEl(获取option.el)
queryEl -- 存在 --> checkEl(是否是body或者html) -- 是 --> 抛出异常
checkEl(是否是body或者html) -- 否 --> checkRender
queryEl -- 不存在 --> checkRender(检测是否有render函数)
checkRender -- 有 --> mount(挂载)
checkRender -- 否 --> checkTemplate(检测是否有template选项)
checkTemplate -- 是 --> classifyTemplate(判断template)
classifyTemplate -- 字符串以 # 开头 --> idToTemplate(获取id的节点的innerHTML) --> compileToFunction(转成render函数)
classifyTemplate -- 元素节点
--> compileToFunction(转成render函数)
classifyTemplate -- 否则 --> 报错
checkTemplate -- 否 --> checkIsEl(检测el是否存在)
checkIsEl -- 是 --> compileToFunction(转成render函数)
compileToFunction --> mount(挂载)
从上吗的流程图我们可以看到:
- 如果
option有render函数, 那么直接去挂载 - 如果 是合法的
template或者el那么将模板编译成render函数 ,再去挂载 上面所有的工作 都是为了 得到render函数
下面是源码: 各位读者可以对照源码,再去理解一下 上面的流程图
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 <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)
}
理解了上面的流程图, 我们看下今天的核心部分, 也就是 如何 将 template 和 el 编译成 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
compileToFunctions顾名思义, 就是将模板编译成函数, 那么 它是如何工作的呢?
-
在这里我们看到
compileToFunctions是由createCompiler(baseOptions)生成的 -
createCompiler是由createCompilerCreator(baseCompile)生成的 -
createCompilerCreator返回了compileToFunctions函数 -
但是 发现
compileToFunctions函数又是由createCompileToFunctionFn(compile)生成的 我们整理一下 流程
graph TB
createCompilerCreator --baseCompile函数--> createCompiler -- baseOptions --> compile --> compileToFunctions
createCompileToFunctionFn --> compileToFunctions
流程看清楚了, 我们现在开始阅读源码;
createCompilerCreator
因为 createCompiler 在调用 createCompilerCreator的时候传入了一个 baseCompile 函数,我们现在只需要知道 baseCompile 函数 是用来编译我们的模板的就行了, 这个函数的源码解析,我们会在下一篇文章发布;
createCompilerCreator的基本结构:
看到这个基本结构, 其实就可以看到 闭包的影子了, 当我们在 调用
createCompilerCreator函数的时候, 传入了 baseCompile 函数, 这样我们 在后序的对模版编译的时候, 不需要 再传入 baseCompile函数了; 然后返回 createCompiler 函数
createCompiler
在调用 createCompiler函数的时候, 传入了一个 baseOptions 参数, 这是用来辅助 baseCompile函数, 生成我们想要的编译函数
graph LR
baseOptions --> compile
baseCompile --> compile
createCompileToFunctionFn
在我们通过 调用 createCompiler的时候, compileToFunctions 是由 createCompileToFunctionFn 结合 我们自己的定制的编译函数, 这样,我们下次编译模板的时候, 只需要 调用 compileToFunctions函数, 不需要重新定制编译函数了
compileToFunctions
前面的一系列操作, 都是为 生成 compileToFunctions 作准备的, 为了 阅读清晰,我删除了一些 开发的输出
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null) // 缓存策略
// 返回 compileToFunctions 函数
// 这就是我们文章一开始 编译模板的函数
return function compileToFunctions (
template: string, // 我们传入模板
options?: CompilerOptions, // 传入的 编译选项
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
// check cache 设置 key, 用于 读取 或者 缓存 已经编译的模板
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) { // 如果存在 那么直接返回
return cache[key]
}
// compile 根据我们定制的 编译函数 编译模板
const compiled = compile(template, options)
// turn code into functions
const res = {}
const fnGenErrors = []
// 返回渲染函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 缓存
return (cache[key] = res)
}
}
因为我们 通过 compile 生成的后的 render 是一个字符串,所以我们要转成 函数; 关于 with 的用法,读者可以查阅其他资料, 这里不做赘述;
综上, 我们可以基本上了解到 Vue模板的基本的步骤
- 首先 根据
baseCompile通过createCompilerCreator生成 创建编译器createCompiler函数 baseOptions通过 创建编译器createCompiler函数 生成我们想要的 模板编译函数compilecompile通过createCompileToFunctionFn生成 我们 需要的compileToFunctions函数- 当我们调用
compileToFunctions函数时, 会根据我们的传入的 模板和配置 ,生成 渲染函数
自此, 模板编译的整体流程,已经结束, 后面我们会解析,