前言
小伙伴们在使用vue的时候,在模板template
中写入一段html代码,vue将template
中的代码解析并将其转化为虚拟DOM,这其中发生了什么呢?
compiler
首先来说说compiler
,在 Vue 3 中,编译器(compiler)的主要作用是将模板(template)转换为渲染函数(render function),以及将模板中的指令、插值等转换为对应的代码,当我们执行渲染函数时,就会返回VDOM
。
这里我们输入一段这样的template
:
js
let template = `
<div id="#app">
<div @click="()=>console.log('xx')" :id="name">{{name}}</div>
<h1 :name="title">玩转Vue3</h1>
<p>编译原理</p>
</div>
`;
tokenizer
这是第一步,当我们输入一段模板template
后,tokenizer
函数将模板template
进行分词,得到一个tokens
数组,分词的规则是将标签、属性、内容一一分解出来,接下来我们来看一段代码:
js
function tokenizer(input) {
let tokens = []
let type = '' // 标签 属性 ....
let val = ''
// 逐一字符分词
for (let i = 0; i < input.length; i++) {
let ch = input[i] // 每个字符
if (ch === '<') {
push()
if (input[i + 1] === '/') {
type = 'tagend'
} else {
type= 'tagstart'
}
}
if (ch === '>') {
if (input[i-1] == '=') {
// 箭头函数
} else{
push()
type="text"
continue
}
} else if (/[\s]/.test(ch)) {
push()
type='props'
continue // div 拿完了 不需要再加ch
}
val += ch
}
return tokens;
function push() {
if (val) {
// <div
if (type === 'tagstart') val = val.slice(1) // <div div
if (type === 'tagend') val = val.slice(2) // </div div
tokens.push({
type,
val
})
val = ''
}
}
}
tokenizer
函数接受一个字符串input
作为输入,然后返回一个包含词法单元的数组tokens
。- 在函数内部,首先定义了变量
tokens
用于存储词法单元,type
用于表示当前词法单元的类型,val
用于表示当前词法单元的值。 - 接下来是一个
for
循环,遍历输入字符串的每个字符。 - 在循环中,首先判断当前字符是否为
<
,如果是,则调用push()
函数将之前收集的词法单元推入tokens
数组中,并根据下一个字符判断当前<
是起始标签还是结束标签,分别设置type
为'tagstart'
或'tagend'
。 - 如果当前字符为
>
,则也调用push()
函数将之前收集的词法单元推入tokens
数组中,并设置type
为'text'
,表示文本节点。如果前一个字符是=
,则说明可能是箭头函数,这里我们没有写出 - 如果当前字符是空白符(空格、制表符、换行符等),则同样调用
push()
函数将之前收集的词法单元推入tokens
数组中,并设置type
为'props'
,表示属性。 - 如果以上条件都不满足,则将当前字符加入到
val
中,用于构建当前词法单元的值。 - 最后返回
tokens
数组作为输出。 - 在
push()
函数中,如果val
不为空,则根据当前词法单元的类型进行一些处理,如去除起始标签和结束标签的<
和</
,然后将该词法单元推入tokens
数组中,并清空val
。
分完词后,将会返回一个tokens
数组,我们输出一下这个数组来看看结果:
parse
调用parse
函数,是我们将要进行的第二个操作,将tokens
转化为一个抽象语法树ast
,我们来看看代码:
js
function parse(template) {
// 分词
const tokens = tokenizer(template);
console.log(tokens);
let cur = 0
let ast = {
type: 'root',
props: [],
children: []
}
while(cur < tokens.length) {
ast.children.push(walk())
}
return ast
function walk() {
let token = tokens[cur]
if (token.type == 'tagstart') {
let node = {
type: 'element',
tag: token.val,
props: [],
children: []
}
token = tokens[++cur]
while (token.type !== 'tagend') {
if (token.type == 'props') {
node.props.push(walk())
} else {
node.children.push(walk())
}
token = tokens[cur]
}
cur++
return node
}
if (token.type === 'tagend') {
cur++
}
if (token.type === 'text') {
cur++
return token
}
if (token.type === 'props') {
cur++
const [key,val] = token.val.replace('=', '~').split('~')
return {
key,
val
}
}
}
}
-
function parse(template) { ... }
: 这是一个名为parse
的函数,它接收一个模板字符串作为参数,然后调用tokenizer
函数对模板字符串进行分词,并利用分词结果生成抽象语法树(AST)。 -
const tokens = tokenizer(template);
: 调用tokenizer
函数将模板字符串转换成一个 tokens 数组,tokens 数组中包含了模板字符串中的各个词法单元。 -
let cur = 0
:cur
用于记录当前处理的 token 在 tokens 数组中的索引。 -
let ast = { type: 'root', props: [], children: [] }
: 创建一个名为ast
的对象,表示整个模板的抽象语法树。ast
对象包含了type
(类型)、props
(属性)和children
(子节点)三个字段,初始化为一个根节点。 -
while(cur < tokens.length) { ... }
: 使用while
循环遍历 tokens 数组中的每个 token,并通过调用walk
函数来递归地构建抽象语法树。 -
function walk() { ... }
:walk
函数用于递归地构建抽象语法树的节点。它根据当前处理的 token 类型进行不同的处理逻辑,并返回构建好的节点。- 当 token 类型为
tagstart
时,表示遇到了标签的开始,此时会创建一个元素节点,处理该标签的属性和子节点,并返回该节点。 - 当 token 类型为
tagend
时,表示遇到了标签的结束,跳过处理。 - 当 token 类型为
text
时,表示遇到了文本节点,直接返回该节点。 - 当 token 类型为
props
时,表示遇到了属性节点,将属性键值对提取出来,并返回键值对对象。
- 当 token 类型为
使用parse会得到一个抽象语法树ast
,接下来我们调用这些函数来看看输出结果:
js
function compiler(template) {
const ast = parse(template)
console.log(ast);
}
const renderFunction = compiler(template);