解析模板中的文本——vue2源码探究(5)

170 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

上一篇文章提到,html-parser.ts文件在通过正则取出文本后,将其交给外部的char方法进行处理:

// 源码文件:src\compiler\parser\index.ts
let res
let child: ASTNode | undefined
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  child = {
    type: 2,
    expression: res.expression,
    tokens: res.tokens,
    text
  }
} else if (
  text !== ' ' ||
  !children.length ||
  children[children.length - 1].text !== ' '
) {
  child = {
    type: 3,
    text
  }
}
if (child) {
  if (__DEV__ && options.outputSourceRange) {
    child.start = start
    child.end = end
  }
  children.push(child)
}

可以看到,这里对传出来的文本使用了parseText方法进行处理,如得到返回值说明文本中存在动态文本,如果返回值不存在则说明是静态文本。而我们可以看到,两种文本生成的节点的不同主要体现在expressiontokens这两个属性上,也就是说,包含动态文本才会具有这两个属性。

文本解析器text-paser

那么parseText做了什么呢,我们就要去源码的text-parser.ts文件中看一下:

// 源码文件 src\compiler\parser\text-parser.ts
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
export function parseText(
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  //@ts-expect-error
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens: string[] = []
  const rawTokens: any[] = []
  let lastIndex = (tagRE.lastIndex = 0)
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      rawTokens.push((tokenValue = text.slice(lastIndex, index)))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push((tokenValue = text.slice(lastIndex)))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

首先,定义一个tagRE正则表达式对动态文本进行判断,如果取不到就说明是静态文本,直接返回空给外层处理即可。这个tagRE是可以自定义的,解析时传入一对字符串作为delimiters即可,而默认情况下则是我们在模板里常用的双大括号{{}}

之后,则是利用正则对象Regexmatch方法对传入的文本进行遍历,match方法会对符合条件的字符串进行遍历,到结尾后返回null,然后再重新开始遍历:

var text = "this is a {{dynamic}} sentence with a lot of {{dynamics}}."
var reg = /\{\{((?:.|\r?\n)+?)\}\}/g
reg.exec(text)
// ['{{dynamic}}', 'dynamic', index: 10, input: 'this is a {{dynamic}} sentence with a lot of {{dynamics}}.', groups: undefined]
reg.exec(text)
// ['{{dynamics}}', 'dynamics', index: 45, input: 'this is a {{dynamic}} sentence with a lot of {{dynamics}}.', groups: undefined]
reg.exec(text)
// null
reg.exec(text)
// ['{{dynamic}}', 'dynamic', index: 10, input: 'this is a {{dynamic}} sentence with a lot of {{dynamics}}.', groups: undefined]

因此每次取出正则匹配到的动态文本进行处理(中间有一步过滤器处理parseFilters,回头细说):

// 源码文件 src\compiler\parser\text-parser.ts
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length

每次查找都会存储查找到最后一位的索引,如果下一次查找到动态文本的索引它大,说明中间存在静态文本,进行处理:

// 源码文件 src\compiler\parser\text-parser.ts
index = match.index
// push text token
if (index > lastIndex) {
  rawTokens.push((tokenValue = text.slice(lastIndex, index)))
  tokens.push(JSON.stringify(tokenValue))
}

最后将rowTokens中的表达式用加号进行连接,最后将文本转化成了这样的结构。

{
    expression: "this is a "+_s(dynamic)+"sentence with a lot of "+_s(dynamics),
    tokens:[
        "this is a ",
        {'@binding': dynamic},
        "sentence with a lot of ",
        {'@binding': dynamics},
    ]
}

而这个结构将在最后render函数的生成中派上用场。

总结

利用两篇文章的篇幅,我们了解了vue对模板的解析,分别对标签、注释、文本等模板内容进行了不同方式的解析,使之生成了AST抽象代码树,下一步我们将对其进行优化(实质上就是对静态节点打标记),以及在最后的阶段生成render函数以获得虚拟DOM。