Compile entry
// 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
The definition of : define the variable mount
as $mount
the method on the prototype, and then $mount
redefine the ;
// 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)
}
As can be seen from the above code, $mount
the following things are mainly done:
- Determine
el
whether is mounted to a normal node, notbody
orhtml
; - If
template
the attribute , it is processed:- If the incoming string
#
starts with , it is considered toid
gettemplate
the template ; - If a node is passed in, get the node content directly;
- If none of the above two conditions exist, a warning is thrown;
- If the incoming string
- If the
render
andtemplate
, directly assignel
the corresponding outer html structure totemplate
; template
After processing , callcompileToFunctions
to convert it intorender
a function and assign it tooptions
;- Return the result
mount.call(this, el, hydrating)
of , and then make supplementary explanations later;
compileToFunctions
is the core method of template compilation, we can see how it is implemented:
// 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
}
})
Analyzing the above code, we can divide it into several steps:
- Convert the incoming
template
template into an ast abstract syntax tree; - Optimize static nodes;
- Generated from the transformed
ast
treecode
, that is,render
the function ;
Let's first look at the specific implementation of parse
the method :
// 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
}
It can be seen from parse
the method that it mainly executes parseHTML
to parse the template. The parsing process generates the corresponding ast
and . The timing of executing the callback includes matching the start tag, end tag, text, and comment . parseHTML
How to do it specifically We can continue to look at:
// 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)
}
}
}
}
Analyzing the key code above, we can know parseHTML
that it uses regular expressions to match template strings, and parses when encountering start tags, end tags, text, and comments. After parsing, the corresponding ast
tree and the corresponding parent and child can be established. relationship, and then continue to intercept the string until the string is parsed;
After parsing the template string to generate ast
a tree , the next step is to ast
generate the corresponding one according to the tree 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)
}
}
The above code is part of the code in the file corresponding to generate
the method . If you are interested in methods such as genComment
, genText
, and so on, you can look at the source code by yourself. Here we mainly talk about the main idea of generating ;genProps
code
generate
The method is mainly obtained by getting ast
the tree for parsing, executing getElement
to obtain code
, getElement
and el
will judge
- If it is a static node, a node containing instructions
v-once
,v-for
,v-if
(here it can also be seenv-for
that the instructionv-if
has a higher priority than the instruction), template, slot, etc., then perform corresponding processing; - If it is a component, it is
genComponent
generatedcode
; - Otherwise execute
genChildren
recursive creation;
After the final execution, a similar structure will be generated:
// _c 代表创建节点,_v 代表创建文本,_s 代表变量,下面只是个例子,还有其他的函数
_c('div',{
id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
Summarize:
The entry point of template compilation is vm.$mount(vm.$options.el)
to $mount
parse template
or el
and convert it to render
, and then execute the original $mount
for rendering. render
The core method of conversion is that compileToFunctions
the core steps of this method are mainly divided into three steps:
template
Convert the incoming intoast
a tree;- Optimize static nodes;
- generated from the generated
ast
treecode
;