「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文讨论了 parseHTML 方法中收到回调方法中的两个比较简单的方法:options.comment、options.chars;
其中 options.comment 用于处理注释,方法忽略 root 平级的注释节点,然后创建注释内容的 ast 节点,并添加到 currentParent.children 数组中;
options.chars 处理字符,根据配置的相关字符选项决定压缩与否,然后根据是否有 Vue 的动态绑定语法 {{xx}} 创建不同类型 type 的 ast 节点,有动态绑定语法 type 为 2,没有 type 为 3
两个最重要的方法 options.start、options.end 方法,这两个方法作为重中之重,而本篇小作文的重点是先了解 options.start 方法的大致逻辑。其中涉及的繁杂的细节处理方法的逻辑,将放到下一篇小作文中讨论。
二、options.start
方法位置:parse 内方法,parseHTML 方法的回调方法;
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ....
parseHTML(template, {
// ...
shouldKeepComment: options.comments,
start (tag, attrs, unary, start: number, end: number) {
}
})
// 返回生成的 ast 对象
return root
}
方法参数:
tag: 开始标签名;attrs:attrs数组,是经过handleStartTag后的attrs,形如 :[{ name: attrName, value: attrValue, start, end }, ....]unary:是否自闭和标签start:开始位置end:结束位置
方法作用:
parseHTML 匹配到开始标签时调用 parseStartTag 得到 startMatch 对象,再调用 handleStartTag 方法处理 startMatch 对象,handlerStartTag 就会调用 options.start 方法生成 ast,其具体过程包含:
- 调用
createASTElement方法创建AST节点 ——element; for循环preTransforms,这个数组是从parse方法接收到的options.modules中提取出来的,用于处理class、style、model,这个内容我们稍后会详细介绍;- 检查当前元素是否有
v-pre指令,存在则设置element.pre = true; - 检测是否有
<pre/>标签,有则将标识符遍历inPre置为true; - 如果有
v-pre指令,调用processRawAttrs方法传入element对象; - 若
element.prcessed不为true就调用proceessFor处理v-for,proceessIf处理v-if,processOnce处理v-once; - 处理
root,如果root不存在说明当前正在处理的元素就是root - 如果
unary参数不为true,说明是非自闭和标签,此时维护currentParent元素为当前element,后面处理到它的子元素时就知道要给这个currentParent添加子元素;此外还要将当前elementpush到stack中,这里这个stack和parseHTML中的stack不一样,parseHTML中的stack收录的是开始标签的一些基本信息,而这个options.start方法中的stack存放的都是ast节点即element; - 如果是自闭和标签即
unary是true,则执行closeElement(element),而非自闭和标签在option.end中调用closeElment方法;
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
transforms = pluckModuleFunction(options.modules, 'transformNode')
// 这个就是创建 ast 后 for 循环中药
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const stack = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
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(template, {
// ....
start (tag, attrs, unary, start, end) {
// 获取命名空间,如果父元素有命名空间就继承父元素的命名空间,否则获取平台命名空间
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// 给当前标签创建 AST 节点对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
// 非SSR,html模板中不能有 style、script 标签
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 }
)
}
// 为 element 对象分别执行 class、style、model
// 这个 preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
// options.modules 是 creaetCompiler(baseOptions) 中 baseOptions 中配置的 modules
// 包含三个模块:klass、style、model,
// 但是这这三个模块在 web 下只有 model 存在 'preTransformNode' 方法,所以这里只执行
// model.preTransformNode 方法处理 element 上的 v-model 指令
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
// 表示 element 是否存在 v-pre 指令,
// 存在则设置 element.pre = true
processPre(element)
if (element.pre) {
// 存在 v-pre 指令,则设置 inPre 为 true
inVPre = true
}
}
// 如果当前元素时 <pre></pre> 标签,则设置 inPre 为 true
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 说明标签上存在 v-pre 指令,将节点上 attrList 中的值赋值到到 el.attrs 数组
processRawAttrs(element)
} else if (!element.processed) {
// 处理 v-for 属性,得到 element.for = 要迭代的对象
// element.alias = 别名
processFor(element)
// 处理 v-if v-else-if v-else
// 得到 element.if = 'exp', element.elseif = exp, element.else = true
// v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象
processIf(element)
// 处理 v-once 指令,得到 element.once = true
processOnce(element)
}
// 如果 root 不存在,当前 element 就是第一个元素,即组件的根元素 root
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {}
}
if (!unary) {
// 非自闭和标签,维护 currentParent 记录当前元素,
// 当处理到下一个开始标签的时候,其父元素就是 currentParent
currentParent = element
// 将 element push 到 stack,将来处理到当前元素的闭合标签时出栈
stack.push(element)
} else {
// 说明当前元素为自闭和标签,在当前的的 start 方法中就执行 closeElment 方法
// 而非自闭和标签则在 options.end 中执行 closeElement
closeElement(element)
}
},
})
// 返回生成的 ast 对象
return root
}
三、总结
本篇小作文的主题是讨论 parseHTML 方法执行过程中解析到开始标签后调用 parseHTML 方法接收到的参数options.start 回调方法处理开始标签,其主要工作如下:
- 创建
AST节点element - 调用
options.modules中的preTransformNode方法处理element,options.modules来之createCompiler时传入的baseOptions; - 处理
v-pre指令以及在pre标签内的情景,接着处理v-for、v-for、v-if、v-once; - 维护
root节点,root只在第一次处理时会被赋值,后面处理的所有节点都是root的子节点; - 维护
currrentParent变量,这个意义在于:调用parseHTML的时候是以一种一维的方式解析树形的模板字符串,但是建立AST时却需要还原模板描述的节点间的父子关系,也就是说AST是有深度的。 - 非自闭和元素时维护
element入栈stack,当解析到闭合标签时出栈,如果是自闭合标签执行closeElement自动闭合当前元素,因为它没有闭合标签了,闭合标签的逻辑就要在这儿调用