【Vue2.0源码学习】模板编译篇-模板解析阶段(文本解析器)

简介: 【Vue2.0源码学习】模板编译篇-模板解析阶段(文本解析器)

1. 前言

在上篇文章中我们说了,当HTML解析器解析到文本内容时会调用4个钩子函数中的chars函数来创建文本型的AST节点,并且也说了在chars函数中会根据文本内容是否包含变量再细分为创建含有变量的AST节点和不包含变量的AST节点,如下:

// 当解析到标签的文本时,触发chars
chars (text) {
  if(res = parseText(text)){
       let element = {
           type: 2,
           expression: res.expression,
           tokens: res.tokens,
           text
       }
    } else {
       let element = {
           type: 3,
           text
       }
    }
}

从上面代码中可以看到,创建含有变量的AST节点时节点的type属性为2,并且相较于不包含变量的AST节点多了两个属性:expressiontokens。那么如何来判断文本里面是否包含变量以及多的那两个属性是什么呢?这就涉及到文本解析器了,当VueHTML解析器解析出文本时,再将解析出来的文本内容传给文本解析器,最后由文本解析器解析该段文本里面是否包含变量以及如果包含变量时再解析expressiontokens。那么接下来,本篇文章就来分析一下文本解析器都干了些什么。

2. 结果分析

研究文本解析器内部原理之前,我们先来看一下由HTML解析器解析得到的文本内容经过文本解析器后输出的结果是什么样子的,这样对我们后面分析文本解析器内部原理会有很大的帮助。

从上面chars函数的代码中可以看到,把HTML解析器解析得到的文本内容text传给文本解析器parseText函数,根据parseText函数是否有返回值判断该文本是否包含变量,以及从返回值中取到需要的expressiontokens。那么我们就先来看一下parseText函数如果有返回值,那么它的返回值是什么样子的。

假设现有由HTML解析器解析得到的文本内容如下:

let text = "我叫{{name}},我今年{{age}}岁了"

经过文本解析器解析后得到:

let res = parseText(text)
res = {
    expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
    tokens:[
        "我叫",
        {'@binding': name },
        ",我今年"
        {'@binding': age },
      "岁了"
    ]
}

从上面的结果中我们可以看到,expression属性就是把文本中的变量和非变量提取出来,然后把变量用_s()包裹,最后按照文本里的顺序把它们用+连接起来。而tokens是个数组,数组内容也是文本中的变量和非变量,不一样的是把变量构造成{'@binding': xxx}

那么这样做有什么用呢?这主要是为了给后面代码生成阶段的生成render函数时用的,这个我们在后面介绍代码生成阶段是会详细说明,此处暂可理解为单纯的在构造形式。

OK,现在我们就可以知道文本解析器内部就干了三件事:

  • 判断传入的文本是否包含变量
  • 构造expression
  • 构造tokens

那么接下来我们就通过阅读源码,逐行分析文本解析器内部工作原理。

3. 源码分析

文本解析器的源码位于 src/compiler/parser/text-parsre.js 中,代码如下:

const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const buildRegex = cached(delimiters => {
  const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
export function parseText (text,delimiters) {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  /**
   * let lastIndex = tagRE.lastIndex = 0
   * 上面这行代码等同于下面这两行代码:
   * tagRE.lastIndex = 0
   * let lastIndex = tagRE.lastIndex
   */
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      // 先把'{{'前面的文本放入tokens中
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    // 取出'{{ }}'中间的变量exp
    const exp = parseFilters(match[1].trim())
    // 把变量exp改成_s(exp)形式也放入tokens中
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    // 设置lastIndex 以保证下一轮循环时,只从'}}'后面再开始匹配正则
    lastIndex = index + match[0].length
  }
  // 当剩下的text不再被正则匹配上时,表示所有变量已经处理完毕
  // 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本
  // 最后将后面的文本再加入到tokens中
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  // 最后把数组tokens中的所有元素用'+'拼接起来
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

我们看到,除开我们自己加的注释,代码其实不复杂,我们逐行分析。

parseText函数接收两个参数,一个是传入的待解析的文本内容text,一个包裹变量的符号delimiters。第一个参数好理解,那第二个参数是干什么的呢?别急,我们看函数体内第一行代码:

const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE

函数体内首先定义了变量tagRE,表示一个正则表达式。这个正则表达式是用来检查文本中是否包含变量的。我们知道,通常我们在模板中写变量时是这样写的:hello 。这里用{{}}包裹的内容就是变量。所以我们就知道,tagRE是用来检测文本内是否有{{}}。而tagRE又是可变的,它是根据是否传入了delimiters参数从而又不同的值,也就是说如果没有传入delimiters参数,则是检测文本是否包含{{}},如果传入了值,就会检测文本是否包含传入的值。换句话说在开发Vue项目中,用户可以自定义文本内包含变量所使用的符号,例如你可以使用%包裹变量如:hello %name%。

接下来用tagRE去匹配传入的文本内容,判断是否包含变量,若不包含,则直接返回,如下:

if (!tagRE.test(text)) {
    return
}

如果包含变量,那就继续往下看:

const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
}

接下来会开启一个while循环,循环结束条件是tagRE.exec(text)的结果match是否为nullexec( )方法是在一个字符串中执行匹配检索,如果它没有找到任何匹配就返回null,但如果它找到了一个匹配就返回一个数组。例如:

tagRE.exec("hello {{name}},I am {{age}}")
//返回:["{{name}}", "name", index: 6, input: "hello {{name}},I am {{age}}", groups: undefined]
tagRE.exec("hello")
//返回:null

可以看到,当匹配上时,匹配结果的第一个元素是字符串中第一个完整的带有包裹的变量,第二个元素是第一个被包裹的变量名,第三个元素是第一个变量在字符串中的起始位置。

接着往下看循环体内:

while ((match = tagRE.exec(text))) {
    index = match.index
    if (index > lastIndex) {
      // 先把'{{'前面的文本放入tokens中
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    // 取出'{{ }}'中间的变量exp
    const exp = match[1].trim()
    // 把变量exp改成_s(exp)形式也放入tokens中
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    // 设置lastIndex 以保证下一轮循环时,只从'}}'后面再开始匹配正则
    lastIndex = index + match[0].length
  }

上面代码中,首先取得字符串中第一个变量在字符串中的起始位置赋给index,然后比较indexlastIndex的大小,此时你可能有疑问了,这个lastIndex是什么呢?在上面定义变量中,定义了let lastIndex = tagRE.lastIndex = 0,所以lastIndex就是tagRE.lastIndex,而tagRE.lastIndex又是什么呢?当调用exec( )的正则表达式对象具有修饰符g时,它将把当前正则表达式对象的lastIndex属性设置为紧挨着匹配子串的字符位置,当同一个正则表达式第二次调用exec( ),它会将从lastIndex属性所指示的字符串处开始检索,如果exec( )没有发现任何匹配结果,它会将lastIndex重置为0。示例如下:

const tagRE = /\{\{((?:.|\n)+?)\}\}/g
tagRE.exec("hello {{name}},I am {{age}}")
tagRE.lastIndex   // 14

从示例中可以看到,tagRE.lastIndex就是第一个包裹变量最后一个}所在字符串中的位置。lastIndex初始值为0。

那么接下里就好理解了,当index>lastIndex时,表示变量前面有纯文本,那么就把这段纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中,如下:

if (index > lastIndex) {
    // 先把'{{'前面的文本放入tokens中
    rawTokens.push(tokenValue = text.slice(lastIndex, index))
    tokens.push(JSON.stringify(tokenValue))
}

如果index不大于lastIndex,那说明index也为0,即该文本一开始就是变量,例如:hello。那么此时变量前面没有纯文本,那就不用截取,直接取出匹配结果的第一个元素变量名,将其用_s()包裹存入tokens中,同时再把变量名构造成{'@binding': exp}存入rawTokens中,如下:

// 取出'{{ }}'中间的变量exp
const exp = match[1].trim()
// 把变量exp改成_s(exp)形式也放入tokens中
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })

接着,更新lastIndex以保证下一轮循环时,只从}}后面再开始匹配正则,如下:

lastIndex = index + match[0].length

接着,当while循环完毕时,表明文本中所有变量已经被解析完毕,如果此时lastIndex < text.length,那就说明最后一个变量的后面还有纯文本,那就将其再存入tokensrawTokens中,如下:

// 当剩下的text不再被正则匹配上时,表示所有变量已经处理完毕
// 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本
// 最后将后面的文本再加入到tokens中
if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
}

最后,把tokens数组里的元素用+连接,和rawTokens一并返回,如下:

return {
    expression: tokens.join('+'),
    tokens: rawTokens
}

以上就是文本解析器parseText函数的所有逻辑了。

4. 总结

本篇文章介绍了文本解析器的内部工作原理,文本解析器的作用就是将HTML解析器解析得到的文本内容进行二次解析,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工,为后续生产render函数做准备。

目录
相关文章
|
2天前
|
算法
二分查找及模板深度解析:right <= left 还是 right < left ? mid=left+(right-left)/2还是mid=left+(right-left +1 )/2 ?
二分查找及模板深度解析:right <= left 还是 right < left ? mid=left+(right-left)/2还是mid=left+(right-left +1 )/2 ?
29 0
|
2天前
|
Linux 程序员 计算机视觉
【linux 学习】在Linux中经常用到的cmake、make、make install等命令解析
【linux 学习】在Linux中经常用到的cmake、make、make install等命令解析
16 0
|
2天前
|
SQL 缓存 JavaScript
深入解析JavaScript中的模板字符串
深入解析JavaScript中的模板字符串
14 1
|
2天前
|
C++
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
|
2天前
项目管理工具计划模板解析:项目管理工具的双重功能与创建方法
本文介绍了项目计划模板的含义和重要性。项目计划模板是用于规划项目结构的可编辑文档,帮助团队明确任务、分配责任和管理时间。模板有助于跟踪项目进度、避免任务冲突,并简化会议安排。创建模板通常涉及选择合适的项目管理工具,如Zoho Projects或Microsoft Excel,然后分解任务、定义日期并持续调整。在Zoho Projects中,用户可以按步骤创建模板,包括命名、添加任务和设置相关细节。
21 0
|
2天前
|
JavaScript 前端开发 算法
vue生命周期函数原理解析,vue阻止事件冒泡方法实现
vue生命周期函数原理解析,vue阻止事件冒泡方法实现
|
2天前
|
JavaScript 前端开发 开发者
深入比较Input、Change和Blur事件:Vue与React中的行为差异解析
深入比较Input、Change和Blur事件:Vue与React中的行为差异解析
|
2天前
|
设计模式 JavaScript 开发者
Vue的混入(Mixins):混入的使用和设计模式解析
【4月更文挑战第24天】Vue Mixins是实现组件复用的灵活工具,允许共享可复用功能。混入对象包含组件选项,如数据、方法和生命周期钩子,可被合并到使用它的组件中。通过组合模式和钩子注入模式,混入能提高代码复用和可维护性。然而,注意命名冲突、选项合并策略以及慎用全局混入以防止副作用。正确使用混入能提升开发效率和软件质量。
|
2天前
|
JavaScript 前端开发 开发者
理解Vue实例:生命周期钩子全面解析
【4月更文挑战第22天】Vue.js 框架的核心概念之一是 Vue 实例及其生命周期钩子,包括创建、挂载、更新和销毁四个阶段。这些钩子函数如 `beforeCreate`、`created`、`mounted`、`updated`、`beforeDestroy` 和 `destroyed`,在实例不同阶段调用,使开发者能精确控制应用状态。了解其执行顺序和应用场景(如初始化、DOM 操作、数据更新和资源清理)对于编写高效 Vue 代码至关重要。
|
2天前
|
机器学习/深度学习
yolov7论文学习——创新点解析、网络结构图
yolov7论文学习——创新点解析、网络结构图

推荐镜像

更多