浅聊一下
在vue中,我们在template
模板中写代码,vue将template
模板中的代码解析成抽象语法树
,再通过一系列操作变成DOM挂载在页面上,那么vue是如何识别template
模板并且将其转为虚拟DOM的呢?
compile
在vue中,compile 是指将模板编译为渲染函数的过程。我们来看看在这个过程中,到底发生了什么?
js
let template = `
<div id="#app">
<div @click="()=>console.log('xx')" :id="name">{{name}}</div>
<h1 :name="title">玩转Vue3</h1>
<p>编译原理</p>
</div>
`
这是我们的一个模板,我们要将标签、属性、内容逐一分解出来,聪明的掘友可能已经想到了,这里使用正则来完成...
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
}
console.log(tokens);
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 函数,并根据下一个字符是否为 "/" 来判断是标签的开始还是结束。
- 如果当前字符是 ">",则调用 push 函数,并根据前一个字符是否是 "=" 来判断是箭头函数还是普通文本,然后继续处理下一个字符。
- 如果当前字符是空白字符(包括空格、制表符等),则调用 push 函数,并将当前类型设置为属性,然后继续处理下一个字符。
- 否则,将当前字符加入到 val 变量中。
-
在 push 函数中,将当前处理的值和类型推入 tokens 数组中,并根据类型对值进行修剪处理,然后重置 val 变量。
-
最后,输出 tokens 数组并返回。
来看看token数组
完美地将每一块内容都精准地分开,接下来的操作就是将这个token数组转为一个抽象语法树了,其实我在之前就已经讲过类似的内容了面试官:来道送分题 - 掘金 (juejin.cn),有点像将列表转为树...但是这里要复杂一点
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
}
}
}
}
-
定义了一个名为 parse 的函数,该函数接收一个模板字符串作为参数,然后调用 tokenizer 函数对模板进行分词,得到 tokens 数组。
-
初始化 cur 变量为 0,表示当前处理的 token 索引,同时初始化一个空的 ast 对象作为最终的抽象语法树,其中包含一个根节点,具有类型、属性和子节点等字段。
-
使用 while 循环遍历 tokens 数组,并在循环内部调用 walk 函数来构建 AST 的子节点,将子节点添加到根节点的 children 数组中。
-
walk 函数用于递归地处理每个 token,并根据不同类型的 token 构建对应的 AST 节点:
- 如果 token 类型为 'tagstart',表示一个标签的开始,创建一个 element 类型的节点,包含标签名、属性和子节点等信息,然后递归处理其子节点。
- 如果 token 类型为 'tagend',表示一个标签的结束,直接跳过。
- 如果 token 类型为 'text',表示文本节点,直接返回该文本节点。
- 如果 token 类型为 'props',表示属性节点,解析属性键值对并返回。
-
最后,返回构建好的抽象语法树 ast。
来看看输出
结尾
到这里我们已经完成从template
模板到抽象语法树的过程了,接下来将该来到diff算法环节...