前言
今天来了解一下编译器
编译器
编译器其实就是一段程序,将自己写的源代码转化成计目标代码可以被计算机执行。Vue.js模板编译器会首先对模板进行进行词法分析和语法分析,得到模板AST。接着将模板AST转换成JavaScript AST。最后,根据JavaSript AST 生成JavaScript代码,即渲染函数代码。
js
// 编译前
<div>
<h1 :id="dynamicId">Vue Template</h1>
</div>
// 编译后
function render() {
return h('div', [ h('h1', { id: dynamicId}, 'Vue Template')])
}
词法分析,解析token
通过有限状态机,我们可以将模板解析成一个个Token,进而可以用它们构建一颗AST。
标签转成token列表
js
tokenzie('<div><p>123</p><p>456</p></div>')
// 解析后的token列表
[
{
"type": "tag",
"name": "div"
},
{
"type": "tag",
"name": "p"
},
{
"type": "text",
"content": "123"
},
{
"type": "tagEnd",
"name": "p"
},
{
"type": "tag",
"name": "p"
},
{
"type": "text",
"content": "456"
},
{
"type": "tagEnd",
"name": "p"
},
{
"type": "tagEnd",
"name": "div"
}
]
方法实现
js
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始
tagName: 3, // 标签名开始
text: 4, // 文本状态
tagEnd: 5, // 结束标签
tagEndName: 6 // 结束标签名
}
function isAlpha(char) {
return /\w/.test(char)
}
// 解析token
function tokenzie(str) {
let currentState = State.initial; // 初始状态
const chars = [] // 用于缓存字符
const tokens = []
// 添加token
const addToken = (type) => {
tokens.push({
type,
[type === 'text' ? 'content' : 'name']: chars.join('')
})
chars.length = 0;
}
while (str) {
const char = str[0];
str = str.slice(1) // 消费字符串
if (isAlpha(char)) {
chars.push(char) // 缓存文本
}
switch (currentState) {
case State.initial:
if (char === '<') {
// 切换到标签开始
currentState = State.tagOpen
} else if (isAlpha(char)) {
// 切换到文本状态
currentState = State.text
}
break
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
} else if (char === '/') {
currentState = State.tagEnd
}
break
case State.tagName:
if (char === '>') {
currentState = State.initial
addToken('tag')
}
break
case State.text:
if (char === '<') {
currentState = State.tagOpen
addToken('text')
}
break
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
}
break
case State.tagEndName:
if (char === '>') {
currentState = State.initial
addToken('tagEnd')
}
break
}
}
return tokens
}
语法分析,构建AST树
扫描token列表,利用栈维护父子关系,构建ast
方法执行
js
parse('<div><p><span>123</span></p>')
// 转换后的结构
{
"type": "Root",
"children": [
{
"type": "Element",
"tag": "div",
"children": [
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "123"
}
]
},
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "456"
}
]
}
]
}
]
}
方法实现
js
function parse(str) {
const tokens = tokenzie(str);
// 创建根节点
const root = {
type: 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':
parent.children.push({
type: 'Text',
content: t.content
})
break
case 'tagEnd':
elementStack.pop()
break
}
tokens.shift()
}
return root
}
将AST转成JavaScript AST
转换后的json
js
const res = parse('<div><p>123</p><p>456</p></div>')
transform(res)
// res.jsNode的值,描述function render () { return h('div', [h('p', '123'), h('p', '456')]) }
{
"type": "FunctionDecl",
"id": {
"type": "Identifier",
"name": "render"
},
"params": [],
"body": [
{
"type": "ReturnStatement",
"return": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "div"
},
{
"type": "ArrayExpression",
"elements": [
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "p"
},
{
"type": "StringLiteral",
"value": "123"
}
]
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "p"
},
{
"type": "StringLiteral",
"value": "456"
}
]
}
]
}
]
}
}
]
}
transform方法
js
// 转换
function transform(ast) {
const context = {
currentNode: null,
parent: null,
childrenIndex: -1,
nodeTransforms: [transformText, transformElement, transformRoot] // 转换ast树过程中执行这几个方法
}
traverseNode(ast, context)
}
// 递归遍历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)
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.childrenIndex = i
traverseNode(children[i], context)
}
}
// 从里向外执行回调,处理文本、元素节点
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
transformText转换文本
js
function transformText(node) {
if (node.type !== 'Text') {
return
}
node.jsNode = {
type: 'StringLiteral',
value: node.content
}
}
transformElement转换元素
js
function transformElement(node) {
return () => {
if (node.type !== Type.Element) {
return
}
// div => h('div')
const callExp = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'h'
},
arguments: [{
type: 'StringLiteral',
value: node.tag
}]
}
// 有子元素,将参数加入到arguments里,h('div', ...)
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push({
type: 'ArrayExpression',
elements: node.children.map(v => v.jsNode)
})
node.jsNode = callExp;
}
}
transformRoot转换根节点
js
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
}
]
}
}
}
根据JavaScript AST生成渲染函数的代码
方法执行
js
function compile(template) {
const ast = parse(template) // ast模板
transform(ast) // ast转换成JavaScript AST
return generate(ast.jsNode) // 代码生成
}
compile('<div><p>123</p><p>456</p></div>')
function render () {
return h('div', [h('p', '123'), h('p', '456')])
}
方法实现
generate方法
js
function generate(node) {
const context = {
code: '',
currentIndent: 0,
newline() { // 换行
context.code += '\n' + ' '.repeat(context.currentIndent)
},
indent() { // 缩进2行
context.currentIndent++
context.newline()
},
deIndent() { // 取消缩进
context.currentIndent--
context.newline()
},
push(code) { // 拼接代码
context.code += code
}
}
genNode(node, context)
return context.code
}
genNode方法
js
function genNode(node, context) {
const { push, indent, deIndent } = context
switch (node.type) {
case 'FunctionDecl':
push(`function ${node.id.name} (`)
genNodeList(node.params, context)
push(`) {`)
indent()
node.body.forEach(n => genNode(n, context))
deIndent()
push('}')
break;
case 'ReturnStatement':
push('return ')
genNode(node.return, context)
break;
case 'CallExpression':
const { callee, arguments: args } = node
push(`${callee.name}(`)
genNodeList(args, context)
push(')')
break;
case 'StringLiteral':
push(`'${node.value}'`)
break;
case 'ArrayExpression':
push('[')
genNodeList(node.elements, context)
push(']')
break;
}
}
function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
genNode(nodes[i], context)
if (i < nodes.length - 1) {
push(', ')
}
}
}
最后
这段时间看的《Vue.js设计与实现》一书的第17章,觉得对于从文本到编译成render函数写的不错,记录一下学习笔记,感兴趣的朋友可以去看看这本书,收获不少。