1、模板DSL的编译器
编译器本质也只是一段程序,把源代码编译为目标代码。
编译的过程包括:词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成。如下图所示
以上的编译过程是所有源代码的基本过程,但每个编程语言具体又有所区别,拥有JS运行时的平台代码,编译过程是这样的
从图中可以看出,Vue.js模板编译器的目标代码其实就是渲染函数
详细过程如下:Vue.js模板编译器会首先对模板进行词法分析和语法分析,得到模板AST(abstract syntax tree抽象语法树)。
再将模板AST转换成JS AST。最后,根据JS AST 生成 JS代码,即渲染函数代码,流程如下图所示
AST实质上是一个具有层级的结构对象,其中主要包括
1.type属性区分节点
2.子节点存储在其children数组中
3.属性节点和指令节点存储在props数组中
4.不同类型的节点会使用不同对象属性进行描述
封装parse函数封装词法分析和语法分析,得到模板AST,如图所示
得到模板AST之后通过transform函数把模板AST转为 JS AST,如下图
再通过generate函数产生模板函数
串起来就是Vue.js模板编译为渲染函数的完整流程
2、parse的实现原理与状态机
parse(解析器)原理:解析器的入参是字符串模板,逐个读取字符串模板中的字符,并根据规则将整个字符串切割为一个个Token(词法记号)
根据规则,规则指的是,有限状态自动机,
有限状态:就是有限个状态
自动机:指随着字符的输入,解析器会自动的在不同状态间迁移
解析器状态机大致流程如上图,简单描述为:
1.初始状态,遇到< 符号进入标签开始状态,接下来遇到的字母,在没遇到 > 符号前都算做开始标签。
2.遇到了 > 符号,开始标签结束,返回到解析器的初始状态。
3.初始状态遇到了字母,接下来的内容是文本状态,再次遇到 < 符号,又进入到标签开始状态。
4.遇到了 / 符号,进入结束标签状态,再遇到字母,进入到结束标签名称状态。
5.最后,遇到了 > 符号,返回解析器的初始状态
这个流程就是解析器状态机的大致流程,其代码如下
//定义状态机的状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始状态
tagName: 3, // 标签名称状态
text: 4, // 文本状态
tagEnd: 5, // 结束标签状态
tagEndName: 6 // 结束标签名称状态
}
// 判断是否是字母
function isAlpha(char) {
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}
// 接收模板字符串作为参数,切割为Token返回
function tokenize(str) {
// 状态机的当前状态:初始状态
let currentState = State.initial
// 缓存字符
const chars = []
// 存储Token, 并作为函数返回值
const tokens = []
// while循环开启自动机,字符串没用完,就一直运行
while(str) {
const char = str[0]
switch (currentState) {
case State.initial:
if (char === '<') {
currentState = State.tagOpen
str = str.slice(1)
} else if (isAlpha(char)) {
currentState = State.text
chars.push(char)
str = str.slice(1)
}
break
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
chars.push(char)
str = str.slice(1)
} else if (char === '/') {
currentState = State.tagEnd
str = str.slice(1)
}
break
case State.tagName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tag',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.text:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '<') {
currentState = State.tagOpen
tokens.push({
type: 'text',
content: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
chars.push(char)
str = str.slice(1)
}
break
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tagEnd',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
}
}
return tokens
}
function parse(str) {
const tokens = tokenize(str)
const root = {
type: 'Root',
children: []
}
const elementStack = [root]
while (tokens.length) {
const parent = elementStack[elementStack.length - 1]
const t = tokens[0]
switch (t.type) {
case 'tag':
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
parent.children.push(elementNode)
elementStack.push(elementNode)
break
case 'text':
const textNode = {
type: 'Text',
content: t.content
}
parent.children.push(textNode)
break
case 'tagEnd':
elementStack.pop()
break
}
tokens.shift()
}
return root
}
const ast = parse(template)
console.log(ast)
3、构造AST
AST的构造方式,对于通用用途语言(GPL)来说,为其构造AST,通常使用递归下降算法
注意:Vue.js模板的DSL不具有运算符,不需要考虑运算符优先级
1.AST的结构
AST在结构上与模板是“同构”,具有树型结构,如下图所示
构建AST,其实就是对Token表进行扫描的过程,顺序扫描整个Token列表,需要维护一个栈elementStack,用于维护元素间的父子关系,遇到一个开始标签,构造Element类型的AST节点,并将其压入栈中,遇到结束节点,就弹出当前栈顶节点,操作如下10张图。
4、AST的转换与插件化架构
1、节点的访问
对AST进行转换,需要可以访问AST的每一个节点,才可以对特定节点进行修改,AST是树型结构,需要一个深度优先的遍历算法。在此之前需要编写一个dump算法打印节点信息
function dump(node, indent = 0) {
const type = node.type
const desc = node.type === 'Root'
? ''
: node.type === 'Element'
? node.tag
: node.content
console.log(`${'-'.repeat(indent)}${type}: ${desc}`)
if (node.children) {
node.children.forEach(n => dump(n, indent + 2))
}
}
2、转换上下文与节点操作
上下文:实质上就是程序在某个范围内的“全局变量”
使用上下文context的好处:所有AST转换函数都可以通过context共享数据,上下文对象会维护程序的当前状态。
转换节点的最终版本
function traverseNode(ast, context) {
context.currentNode = ast
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
transforms[i](context.currentNode, context)
if (!context.currentNode) return
}
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
}
3、进入与退出
在转换AST节点的过程中,通常需要根据子节点的情况来决定如何对当前节点进行转换,要求父节点的转换操作必须等待所有子节点转换完成之后再执行,其顺序执行工作流如图所示
当前的工作流仍然有着弊端,“不能回头”处理父节点,更理想的工作流应该是这样的
function traverseNode(ast, context) {
context.currentNode = ast
const exitFns = []
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
const onExit = transforms[i](context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
if (!context.currentNode) return
}
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
5、将模板AST转为JavaScript AST
使用一个对象来描述一个JavaScript AST节点,每个节点都具有type字段,用id字段来存储函数的名称
封装一些辅助函数
// =============================== AST 工具函数 ===============================
function createStringLiteral(value) {
return {
type: 'StringLiteral',
value
}
}
function createIdentifier(name) {
return {
type: 'Identifier',
name
}
}
function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements
}
}
function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments
}
}
// =============================== AST 工具函数 ===============================
function transformText(node) {
if (node.type !== 'Text') {
return
}
node.jsNode = createStringLiteral(node.content)
}
function transformElement(node) {
return () => {
if (node.type !== 'Element') {
return
}
const callExp = createCallExpression('h', [
createStringLiteral(node.tag)
])
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push(
createArrayExpression(node.children.map(c => c.jsNode))
)
node.jsNode = callExp
}
}
转换Root如下所示
function transformRoot(node) {
return () => {
if (node.type !== 'Root') {
return
}
const vnodeJSAST = node.children[0].jsNode
node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST
}
]
}
}
}
6、代码生成
接下来进行generate操作,与AST一样需要context,需要处理5种节点:
1.函数类型 2.返回类型 3.调用类型
4.字符串类型 5.数组类型
目前尚不完善的编译器完整代码如下
const State = {
initial: 1,
tagOpen: 2,
tagName: 3,
text: 4,
tagEnd: 5,
tagEndName: 6
}
function isAlpha(char) {
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}
function tokenize(str) {
let currentState = State.initial
const chars = []
const tokens = []
while(str) {
const char = str[0]
switch (currentState) {
case State.initial:
if (char === '<') {
currentState = State.tagOpen
str = str.slice(1)
} else if (isAlpha(char)) {
currentState = State.text
chars.push(char)
str = str.slice(1)
}
break
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
chars.push(char)
str = str.slice(1)
} else if (char === '/') {
currentState = State.tagEnd
str = str.slice(1)
}
break
case State.tagName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tag',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.text:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '<') {
currentState = State.tagOpen
tokens.push({
type: 'text',
content: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
chars.push(char)
str = str.slice(1)
}
break
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tagEnd',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
}
}
return tokens
}
function parse(str) {
const tokens = tokenize(str)
const root = {
type: 'Root',
children: []
}
const elementStack = [root]
while (tokens.length) {
const parent = elementStack[elementStack.length - 1]
const t = tokens[0]
switch (t.type) {
case 'tag':
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
parent.children.push(elementNode)
elementStack.push(elementNode)
break
case 'text':
const textNode = {
type: 'Text',
content: t.content
}
parent.children.push(textNode)
break
case 'tagEnd':
elementStack.pop()
break
}
tokens.shift()
}
return root
}
function traverseNode(ast, context) {
context.currentNode = ast
const exitFns = []
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
const onExit = transforms[i](context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
if (!context.currentNode) return
}
const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
function transform(ast) {
const context = {
currentNode: null,
parent: null,
replaceNode(node) {
context.currentNode = node
context.parent.children[context.childIndex] = node
},
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1)
context.currentNode = null
}
},
nodeTransforms: [
transformRoot,
transformElement,
transformText
]
}
// 调用 traverseNode 完成转换
traverseNode(ast, context)
}
// =============================== AST 工具函数 ===============================
function createStringLiteral(value) {
return {
type: 'StringLiteral',
value
}
}
function createIdentifier(name) {
return {
type: 'Identifier',
name
}
}
function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements
}
}
function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments
}
}
// =============================== AST 工具函数 ===============================
function transformText(node) {
if (node.type !== 'Text') {
return
}
node.jsNode = createStringLiteral(node.content)
}
function transformElement(node) {
return () => {
if (node.type !== 'Element') {
return
}
const callExp = createCallExpression('h', [
createStringLiteral(node.tag)
])
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push(
createArrayExpression(node.children.map(c => c.jsNode))
)
node.jsNode = callExp
}
}
function transformRoot(node) {
return () => {
if (node.type !== 'Root') {
return
}
const vnodeJSAST = node.children[0].jsNode
node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST
}
]
}
}
}
const ast = parse(`<div><p>Vue</p><p>Template</p></div>`)
transform(ast)
console.log(ast)
console.log(generate(ast.jsNode))
// ============================ code generate ============================
function generate(node) {
const context = {
code: '',
push(code) {
context.code += code
},
currentIndent: 0,
newline() {
context.code += '\n' + ` `.repeat(context.currentIndent)
},
indent() {
context.currentIndent++
context.newline()
},
deIndent() {
context.currentIndent--
context.newline()
}
}
genNode(node, context)
return context.code
}
function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl':
genFunctionDecl(node, context)
break
case 'ReturnStatement':
genReturnStatement(node, context)
break
case 'CallExpression':
genCallExpression(node, context)
break
case 'StringLiteral':
genStringLiteral(node, context)
break
case 'ArrayExpression':
genArrayExpression(node, context)
break
}
}
function genFunctionDecl(node, context) {
const { push, indent, deIndent } = context
push(`function ${node.id.name} `)
push(`(`)
genNodeList(node.params, context)
push(`) `)
push(`{`)
indent()
node.body.forEach(n => genNode(n, context))
deIndent()
push(`}`)
}
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
genNode(node, context)
if (i < nodes.length - 1) {
push(', ')
}
}
}
function genReturnStatement(node, context) {
const { push } = context
push(`return `)
genNode(node.return, context)
}
function genCallExpression(node, context) {
const { push } = context
const { callee, arguments: args } = node
push(`${callee.name}(`)
genNodeList(args, context)
push(`)`)
}
function genStringLiteral(node, context) {
const { push } = context
push(`'${node.value}'`)
}
function genArrayExpression(node, context) {
const { push } = context
push('[')
genNodeList(node.elements, context)
push(']')
}
总结
1、学习了编译器的工作流程:
分析模板,解析为模板AST
将模板AST解析为描述渲染函数的JavaScript AST
根据JavaScript AST生成渲染函数代码
2、解析器的实现原理,如何实现有限状态自动机,构建树型AST
3、AST是树型结构,需要编写深度优先遍历AST树,设置context转换上下文,上下文对象会维护程序的当前状态。
4、AST转为JavaScript AST,生成代码,为不同类型的节点编写对应的代码生成函数