上一小节中提到 compile 函数(src/compiler/index.js)就是将 template 编译成 render function 的字符串形式。接下来就详细讲解这个函数:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) //1. parse
optimize(ast, options) //2.optimize
const code = generate(ast, options) //3.generate
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompiler
函数主要通过3个步骤:parse
、optimize
、generate
来生成一个包含ast
、render
、staticRenderFns
的对象。
在说parse
函数之前,我们先来了解一个概念:AST(Abstract Syntax Tree)抽象语法树:
AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 John Resig 的 HTML Parser 对模板进行解析,得到的就是 AST 代码。
接着我们看一下Vue中对AST数据的定义:
declare type ASTNode = ASTElement | ASTText | ASTExpression;
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<{ name: string; value: string }>;
attrsMap: { [key: string]: string | null };
parent: ASTElement | void;
children: Array<ASTNode>;
...
}
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
static?: boolean;
// 2.4 ssr optimization
// 2.4+ 增加了对ssr的标识
ssrOptimizability?: number;
};
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
// 2.4 ssr optimization
// 2.4+ 增加了对ssr的标识
ssrOptimizability?: number;
};
可以看到 ASTNode
有三种形式:ASTElement
,ASTExpression
,ASTText
。通过属性type
来进行标识。
下面我们正式进入parse
函数功能:
function parse(template) {
...
const stack = [];
let currentParent; //当前父节点
let root; //最终返回出去的AST树根节点
...
parseHTML(template, {
start: function start(tag, attrs, unary) {
......
},
end: function end() {
......
},
chars: function chars(text) {
......
}
})
return root
}
我们省略了parse的相关内容,只看一下大体的功能,其主要的功能函数应该是parseHTML
方法。接受了2个参数,一个使我们的模板template
,另一个是包含start
、end
、chars
的方法。
在看parseHTML
之前,我们需要先了解一下下面这几个正则:
// 该正则式可匹配到 <div id="index"> 的 id="index" 属性部分
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配起始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配DOCTYPE、注释等特殊标签
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。有了上面这些基础,我们再来看看parseHtml
的内部实行:
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) {
// 保留 html 副本
last = html
// 如果没有lastTag,并确保我们不是在一个纯文本内容元素中:script、style、textarea
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
...
}
if (conditionalComment.test(html)) {
...
}
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
...
}
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
...
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
...
}
}
let text, rest, next
if (textEnd >= 0) {
...
}
if (textEnd < 0) {
text = html
html = ''
}
// 绘制文本内容,使用 options.char 方法。
if (options.chars && text) {
options.chars(text)
}
} else {
...
}
...
}
上面只看一下代码的大概意思:
- 首先通过
while (html)
去循环判断html
内容是否存在。 - 再判断文本内容是否在
script/style
标签中 - 上述条件都满足的话,开始解析
html
字符串
假设我们传递这样一个html字符串<div id="demo">{{msg}}</div>
。我们来看其中一段关于Start tag
解析的方法:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
这里面有parseStartTag
和 handleStartTag
两个方法值得关注一下:
function parseStartTag () {
//判断html中是否存在开始标签
const start = html.match(startTagOpen)
if (start) {
// 定义 match 结构
const match = {
tagName: start[1], // 标签名
attrs: [], // 属性名
start: index // 起点位置
}
/**
* 通过传入变量n来截取字符串,这也是Vue解析的重要方法,通过不断地蚕食掉html字符串,一步步完成对他的解析过程
*/
advance(start[0].length)
let end, attr
// 如果还没有到结束标签的位置
// 存入属性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
// 返回处理后的标签match结构
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
经过上面一步的解析,我们得到了一个起始标签match
的数据结构:
function handleStartTag (match) {
// match 是上面调用方法的时候传递过来的数据结构
const tagName = match.tagName
const unarySlash = match.unarySlash
...
const unary = isUnaryTag(tagName) || !!unarySlash
// 备份属性数组的长度
const l = match.attrs.length
// 构建长度为1的空数组
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] || ''
// 改变attr的格式为 [{name: 'id', value: 'demo'}]
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
}
}
// stack中记录当前解析的标签
// 如果不是自闭和标签
// 这里的stack这个变量在parseHTML中定义,作用是为了存放标签名 为了和结束标签进行匹配的作用。
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
// parse 函数传入的 start 方法
options.start(tagName, attrs, unary, match.start, match.end)
}
到这里似乎一切明朗了许多,parseHTML
主要用来蚕食html字符串,解析出字符串中的tagName
,attrs
,match
等元素,传入start
方法:
start (tag, attrs, unary) {
...
// 创建基础的 ASTElement
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
...
if (!inVPre) {
// 判断有没有 v-pre 指令的元素。如果有的话 element.pre = true
// 官网有介绍:<span v-pre>{{ this will not be compiled }}</span>
// 跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 处理原始属性
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
// v-for v-if v-once
processFor(element)
processIf(element)
processOnce(element)
// element-scope stuff
processElement(element, options)
}
// 检查根节点约束
function checkRootConstraints (el) {
if (process.env.NODE_ENV !== 'production') {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.'
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
)
}
}
}
// tree management
if (!root) {
// 如果不存在根节点
root = element
checkRootConstraints(root)
} else if (!stack.length) {
// 允许有 v-if, v-else-if 和 v-else 的根元素
...
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
// 将元素插入 children 数组中
currentParent.children.push(element)
element.parent = currentParent
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
endPre(element)
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
其实start
方法就是处理 element
元素的过程。确定命名空间;创建AST元素 element;执行预处理;定义root;处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。
最终通过 createASTElement 方法定义了一个新的 AST 对象:
export function createASTElement (
tag: string,
attrs: Array<Attr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
}
下面我们来屡一下parse
整体的过程:
- 通过
parseHtml
来一步步解析传入html字符串的标签、元素、文本、注释.. parseHtml
解析过程中,调用传入的start
,end
,chars
方法来生成AST语法树