概览
Vue.js 作为目前最风行的前端框架之一,一些概念和原理还是须要咱们前端开发人员理解与深刻了解的。
Vue.js 波及的知识点很多,一些重要概念,例如:如何应用 proxy
实现响应式 effect
,虚构DOM
的Diff
算法及演变过程(双端 Diff 算法、疾速 Diff 算法等),渲染器原理的实现,编译器、解析器的工作原理,动静节点、动态晋升等等;
当初重点采纳图解步骤剖析一下编译器的简略工作原理;
编译器概念
编译器其实就是一段 JavaScript
代码程序,它将一种语言(A
)编译成另外一种语言 (B
),其中前者A
通常被叫做源代码,后者 B
通常被叫做为指标代码。例如咱们 vue 的前端我的项目的 .vue
文件个别即为源代码,而编译后 dist
文件里的 .js
文件即为指标代码;这个过程就被称为编译(compile
)
要害概念
次要波及的概念:
DSL
畛域特定语言AST
形象语法树(Abstract Syntax Tree)- 无限状态机
- 深度优先算法
简略流程
一个规范的编译器流程如下图所示:Vue.js
作为 DSL
,其编译流程会与上图有所不同,对于Vue.js
来说,源代码就是组件的模板代码,而指标代码就是可能在浏览器(或其余平台)平台上运行的 JavaScript
代码。
Vue 的编译器
Vue.js
的指标代码其实就是渲染函数(render
函数)。详情而言,Vue.js
编译器首先对模板进行词法剖析、语法分析,而后失去模板的形象语法树(AST
)。随后将模板 AST
转换成 JavaScript
AST,最初再转换成JavaScript
代码,及渲染函数。一个简略的 Vue.js 模板编译器的工作流如下:
简略如下:
模板代码
<div>
<h1 id="vue">vue_compiler</h1>
</div>
指标的 AST
const ast = {
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
children: [
{
type:'Element',
tag: 'h1',
props: [
{
type: 'Attribute',
name: 'id',
content: 'vue'
}
],
children: [
{
type: 'Text',
content: 'vue_compiler'
}
]
}
]
}
]
}
指标代码
function render() {
return h('div', [h('h1', {id: 'vue'}, 'vue_compiler')
])
}
由以上代码能够看出,AST
其实就是一个具备层级构造的对象,模板的 AST
与模板具备雷同的嵌套构造。每一颗 AST
都有一个逻辑上的根节点,其类型为 Root
,而模板中真正的根节点则作为Root
节点的 children
存在。
察看 AST
可知:
- 不同类型的节点是通过节点的
type
属性进行辨别的。 - 标签节点的子节点存储在其
children
数组中。 - 标签节点的属性节点会存储在
props
数组中。 - 不同类型的节点会应用不同的对象属性进行形容。
编译过程
parse 函数
Vue.js
通过封装 parse
函数,实现对模板的词法剖析和语法分析,最终失去模板的 AST。parse
函数接管模板字符串作为参数,并将解析后的 AST
作为返回值返回;
const template = `
<div>
<h1>vue<h1>
</div>
`
const templateAst = parse(template)
解析器是如何对模板字符串进行宰割的呢,此处就须要用到无限状态自动机。指的是在无限个状态之间,随着字符的输出,解析器会主动地在不同的状态之间进行切换。(实际上无限状态机是能够应用正则表达式来实现的)。
简略的状态机流程图:
通过无限状态机原理,能够帮忙咱们实现对模板的标记,最终将失去一系列Token
(词法标记号)。
假如有如下代码:
const template = `<div><span>Vue</span><p>Vue Compiler</p></div>` // 模板字符串
// 通过无限状态机原理实现词法合成失去三个 Token
// 开始标签 <div>
// 文本节点 vue
// 完结标签 </div>
// 最终值为
const tokens = tokenize(template);
// [
// {
// type: 'tag', name: 'div'
// },
// {
// type: 'tag', name: 'span'
// },
// {
// type: 'text', name: 'Vue'
// },
// {
// type: 'tagEnd', name: 'span'
// },
// {
// type: 'tag', name: 'p'
// },
// {
// type: 'text', name: 'Vue Compiler'
// },
// {
// type: 'tagEnd', name: 'p'
// },
// {
// type: 'tagEnd', name: 'div'
// }
// ]
// 此代码须要生成的 AST 应为
const ast = {
type: 'Root',
children: [
{
// 理论的根节点
type: 'Element',
tag:: 'div',
children: [
{
type: 'Element',
tag:: 'span',
children: [
{
type: 'Text',
content: 'Vue'
}
]
},
{
type: 'Element',
tag:: 'p',
children: [
{
type: 'Text',
content: 'Vue Compiler'
}
]
}
]
}
]
}
以上代码生成的 AST 数据结构 HTML 构造雷同,都是树状构造
接下来要做的就是将生成的 tokens
转换成 AST
,在转换过程中须要保护一个Stack
, 这个栈将用来保护元素间的父子关系。每到遇到一个开始标签,就创立一个Element
类型的 AST 节点,并将其压入栈内,相似的,每当遇到一个完结标签节点,咱们就将以后栈顶的节点弹出。这样栈顶的节点将始终充当父节点的角色。转换过程中的所有节点,都将作为以后栈顶节点的子节点,并增加到栈顶节点的 children
属性下。流程如下图示:
最后节点只有根节点 Root
当扫描到第一个标签是开始节点时,因而咱们创立一个类型为 Element
的 AST 节点 Element(div)
,并将该节点作为以后节点的子节点。因为以后的栈顶节点是Root
节点,所以新创建的 Element(div)
节点作为 Root
节点的子节点被增加到 AST
中,最初将新建的 Element(div)
节点压入栈中。
因为第二个节点也是一个开始标签,所以流程同上一步,只不过以后的栈顶节点为 Element(div)
,所以将以后的节点Element(span)
作为其子节点增加到 AST
中,最初将 Element(div)
节点压入栈中。
接下来的节点是一个文本节点,所以须要创立一个 Text
类型的 AST
节点,并将其作为栈顶节点 Element(span)
的子节点退出到 AST 中,不同的时,以后接待不是 Element
类型,所以不须要压入栈中;
上面是一个完结标签节点,依据规定,则须要将以后栈顶的节点弹出。
前面的流程此处就不再累述
最终实现后的成果如下:
当初咱们来实现 parse 函数
function parse(str) {
// 对模板进行词法剖析,失去节点 list
const tokens = okenize(template);
// 创立跟节点
const root = {
type: 'Root',
children: []};
// 创立节点栈,root 节点作为栈的根节点
const stack = [root];
while(tokens.length) {const parent = stack[stack.length - 1];
const token = tokens[0] // 从第一个点开始
switch(t.type) {
case 'tag':
const eleNode = {
type: 'Element',
tag: t.name,
children: []}
parent.children.push(eleNode);
stack.push(eleNode);
break;
case 'text':
const textNode = {
type: 'Text',
content: t.content
}
parent.children.push(textNode);
break;
case 'tagEnd':
// 完结标签,将栈顶节点弹出栈
stack.pop();
break;
}
// 生产掉已解决的节点
tokens.shift()}
return root
}
以上就是一个简版的 parse 函数的实现,当然绝对于 Vue.js 的源码还有很多差别,但基本原理大致相同。
上面对于 transform
函数和 generate
函数仅做了简要阐明,具体实现原理敬请期待;
transform 函数
const template = `
<div>
<h1>vue<h1>
</div>
`
const templateAst = parse(template)
const jsAst = transform(templateAst)
generate 函数
const template = `
<div>
<h1>vue<h1>
</div>
`
const templateAst = parse(template)
const jsAst = transform(templateAst)
const code = generate(jsAst)
残缺流程
以上就是 Vue
模板编译器的根本构造和工作流程,它次要有三个局部组成:
- 用来将模板字符串解析为模板
AST
的解析器(parser
); - 用来将模板
AST
解析成JavaScript AST
的转换器(transformer
); - 用来依据
JavaScript AST
生成渲染函数代码的生成器(generator
);
本文章次要探讨了 parser 的根本实现原理(实际上 Vue.js 的真正实现要简单的多,比方正则解析、Vue 语法解析 v -if、v-show、内插值 {{}} 等等),以及如何应用无限状态自动机来结构一个词法分析器,其过程就是状态机在不同的状态之间进行迁徙的过程,并生成一个 Token 列表汇合。而后应用 Token 列表汇合和顶节点元素栈来结构一个能够用来形容模板的 AST,最初应用模板 AST 来解析成 JavaScript AST 和渲染函数。
作者:GFE- 绝对零度
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。
参考
Vue.js 源码;
Vue.js 设计与实现;