上一篇 Vue2源码阅读——模板编译(一) 我们解析了模板编译的一个整体流程, 这篇文章我们看看 Vue时如何编译模板的;
compileToFunctions
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)
// turn code into functions
const res = {}
const fnGenErrors = []
// 因为 compile 返回的 render是字符串所以需要 转成函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 缓存已经编译的模板,并且返回
return (cache[key] = res)
}
compile
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// baseOptions 由 createCompiler 传入
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// 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
// 将 最终的配置 和 模板 传入到 baseCompile中
// 返回最后编译的对象 {ast, render, staticRenderFns }
const compiled = baseCompile(template.trim(), finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled
}
我们可以在控制台输出finalOptions
的最终配置:
baseCompile
最基本的编译。我们看下 baseCompile
函数
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
- 将传入的
template
通过parse
函数 转成AST
(抽象语法树) - 将生成的
AST
(抽象语法树) 通过generate
函数 ,转成code
- 返回
{ast, render: code.render, staticRenderFns: code.staticRenderFns}
parse
函数
以下面的代码为例,我们进行讲解
<div id="app"></div>
new Vue
el: '#app'
})
我们传入的template: "<div id=\"app\"></div>"
生成的AST
:
我们进一步了解,
parse
函数 是如何生成 AST
源码文件定位: src/compiler/parser/index.js
/**
* Convert HTML string to AST.
*/
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce (msg, range) {
if (!warned) {
warned = true
warn(msg, range)
}
}
function closeElement (element) {
trimEndingWhitespace(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
function trimEndingWhitespace (el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
function checkRootConstraints (el) {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start (tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production') {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length
}
)
}
})
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars (text: string, start: number, end: number) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment (text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
})
return root
}
generate
函数
将传入的AST
转成代码, 我们现在可以看到 render
的样子了, 这是不是 跟我们直接写 render
函数很相似呢?