[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST

本文主要是介绍[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。

  • 一起来看Vue双向绑定原理-数据劫持和发布订阅
  • 一起来看Vue模板编译原理(一)-Template生成AST
  • 一起来看Vue模板编译原理(二)-AST生成Render字符串
  • 一起来看Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法

这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。

编译过程

模板编译是Vue中比较核心的一部分。关于Vue编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

第一步:将模板字符串转换成element ASTs(解析器)

第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)

第三步:使用element ASTs生成render函数代码字符串(代码生成器)

对应的Vue源码如下,源码位置在src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (template: string,options: CompilerOptions
): CompiledResult {// 1.parse,模板字符串 转换成 抽象语法树(AST)const ast = parse(template.trim(), options)// 2.optimize,对 AST 进行静态节点标记if (options.optimize !== false) {optimize(ast, options)}// 3.generate,抽象语法树(AST) 生成 render函数代码字符串const code = generate(ast, options)return {ast,render: code.render,staticRenderFns: code.staticRenderFns}
})

这篇文档主要讲第一步将模板字符串转换成对象语法树(element ASTs),对应的源码实现我们通常称之为解析器。

解析器运行过程

在分析解析器的原理前,我们先举例看下解析器的具体作用。

来一个最简单的实例:

<div><p>{{name}}</p>
</div>

上面的代码是一个比较简单的模板,它转换成AST后的样子如下:

{tag: "div"type: 1,staticRoot: false,static: false,plain: true,parent: undefined,attrsList: [],attrsMap: {},children: [{tag: "p"type: 1,staticRoot: false,static: false,plain: true,parent: {tag: "div", ...},attrsList: [],attrsMap: {},children: [{type: 2,text: "{{name}}",static: false,expression: "_s(name)"}]}]
}

其实AST并不是什么很神奇的东西,不要被它的名字吓倒。它只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。

事实上,解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器,其中最主要的是HTML解析器。顾名思义,HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。

我们先看下解析器整体的代码结构,源码位置src/compiler/parser/index.js

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) {//...}
})

实际上,模板解析的过程就是不断调用钩子函数的处理过程。整个过程,读取template字符串,使用不同的正则表达式,匹配到不同的内容,然后触发对应不同的钩子函数处理匹配到的截取片段,比如开始标签正则匹配到开始标签,触发start钩子函数,钩子函数处理匹配到的开始标签片段,生成一个标签节点添加到抽象语法树上。

还举上面那个例子来说:

<div><p>{{name}}</p>
</div>

整个解析运行过程就是:解析到

时,会触发一个标签开始的钩子函数start,处理匹配片段,生成一个标签节点添加到AST上;然后解析到

时,又触发一次钩子函数start,处理匹配片段,又生成一个标签节点并作为上一个节点的子节点添加到AST上;接着解析到{{name}}这行文本,此时触发了文本钩子函数chars,处理匹配片段,生成一个带变量文本(变量文本下面会讲到)标签节点并作为上一个节点的子节点添加到AST上;然后解析到

,触发了标签结束的钩子函数end;接着继续解析到
,此时又触发一次标签结束的钩子函数end,解析结束。

正则匹配

模板解析过程会涉及到许许多多的正则匹配,知道每个正则有什么用途,会更加方便之后的分析。

那我们先来看看这些正则表达式,源码位置在src/compiler/parser/index.js

export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\.|^#/: /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/gconst slotRE = /^v-slot(:|$)|^#/const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/gconst invalidAttributeRE = /[\s"'<>\/=]/

上面这些正则相对来说比较简单,基本上都是用来匹配Vue中自定义的一些语法格式,如onRE匹配 @ 或 v-on 开头的属性,forAliasRE匹配v-for中的属性值,比如item in items、(item, index) of items。

下面这些就是专门针对html的一些正则匹配,源码位置在src/compiler/parser/html-parser.js

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

这些正则表达式相对来说就复杂一些,如attribute用来匹配标签的属性,startTagOpen、startTagClose用于匹配标签的开始、结束部分等。这些正则表达式的写法就不多说了,有兴趣的朋友可以针对这些正则一个一个的去测试一下。

HTML解析器

这里我们来看看HTMl解析器。

事实上,解析HTML模板的过程就是循环的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML模板中截取一小段字符串,然后重复以上过程,直到HTML模板被截成一个空字符串时结束循环,解析完毕。

我们通过源码,就可以看到整个函数逻辑就是被一个while循环包裹着。源码位置在:src/compiler/parser/html-parser.js

export function parseHTML (html, options) {const stack = []const expectHTML = options.expectHTMLconst isUnaryTag = options.isUnaryTag || noconst canBeLeftOpenTag = options.canBeLeftOpenTag || nolet index = 0let last, lastTagwhile (html) {//...}parseEndTag()//...
}

下面我用一个简单的模板,模拟一下HTML解析的过程,以便于更好的理解。

<div><p>{{text}}</p>
</div>

最初的HTML模板:

<div><p>{{text}}</p>
</div>

第一轮循环时,截取出一段字符串

,解析出是div开始标签并且触发钩子函数start,截取后的结果为:

<p>{{text}}</p>
</div>

第二轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

  <p>{{text}}</p>
</div>

第三轮循环时,截取出一段字符串

,解析出是p开始标签并且触发钩子函数start,截取后的结果为:

  {{text}}</p>
</div>

第四轮循环时,截取出一段字符串{{name}},解析出是变量字符串并且触发钩子函数chars,截取后的结果为:

  </p>
</div>

第五轮循环时,截取出一段字符串

,解析出是p闭合标签并且触发钩子函数end,截取后的结果为:

</div>

第六轮循环时,截取出一段换行空字符串,会触发钩子函数chars,截取后的结果为:

</div>

第七轮循环时,截取出一段字符串,解析出是div闭合标签并且触发钩子函数end,截取后的结果为:

第八轮循环时,发现只有一个空字符串,解析完毕,循环结束。

现在,是不是就对HTML解析过程很清楚了。其实循环过程对每次匹配到的片段进行分析记录还是很复杂的,因为被截取的片段分很多种类型,比如:

开始标签,例如<div>

结束标签,例如</div>

HTML注释,例如<!-- 注释 -->

DOCTYPE,例如<!DOCTYPE html>

条件注释,例如<!--[if !IE]>-->注释<!--<![endif]-->

文本,例如’字符串’

对每个片段的具体处理这里就不说了,有兴趣的直接看源码去。

文本解析器

文本解析器是对HTML解析器解析出来的文本进行二次加工。文本其实分两种类型,一种是纯文本,另一种是带变量的文本。如下:

这种就是纯文本:

这里有段文本

这种就是带变量的文本:

文本内容:{{text}}

上面HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。

我们知道,HTML解析器在碰到文本时,会触发chars钩子函数,我们先来看看钩子函数里面是怎么区分普通文本和变量文本的。

源码位置在:src/compiler/parser/html-parser.js

chars (text: string, start: number, end: number) {//...let child: ?ASTNodeif (!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}}//...children.push(child)
}

我们重点看res = parseText(text,delimiters)这一行,通过条件判断设置不同的类型。事实上type=2表示表达式类型,type=3表示普通文本类型。

我们再来看看parseText函数具体做了什么

export function parseText (text: string,delimiters?: [string, string]
): TextParseResult | void {const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE// 匹配不到带变量时直接返回了if (!tagRE.test(text)) {return}const tokens = []const rawTokens = []let lastIndex = tagRE.lastIndex = 0let match, index, tokenValue// 对匹配到的变量循环处理成表达式while ((match = tagRE.exec(text))) {index = match.index// push text token// 先把 { { 前边的文本添加到tokens中if (index > lastIndex) {rawTokens.push(tokenValue = text.slice(lastIndex, index))tokens.push(JSON.stringify(tokenValue))}// tag tokenconst exp = parseFilters(match[1].trim())// 使用_s对变量进行包装// 把变量改成`_s(x)`这样的形式也添加到数组中tokens.push(`_s(${exp})`)rawTokens.push({ '@binding': exp })// 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本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}
}

实际上这个函数就是处理带变量的文本,首先如果是纯文本,直接return。如果是带变量的文本,使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。

那么对于上面示例处理结果如下:

parseText('这里有段文本')
// undefinedparseText('文本内容:{{text}}')
// '"文本内容:" + _s(text)'

好了,对于文本解析器就这么多内容。

总结一下

模板解析是Vue模板编译的第一步,即通过模板得到AST(抽象语法树)。

生成AST的过程核心就是借助HTML解析器,当HTML解析器通过正则匹配到不同的片段时会触发对应不同的钩子函数,通过钩子函数对匹配片段进行解析我们可以构建出不同的节点。

文本解析器是对HTML解析器解析出来的文本进行二次加工,主要是为了处理带变量的文本。

相关

  • https://juejin.im/post/5ca44160518825440a4b9fab
  • https://segmentfault.com/a/1190000012922342
  • https://www.jianshu.com/p/743166a8968c
  • https://segmentfault.com/a/1190000013763590
  • https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90ast.md

这篇关于[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/718234

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码