模板:写在标签中的类似于原生HTML的内容
模板编译是用模板生成一个render函数
render函数就可以生成与模板对应的VNode,之后再进行patch算法,最后完成视图渲染。
分三个阶段:
| 功能 | 类 | 源码路径 | |
|---|---|---|---|
| 模板解析 | 将一堆模板字符串用正则等方式解析成抽象语法树AST | 解析器 | src/compiler/parser/index.js |
| 优化 | 遍历AST,找出其中的静态节点,并打上标记 | 优化器 | src/compiler/optimizer.js |
| 代码生成 | 将AST转换成渲染函数 | 代码生成器 | src/compiler/codegen/index.js |
与web 组件API的HTML内容模板(<template>)元素无关,后者是浏览器提供的模版元素。提前在页面中做标记,让浏览器自动将其解析为DOM子树,但先跳过渲染,后面在运行时使用JavaScript实例化
模板解析
- 在template模板内,除了有常规的HTML标签外,还有一些文本信息以及在文本信息中包含过滤器。
- 不同的内容需要不同的解析规则,对应不同解析器。parseHTML,parseText、过滤器解析器parseFilters(解析文本中包含过滤器时)。
- HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。
- 解析器内维护了一个栈,用来保证构建的AST节点层级与真正DOM层级一致。
parseHTML
// 代码位置:/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个不同的内容分别调用的钩子函数
例如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字符串是以'<'开头,则有以下几种可能
* 开始标签:<div>
* 结束标签:</div>
* 注释:<!-- 我是注释 -->
* 条件注释:<!-- [if !IE] --> <!-- [endif] -->
* DOCTYPE:<!DOCTYPE html>
* 需要一一去匹配尝试
*/
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) {
}
}
textParser
判断传入的文本是否包含变量,构造expression、tokens
parseText函数接收两个参数
- 待解析的文本内容text
- 包裹变量的符号delimiters
let text = "我叫{{name}},我今年{{age}}岁了"
let res = parseText(text)
res = {
expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
tokens:[
"我叫",
{'@binding': name },
",我今年"
{'@binding': age },
"岁了"
]
}
- 纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中
- 用_s()包裹变量存入tokens中,同时再把变量名构造成{'@binding': exp}存入rawTokens
如何保证AST节点层级关系
- Vue在HTML解析器的开头定义了一个栈stack
- HTML解析器在从前向后解析模板字符串时,每当遇到开始标签时就会调用start钩子函数,在start钩子函数内部将解析得到的开始标签推入栈中
- 每当遇到结束标签时就会调用end钩子函数,在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出
- 标签没有被正确闭合,此时控制台就会抛出警告:‘tag has no matching end tag.'这就是栈的第二个用途: 检测模板字符串中是否有未正确闭合的标签。
优化
静态节点一旦首次渲染上了之后不管状态再怎么变化它都不会变了
在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)
))
}
静态节点的情况:
- 节点使用了v-pre指令 v-pre:跳过这个元素及其子元素的编译过程。可以用来显示原始标签。会加快编译
- 如果没有使用v-pre指令,那它要成为静态节点必须满足:
-
不能使用动态绑定语法,即标签上不能有v-、@、:开头的属性
-
不能使用v-if、v-else、v-for指令;
-
不能是内置组件,即标签名不能是slot和component;
-
标签名必须是平台保留标签,即不能是组件;
-
当前节点的父节点不能是带有 v-for 的 template 标签;
-
节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;
标记完当前节点是否为静态节点之后,如果该节点是元素节点,那么还要继续去递归判断它的子节点。如果当前节点的子节点有一个不是静态节点,那就把当前节点也标记为非静态节点。
render代码生成
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
}
}
compileToFunctions的来历如图