「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文讲述 parseHTML 过程中用到的工具方法的具体逻辑;
advance: 维护index和html,index记录在原html中的处理位置,html逐渐缩短成没处理过的部分;parseStartTag:用正则匹配出开始标签中的tagName和attrs以及 开始标签的结束部分如>或者/>,并且入栈stack当前开始标签的信息;handleStartTag:进一步处理parsetStartTag得到的match对象,转换attrs数组;parseEndTag:当匹配到结束标签时,维护stack,使stack对应当前结束标签的开始标签出栈;
前面一再强调的是 parseHTML 会在 while(html) 的循环中以 < 作为标志将模板分为注释、条件注释、文档声明、普通文本、开始标签、结束标签,然后调用 options.comment、options.chars、options.start、options.end 方法分别处理对应的内容类型,将其转化为固定类型的 AST 节点。
本篇小作文将围绕 options 上传入的方法展开,之前一直没有讲这部分,是因为看源码的时候各种方法的来回调用,加上 js 更是回调方法的行家里手,就更让人摸不到头脑。正式是因为 opitons 是在 parse 方法内调用 parseHTML 时传入的回调方法,让人更加蒙圈。
虽然我主张看源码时按照代码的执行顺序串行组织小作文,但是也要分场景,这里就明显不适用这个办法。即便如此,我还会以老方法 —— 先回顾调用及传参,然后讲方法位置、参数、作用。
二、options.comment
方法位置:parse 方法内调用 parseHTML 时传入的回调方法,如下:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ....
parseHTML(template, {
warn,
// ....
outputSourceRange: options.outputSourceRange,
comment (text: string, start, end) {
}
})
// 返回生成的 ast 对象
return root
}
方法参数:
text,注释文本start:起始索引位置end:结束索引位置
方法作用:
- 根据
currentParent存在与否决定当前的注释节点是否为与根节点同级,与根节点同级的注释会被忽略,currentParent不存在说明与根节点 root 平级; currentParent存在时,创建AST节点,将其push到currentParent中的children中;
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ....
parseHTML(template, {
warn,
// ....
outputSourceRange: options.outputSourceRange,
comment (text: string, start, end) {
// 禁止添加任何节点作为 root 的兄弟节点,注释虽然被允许,但是创建 ast 的时候会被忽略
if (currentParent) {
// 这就是注释的 ast 节点,isComment: true,注释内容就是 text
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 非生产环境时 ast 节点的开始索引和结束索引
child.start = start
child.end = end
}
// 将当前注释节点放到节点的 children 中
currentParent.children.push(child)
}
}
})
// 返回生成的 ast 对象
return root
}
三、options.chars
方法位置:parse 方法内调用 parseHTML 时传入的回调方法,如下:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// ....
parseHTML(template, {
// ...
shouldKeepComment: options.comments,
chars (text: string, start: number, end: number) {
}
})
// 返回生成的 ast 对象
return root
}
方法参数:
text,文本stringstart,起始索引位置end,结束索引位置
方法作用:
- 如果当前文本没有
currentParent,说明文本没有父元素,忽略并提示在root元素以外的文字 - 接下来分情况处理
text情况:是否在pre标签内,是否压缩换行等操作 - 经过第二步后
text不为空,不再pre中,压缩连续空格,然后创建文本的ast节点对象,如果文本中有Vue的模板绑定语法{{xxx}}这种,ast的type为2,普通的文本的ast对象的type为3 - 将上面生成的文本
astpush到children
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
parseHTML(template, {
//
chars (text: string, start: number, end: number) {
// currentParent 不存在,说明这段文本没有父元素,是在 root 元素以外的元素
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
// 提示 root 元素外的文本
}
return
}
// IE textarea placeholder bug
// 获取当前父元素的所有孩子节点数组
const children = currentParent.children
// 对 text 进行处理,
// 删除空白字符或者者存在 whitespaceOptions 选项,则 text 赋值为空字符串或者空格
if (inPre || text.trim()) {
// 文本在 pre 标签内或者 trim 后不为空
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// 代码如果这行到这里,
// 说明 text 不在 pre 标签中 text.trim() 为空,且当前父元素没有孩子节点
// 则将 text 值为空
text = ''
} else if (whitespaceOption) {
// 压缩
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
// 在压缩模式下,移除包含换行符的空白节点,否则压缩成一个空格
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' ' // 空格
}
} else {
text = preserveWhitespace ? ' ' : ''
}
// 经历前面处理 text 不为空
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// 不在 pre 标签中且配置项中存 condense 配置,将多个连续空格压缩为单个
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
// 基于 text 生成 ast 对象
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
// 文本中存在表达式,即有 {{}} 这种 Vue 的数据绑定语法
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
// 纯文本节点
child = {
type: 3,
text
}
}
// child 创建成功,将 child 放到父元素的子元素们即 children 中,
// 即 push 进 currentParent.children 数组
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
// 返回生成的 ast 对象
return root
}
四、总结
本篇小作文讨论了 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 方法,这两个方法作为重中之重,所以将作为单独的篇幅出现。