前言
小伙伴们在使用vue的时候,在模板template
中写入一段html代码,vue将template
中的代码解析并将其转化为虚拟DOM,这其中发生了什么呢?
compiler
首先来说说compiler
,在 Vue 3 中,编译器(compiler)的主要作用是将模板(template)转换为渲染函数(render function),以及将模板中的指令、插值等转换为对应的代码,当我们执行渲染函数时,就会返回VDOM
。
这里我们输入一段这样的template
:
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
数组,分词的规则是将标签、属性、内容一一分解出来,接下来我们来看一段代码:
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
,我们来看看代码:
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
时,表示遇到了属性节点,将属性键值对提取出来,并返回键值对对象。
使用parse会得到一个抽象语法树ast
,接下来我们调用这些函数来看看输出结果:
function compiler(template) { const ast = parse(template) console.log(ast); } const renderFunction = compiler(template);