模板:写在标签中的类似于原生HTML的内容
模板编译是用模板生成一个render函数
render函数就可以生成与模板对应的VNode,之后再进行patch算法,最后完成视图渲染。
分三个阶段:
功能 | 类 | 源码路径 | |
---|---|---|---|
模板解析 | 将一堆模板字符串用正则等方式解析成抽象语法树AST | 解析器 | src/compiler/parser/index.js |
优化 | 遍历AST,找出其中的静态节点,并打上标记 | 优化器 | src/compiler/optimizer.js |
代码生成 | 将AST转换成渲染函数 | 代码生成器 | src/compiler/codegen/index.js |
在模板内,除了有常规的HTML标签外,还有一些文本信息以及在文本信息中包含过滤器。
不同的内容需要不同的解析规则,对应不同解析器。常规HTML的parseHTML,文本parseText、过滤器解析器parseFilters(解析文本中如果包含过滤器)。
HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。
总之:一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST。解析器内维护了一个栈,用来保证构建的AST节点层级与真正DOM层级一致。
// 代码位置:/src/complier/parser/index.js
/**
* Convert HTML string to AST.
* 将HTML模板字符串转化为AST
*/
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数
start (tag, attrs, unary) {
//生成元素类型的AST节点
},
// 当解析到结束标签时,调用该函数
end () {
},
// 当解析到文本时,调用该函数
chars (text) {
//生成文本类型的AST节点
},
// 当解析到注释时,调用该函数
comment (text) {
//生成评论类型的AST节点
}
})
return root
}
调用parseHTML函数时为其传入的两个参数分别是:
template:待转换的模板字符串;
options:转换时所需的选项,提供了一些解析HTML模板时的一些参数,同时还把这4个钩子函数作为参数传给解析器parseHTML,当解析器解析出不同的内容时调用不同的钩子函数从而生成不同的AST。
例如start
// 当解析到标签的开始位置时,触发start
start (tag, attrs, unary) {
let element = createASTElement(tag, attrs, currentParent)
}
export function createASTElement (tag,attrs,parent) {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent,
children: []
}
}
完整的
function parseHTML(html, options) {
const stack = [] // 维护AST节点层级的栈
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no //用来检测一个标签是否是可以省略闭合标签的非自闭合标签
let index = 0 //解析游标,标识当前从何处开始解析模板字符串
let last, // 存储剩余还未解析的模板字符串
lastTag // 存储着位于 stack 栈顶的元素
// 开启一个 while 循环,循环结束的条件是 html 为空,即 html 被 parse 完毕
while (html) {
last = html;
// 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
/**
* 如果html字符串是以'<'开头,则有以下几种可能
* 开始标签:
* 结束标签:
* 注释:
* 条件注释:
* DOCTYPE:
* 需要一一去匹配尝试
*/
if (textEnd === 0) {
// 解析是否是注释
if (comment.test(html)) {
}
// 解析是否是条件注释
if (conditionalComment.test(html)) {
}
// 解析是否是DOCTYPE
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
}
// 解析是否是结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
}
// 匹配是否是开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
}
}
// 如果html字符串不是以'<'开头,则解析文本类型
let text, rest, next
if (textEnd >= 0) {
}
// 如果在html字符串中没有找到'<',表示这一段html字符串都是纯文本
if (textEnd < 0) {
text = html
html = ''
}
// 把截取出来的text转化成textAST
if (options.chars && text) {
options.chars(text)
}
} else {
// 父元素为script、style、textarea时,其内部的内容全部当做纯文本处理
}
//将整个字符串作为文本对待
if (html === last) {
options.chars && options.chars(html);
if (!stack.length && options.warn) {
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""));
}
break
}
}
// Clean up any remaining tags
parseEndTag();
//parse 开始标签
function parseStartTag() {
}
//处理 parseStartTag 的结果
function handleStartTag(match) {
}
//parse 结束标签
function parseEndTag(tagName, start, end) {
}
}
判断传入的文本是否包含变量
构造expression
构造tokens
let text = "我叫{
{name}},我今年{
{age}}岁了"
let res = parseText(text)
res = {
expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
tokens:[
"我叫",
{
'@binding': name },
",我今年"
{
'@binding': age },
"岁了"
]
}
parseText函数接收两个参数,一个是传入的待解析的文本内容text,一个包裹变量的符号delimiters。
纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中
用_s()包裹变量存入tokens中,同时再把变量名构造成{’@binding’: exp}存入rawTokens
Vue在HTML解析器的开头定义了一个栈stack。HTML解析器在从前向后解析模板字符串时,每当遇到开始标签时就会调用start钩子函数,在start钩子函数内部将解析得到的开始标签推入栈中,而每当遇到结束标签时就会调用end钩子函数,在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出。
标签没有被正确闭合,此时控制台就会抛出警告:‘tag has no matching end tag.'这就是栈的第二个用途: 检测模板字符串中是否有未正确闭合的标签。
静态节点一旦首次渲染上了之后不管状态再怎么变化它都不会变了
在AST中找出所有静态节点并打上标记;
在AST中找出所有静态根节点并打上标记;
function isStatic (node: ASTNode): boolean {
if (node.type === 2) {
// 包含变量的动态文本节点
return false
}
if (node.type === 3) {
// 不包含变量的纯文本节点
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
静态节点的情况:
标记完当前节点是否为静态节点之后,如果该节点是元素节点,那么还要继续去递归判断它的子节点.如果当前节点的子节点有一个不是静态节点,那就把当前节点也标记为非静态节点.
静态根节点的情况:
如果当前节点不是静态根节点,那就继续递归遍历它的子节点node.children和node.ifConditions。
Vue只要调用了render函数,就可以把模板转换成对应的虚拟DOM。
当用户手写了render函数时,那么Vue在挂载该组件的时候就会调用用户手写的这个render函数。
如果没有写,Vue就要自己根据模板内容生成一个render函数供组件挂载的时候调用。
Vue.prototype.$mount = function (el){
const options = this.$options
// 如果用户没有手写render函数
if (!options.render) {
// 获取模板,先尝试获取内部模板,如果获取不到则获取外部模板
let template = options.template
if (template) {
} else {
template = getOuterHTML(el)
}
const {
render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}