编译入口
// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ... 其他代码
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
$mount
的定义:定义变量 mount
为原型上的 $mount
方法,然后对 $mount
进行重新定义;
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
// 重新定义 $mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(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) {
let template = options.template
// 对模板进行处理
if (template) {
// 如果是字符串,且首字符是 #,表示根据 id 来获取 template 模板
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
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) {
// 如果不存在 render 和 template,但存在 el,则直接将 el 对应的外层 html 结构赋值给 template
template = getOuterHTML(el)
}
// 处理完 template 之后,将其转换为 render
if (template) {
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 根据 template 获取 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
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${
this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用原先 Vue 原型上 $mount 方法
return mount.call(this, el, hydrating)
}
从上面的代码可以看出,$mount
主要做了以下几件事:
- 判断
el
是否是挂载到正常节点,而不是body
或html
; - 如果存在
template
属性,则对其进行处理:- 如果传入的是以
#
开头的字符串,则认为是根据id
来获取template
模板; - 如果传入的是一个节点,直接获取节点内容;
- 以上两种情况都没有,则抛出警告;
- 如果传入的是以
- 如果不存在
render
和template
属性,则直接将el
对应的外层 html 结构赋值给template
; - 处理完
template
之后,调用compileToFunctions
将其转为render
函数并赋值给options
; - 返回原先方法
mount.call(this, el, hydrating)
的结果,后续再进行补充说明;
compileToFunctions
是模板编译的核心方法,我们可以看看是怎么实现的:
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 将传进来的 template 转化为 ast 树,描述自身形成的结构
const ast = parse(template.trim(), options)
// 静态节点的优化
if (options.optimize !== false) {
optimize(ast, options)
}
// 根据生成的 ast 树和配置 options 生成代码,也就是 render 函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
分析上面的代码,我们可以将它分为几个步骤:
- 将传进来的
template
模板转化为 ast 抽象语法树; - 对静态节点进行优化;
- 根据转化的
ast
树生成code
,也就是render
函数;
我们先来看看 parse
方法的具体实现:
// src/compiler/parser/index.js
// 创建 ast
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {
},
parent,
children: []
}
}
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ...
// 只关注重点代码,其他代码省略
function closeElement (element) {
trimEndingWhitespace(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
if (!stack.length && element !== root) {
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) {
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {
}))[name] = element
}
// 建立父子关系
currentParent.children.push(element)
element.parent = currentParent
}
}
}
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) {
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
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 }
)
}
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
}
// for、if、once 等指令的处理
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
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]
stack.length -= 1
// 将 currentParent 置为当前匹配到的完整标签内容的父节点
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
}
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) {
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
// 创建 ast 文本节点
if (text) {
if (!inPre && whitespaceOption === 'condense') {
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) {
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
}
从 parse
方法中可以看出,其主要是执行 parseHTML
来对模板进行解析,解析过程通过回调来生成对应的 ast
并建立父子关联,执行回调的时机包括匹配到开始标签、结束标签、文本、注释,parseHTML
具体怎么做我们可以继续看:
// src/compiler/parser/html-parser.js
// 解析 HTML 相关的正则表达式
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${
unicodeRegExp.source}]*`
const qnameCapture = `((?:${
ncname}\\:)?${
ncname})`
const startTagOpen = new RegExp(`^<${
qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${
qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0 // 当前字符串的索引
let last, lastTag
while (html) {
last = html
if (!lastTag || !isPlainTextElement(lastTag)) {
// 查找第一个 "<" 的位置
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 匹配到注释
if (comment.test(html)) {
// 找到第一个 "-->" 的位置
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
// 截取注释内容,执行回调
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// index 往后走,并截取字符串
advance(commentEnd + 3)
continue
}
}
// 匹配条件注释
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// doctype 匹配
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 匹配结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 匹配开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
// 循环往后走,直到找到开始标签 | 结束标签 | 注释 | 条件注释
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
next = rest.indexOf('<', 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
}
// 纯文本
if (textEnd < 0) {
text = html
}
if (text) {
advance(text.length)
}
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${
html}"`, {
start: index + html.length })
}
break
}
}
parseEndTag()
// 截取 html, 记录当前字符串的位置
function advance (n) {
index += n
html = html.substring(n)
}
// 解析开始标签,返回一个对象,包括该标签的属性和位置
function parseStartTag () {
// 匹配开始标签
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
// 匹配到,字符串向前截取匹配的长度
advance(start[0].length)
// 匹配结束标签和属性
let end, attr
// 处理开始标签到闭合符号 ">" 之间的属性
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index
advance(attr[0].length)
attr.end = index
match.attrs.push(attr)
}
// 匹配到结束标签,开始标签解析结束
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
// 属性处理
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
if (!unary) {
// 入栈,后续可用来匹配结束标签
stack.push({
tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
// 找到匹配标签在栈中的位置
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
}
if (pos >= 0) {
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
options.warn(
`tag <${
stack[i].tag}> has no matching end tag.`,
{
start: stack[i].start, end: stack[i].end }
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// 从栈中移除开始标签
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
分析上面的关键代码,我们可以得知 parseHTML
是利用正则表达式匹配模板字符串,遇到开始标签、结束标签、文本、注释则进行解析,解析完毕之后就能生成对应的 ast
树,并建立相应的父子关系,然后不断截取字符串,直到字符串解析完毕;
解析完模板字符串生成 ast
树,接下来就是根据 ast
树生成相应的 code
:
// src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${
code}}`,
staticRenderFns: state.staticRenderFns
}
}
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
// 静态节点、指令节点、插槽和 template 标签的处理
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
let code
// 组件
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${
el.tag}'${
data ? `,${
data}` : '' // data
}${
children ? `,${
children}` : '' // children
})`
}
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
// 获取子节点的 code
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${
(altGenElement || genElement)(el, state)}${
normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${
children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${
normalizationType}` : ''
}`
}
}
function genNode (node: ASTNode, state: CodegenState): string {
// 判断节点类型,根据节点类型返回相应的 code
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
上面的代码是 generate
方法对应文件中的部分代码,像 genComment
、genText
、genProps
等方法有兴趣的可以自行看源码,这里主要讲生成 code
的主要思路;
generate
方法主要通过拿到 ast
树进行解析,执行 getElement
来获取 code
,getElement
会对 el
进行判断
- 如果是静态节点、包含指令
v-once
、v-for
、v-if
的节点(这里也可以看出v-for
的指令比v-if
指令的优先级高)、template、插槽等,则进行相应的处理; - 如果是组件,则通过
genComponent
生成code
; - 否则执行
genChildren
递归创建;
最后执行完毕后会生成类似的结构:
// _c 代表创建节点,_v 代表创建文本,_s 代表变量,下面只是个例子,还有其他的函数
_c('div',{
id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
总结:
模板编译的入口是vm.$mount(vm.$options.el)
,$mount
会解析 template
或 el
并转换为 render
,然后执行原先的 $mount
进行渲染,转换 render
的核心方法是 compileToFunctions
,该方法的核心步骤主要分三步:
- 将传进去的
template
转换为ast
树; - 对静态节点进行优化;
- 根据生成的
ast
树生成code
;