「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文中,我们讨论了 $mount 方法中的模板编译的大致流程,包含 compileToFunctions 方法的获取流程、createCompiler 方法中声明的 compile 方法,在 createCompiler 方法被调用的时候时传入的 baseCompile。最后了解了一下经过编译得到的 AST 对象以及经过 generate 生成的 render 函数体代码字符串;
本篇小作文的重点将放到 AST 对象生成的编译过程;
二、获取 ast 的 parse 方法
方法位置:src/compiler/parser/index.js -> function parse
方法参数:
template:模板字符串options:平台特有的编译配置选项,所谓平台就是web或者weex(weex是Vue的跨端项目,类似RN)
方法作用:该方法在 baseCompile 中调用,用于将模板编译成 AST 对象。
再看 parse 源码之前,先来看下在 baseCompile 中的调用
// 这一段是 baseCompile 函数中对 parse 的调用
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 调用 parse 方法获取 ast 对象,ast 对象是用于描述模板中节点关系和节点信息的对象
const ast = parse(template.trim(), options)
// ....
})
3.1 parse 方法
- 根据
options判断是否为pre标签、必须使用props绑定的属性、是否为平台保留标签、是否是组件 - 用
options.modules下的class、model、style三个模块中的方法处理class、style、v-model - 调用
parseHTML方法,传入start/end/char/comment的回调用于处于HTML节点
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
// 判断是否为 pre 标签
platformIsPreTag = options.isPreTag || no
// 必须用 props 绑定的属性
platformMustUseProp = options.mustUseProp || no
// 获取标签的命名空间
platformGetTagNamespace = options.getTagNamespace || no
// 判断是否是平台保留标签 html/svg
const isReservedTag = options.isReservedTag || no
// 判断一个元素是否为组件: 有几个标准,el.component 为 true 或 用了 :is
maybeComponent = (el: ASTElement) => !!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
// 以下三行是处根据 options.modules 下的 class、model、style 三个模块中的
// transformNode 处理 class
// preTransformNode 处理 style
// postTransformNode 处理 v-model
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
// 界定符:比如 {{}}
delimiters = options.delimiters
const stack = []
// 保留空格
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 声明根节点 root
// 经过处理的节点都会按照层级挂载到 root 下,最后 return 的就是一个 root,一个树形结构
let root
// 当前元素的父元素
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce (msg, range) {
}
function closeElement (element) {
}
function trimEndingWhitespace (el) {
}
function checkRootConstraints (el) {
}
// 调用 parseHTML
// 第二个参数中的 start、end、chars... 就是处理各个节点的方法
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) {
},
end (tag, start, end) {
},
chars (text: string, start: number, end: number) {
},
comment (text: string, start, end) {
}
})
// 返回生成的 ast 对象
return root
}
3.2 parseHTML 方法
方法位置:src/compiler/parser/html-parser.js -> function parseHTML
方法参数:
html:html字符串- options:解析 html 模板时所需配置项,这个配置项中包含处理不同类型模板的方法:
- 2.1
options.start方法,处理html开始标签 - 2.2
opitons.end方法,处理html结束标签 - 2.3
options.chars方法,处理html中的普通文本 - 2.4
options.comment方法,处理html中的注释
- 2.1
方法作用:
parseHTML 方法的核心就是遍历 html 模板字符串,用接收到 otpiosn 参数中对应方法我们写 html 模板的过程是一个有嵌套的过程,给人的感觉也是一个有深度的树形结构。但是 parseHTML 解析 html 模板的时候并非如此,它认为 html 模板是一个一维的字符串,那他如何解决深度的问题呢?
以 <div id="someID"><span></span></div> 结构为例:
parseHTML是一个while(html模板不为空)的while循环,另外有一个记录当前遍历到的字符位置的表示符index,初始值为0;- 接着从
html模板中找到<第一次出现的位置,即indexOf,在html中有很多种语法都会命中<开头,所以接着就是找到<小于号出现的位置后就看是判断本次是以下情形中的哪一种,然后调用对应options上的处理方法,处理后就continue进入下轮循环:-
2.2 HTML注释(
<!-- -->),调用options.comment方法处理注释 -
2.2 条件注释(
<!-- [if IE]-->),越过 -
2.3
DOCTYPE文档声明(<!DOCTYPE html>),越过 -
2.4 如果前面
2.1 - 2.3都没有匹配成功,接着就判断小于号后面是否是开始或者结束标签并调用parseEndTag或者parseStartTag方法处理开始、结束标签, -
2.5 如果前面
2.1 - 2.4都没匹配成功,说明<小于号就是个普通文本了,例如:"< 这就是个小于号",调用options.chars方法处理文本; -
2.6 处理这些
<小于号后面内容的各种情形时,还要处理这些注释、文档声明、开始/结束标签、带<的文本的内容长度有多少,然后就让index前进多少,目的就在于从index开始重新截取(html=html.substring(index)),将截取后的模板作为html,进入下一轮循环。 -
2.7 如果
<出现位置 等于0,说明从索引0开始就是普通字符,所以通过while循环向后查找,一直找到下一个<出现的位置为止,期间所有的都算作普通字符处理
-
export function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
// 是否为自闭和标签
const isUnaryTag = options.isUnaryTag || no
// 是否只有开始标签
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 这个是个标识符,用于记录遍历的起点,每次匹配到内容并且处理后将会从 index 重新截取模板,
// 截取后的模板作为下一轮的处理内容
let index = 0
let last, lastTag
// html = "<div id=\"app\">\n\t{{ msg }}\n\t<some-com :some-key=\"forProp\"></some-com>\n\t<div>someComputed = {{someComputed}}</div>\n\t<div class=\"static-div\">静态节点</div>\n</div>"
while (html) {
last = html
// 确保这些模板 html 不是 script 、style、textarea 这样的纯文本元素
if (!lastTag || !isPlainTextElement(lastTag)) {
// 找到 < 在 html 模板中第一次出现的位置赋值给 textEnd 变量
let textEnd = html.indexOf('<')
// textEnd === 0 说明当前 html 模板是以 < 开头的字符串
if (textEnd === 0) {
// 处理 HTML 注释语法:<!-- xx -->
if (comment.test(html)) {
// HTML 注释语法的结束标识 --> 出现的索引,在开始和结束索引之间就是 HTML 注释的内容
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
// 如果 options 有要求保留注释内容时调用 options.comment 方法处理
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 使 index 前进到注释 --> 结束以后的一个位置
// 并且 advance 会给 html 这个变量重新赋值,当 while(html) 下次循环是就是这个注释之后的内容了
advance(commentEnd + 3)
continue // while 循序下一次循环
}
}
// 处理条件注释语法:<!-- [if IE]>
if (conditionalComment.test(html)) {
// 找到条件注释结束索引位置
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
// 使 index 变量前进,重写截取 html 变量
advance(conditionalEnd + 2)
continue
}
}
// 处理文档声明 Doctype,<!DOCTYPE html>
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// 如果 while 循环能走到这里说明前面的注释、条件注释、文档声明都没有匹配到,
// 因为一旦匹配到了就 continue 了
// 处理结束标签,比如 </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length) // 这里要 index 前进结束标签的长度
parseEndTag(endTagMatch[1], curIndex, index)
continue // 下一轮循环
}
// 处理开始标签,比如 <div id="app">
// startTagMatch = { tagName: 'div', attrs: ["id=\"app\"", "id", "="], start: 0, end: 14 }
const startTagMatch = parseStartTag()
if (startTagMatch) {
// 走到这里,说明匹配到了HTML的标识标签,调用 options.start 方法处理开始标签
// 处理开始标签的核心就是这个 handleStartTag
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 走到这里说明虽然在 html 中匹配到了 < 但是上面的注释、条件注释、文档声明、开始结束标签都不是
// 那他是个啥?就是个普通的文本了: <我是小于号
// 从 < 第一次出现的位置 textEnd 开始截取剩下的 html 模板赋值给 rest
// 后面的这个 while 循环就是在查找下一个 < 出现的位置,这两个 < 之间的内容作为普通文本
rest = html.slice(textEnd)
while (
!endTag.test(rest) && // 不是结束标签
!startTagOpen.test(rest) && // 不是开始标签
!comment.test(rest) && // 不是注释
!conditionalComment.test(rest) // 不是条件注释
) {
// 上述条件成立表示 < 后面为纯文本,然后在 rest 中查找下一个 < 出现的索引
next = rest.indexOf('<', 1)
// 小于 0 表示 rest 后面没有 <,退出循环
if (next < 0) break
// 能走到这里说明后续字符串中找到了 <,索引位置 textEnd 累加 next,表示向后移动
textEnd += next
// 从新找到的 < 位置截取 html,赋值给 rest 表示接着找,
// 让 while 循环接续,知道不满足条件时,
// 即找到了结束标签、开始标签、注释或条件注释的某一种情况
rest = html.slice(textEnd)
}
// 当上面的 while() 结束了,此时 html 从 0 到 textEnd 中间都是普通文本
text = html.substring(0, textEnd)
}
// 如果 textEnd 到这里还小于0,说明压根就没有 <,html 就是一段纯纯的文本
if (textEnd < 0) {
text = html
}
// 使 index 前进 text.length,并更新 html 模板的值
if (text) {
advance(text.length)
}
// 处理 text 中的文本
if (options.chars && text) {
// 处理文本就是创建文本的的 ast 节点,然后把 ast 放到 parent 节点上
options.chars(text, index - text.length, index)
}
} else {
// 处理 script、style、textarea 标签的闭合标签
parseEndTag(stackedTag, index - endTagLength, index)
}
// 到这里如果 html 和 last 一样,说明都是文本,根本没有标签之类的
// 此外,如果 stack 数组中还有内容,则说明标签没闭合要提示
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()
function advance (n) {
}
function parseStartTag () {
}
function handleStartTag (match) {
}
function parseEndTag (tagName, start, end) {
}
}
三、总结
本篇小作文的重点是 parseHTML 的主流程:pareHTML 执行时设置标识符 index,相当于是个指针,它记录了当前需要处理的模板的起始位置,以 < 小于号作为标志判断注释、条件注释、开始标签、结束标签普通文本,调用相应的方法来处理的对应的情景;
parseHTML 方法是 $mount 方法的核心,是整个 Vue 的最复杂的过程,这一篇小作文也不是 $mount 的终章,下一篇会继续未尽的细节部分。
下一篇我们继续讨论 parseHTML 执行中所需要的具体方法如 advance、parseStartTag、parseEndTag ...