我们知道new Vue(options) 最终执行了vm.$mount方法,此方法在具有模版编译的版本中执行会调用entry-runtime-with-compiler.js中定义的$mount方法
$mount中主要逻辑就是如果options中没有用户手写的render函数,那么就取options.template属性,将template转化为render函数
compileToFunctions()就是将模版编译为render函数
/* @flow */
// 引入config文件
import config from '../../core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
// 引入Vue
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
// 返回缓存过了的函数结果
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 这个其实是runtime中的$mount方法
const mount = Vue.prototype.$mount
// 覆盖了runtime中的$mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获得element
el = el && query(el)
/* istanbul ignore if */
// 如果el是body,或者是document,报警告
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
// 如果不是render函数
if (!options.render) {
// 拿到options.template属性的值
let template = options.template
// 如果设置了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
)
}
}
// 否则如果template是节点对象
} 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) {
// 否则去拿到el的子节点
template = getOuterHTML(el)
}
if (template) {
// 性能埋点
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 将模板编译为render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 改变纯文本插入分隔符 默认是 ["{{", "}}"] 如果改成 ['${', '}'] 那么模板上就可以用 ${}去包裹数据了
delimiters: options.delimiters,
// 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
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')
}
}
}
// 执行runtime中的$mount方法
return mount.call(this, el, hydrating)
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
// 拿子节点内容
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
compileToFunctions()
方法就是模版编译的重点
我们先找到compileToFunctions方法的定义
compileToFunctions方法由createCompiler(baseOptions返回
const { compile, compileToFunctions } = createCompiler(baseOptions)
createCompiler方法由createCompilerCreator()方法返回
const createCompiler = createCompilerCreator(/参数省略/)
我们看createCompiler方法的定义
function createCompiler (baseOptions: CompilerOptions) {
// 代码省略
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
此函数返回了compileToFunctions的定义
我们看一下createCompileToFunctionFn(compile)的定义,此函数返回了compileToFunctions函数,此函数中执行了模版编译的函数将template编译为render函数并返回,接下来我们从此函数开始入手详细讲解编译的过程
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 代码省略
return (cache[key] = res)
}
}
compileToFunctions()
compileToFunctions是在entry-runtime-with-compiler.js文件中定义的$mount方法中调用的
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 改变纯文本插入分隔符 默认是 ["{{", "}}"] 如果改成 ['${', '}'] 那么模板上就可以用 ${}去包裹数据了
delimiters: options.delimiters,
// 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
comments: options.comments
}, this)
compileToFunctions函数中关键调用了compile方法,complie方法是作为createCompileToFunctionFn(compile)的参数传入的一个方法,主要就是编译template生成render函数的
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 获得传入的options参数
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// check cache
// 做了缓存处理,如果缓存中有已经编译过的相同模版字符串的render函数,那么直接返回
// ps: 如果template很长,那么这个key未免有点太长了吧,汗。。。
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
// 模版编译,将template编译为ast和render函数字符串并返回
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 = []
// 将render字符串转换为函数
res.render = createFunction(compiled.render, fnGenErrors)
// 生成静态节点的render函数
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
)
}
}
// 将res返回
return (cache[key] = res)
}
}
接下来我们分析一下complie方法
compile()
complie方法主要做了两件事情
- 将compileToFunctions传入的opitons和baseOptions合并,baseOptions后续讲解
- 调用baseCompile(template.trim(),finalOptions)方法编译模版生成ast和render函数
function compile (
template: string,
options?: CompilerOptions
): CompiledResult
{
// baseOptions
const finalOptions = Object.create(baseOptions)
// 错误
const errors = []
// 提示
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
// options传了的话,合并
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
// 合并modules,将传入的options的modules和baseOptions的modules合并
if (options.modules) {
// 合并自定义的 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
// 调用编译函数生成ast和render函数
// return {
// ast,
// render: code.render,
// staticRenderFns: code.staticRenderFns
// }
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
接下来我们分析一下baseCompile(template.trim(),finalOptions)方法
baseCompile()
此方法主要就是进行编译过程,将模版编译为ast和render函数
ast方法是由parse方法生成
code方法是由generate方法生成
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 将模板转换为抽象语法树
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 对ast做一些优化 主要是添加static标识,表示该节点是静态的,从第一次渲染后就不会变的,如文本节点
optimize(ast, options)
}
// 生成渲染函数字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
接下来我们分析一下parse方法
限于篇幅,我们将parse方法放在单独的章节讲解