Vue3
中编译原理(一)
从零手写一.Vue中模板编译原理
Vue中对
template
属性会编译成render
方法。vue-next
源码可以直接运行命令实现在线调试。打开网址:本地地址
npm run dev-compiler
1
二.模板编译步骤
export function baseCompile(template) {
// 1.生成ast语法树
const ast = baseParse(template);
// 2.转化ast语法树
transform(ast)
// 3.根据ast生成代码
return generate(ast);
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
AST
语法树
三.生成创建解析上下文,开始进行解析
function baseParse(content) {
// 创建解析上下文,在整个解析过程中会修改对应信息
const context = createParserContext(content);
// 解析代码
return parseChildren(context);
}
1
2
3
4
5
6
2
3
4
5
6
function createParserContext(content) {
return {
column: 1, // 列数
line: 1, // 行数
offset: 0, // 偏移字符数
originalSource: content, // 原文本不会变
source: content // 解析的文本 -> 不停的减少
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
对不同内容类型进行解析
解析节点的类型有:
export const enum NodeTypes {
ROOT,
ElEMENT,
TEXT,
SIMPLE_EXPRESSION = 4,
INTERPOLATION = 5,
ATTRIBUTE = 6,
DIRECTIVE = 7,
COMPOUND_EXPRESSION = 8,
TEXT_CALL = 12,
VNODE_CALL = 13,
JS_CALL_EXPRESSION = 17
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
function isEnd(context) {
const s = context.source; // 字符串解析完毕就结束
return !s;
}
function parseChildren(context){
const nodes = [];
while (!isEnd(context)) {
let node; // 解析节点
const s = context.source;
if (s.startsWith('{{')) { // 解析双括号
node = parseInterpolation(context);
} else if (s[0] == '<') { // 解析标签
node = parseElement(context);
} else { // 文本
node = parseText(context);
}
nodes.push(node);
}
return nodes
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.解析文本
文本可能是
我是文本、我是文本
function parseText(context) {
const endTokens = ['<', '{{']; // 当遇到 < 或者 {{ 说明文本结束
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
if (index !== -1 && endIndex > index) { // 找到离着最近的 < 或者 {{
endIndex = index
}
}
const start = getCursor(context); // 开始
const content = parseTextData(context, endIndex); // 获取文本内容
return {
type: NodeTypes.TEXT, // 文本
content,
loc: getSelection(context, start)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用于获取当前解析的位置
function getCursor(context) { // 获取当前位置信息
const { column, line, offset } = context;
return { column, line, offset };
}
1
2
3
4
2
3
4
function parseTextData(context, endIndex) { // 截取文本部分,并删除文本
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex);
return rawText;
}
1
2
3
4
5
2
3
4
5
将解析的部分移除掉,并且更新上下文信息
function advanceBy(context, index) {
let s = context.source
advancePositionWithMutation(context, s, index)
context.source = s.slice(index); // 将文本部分移除掉
}
const advancePositionWithMutation = (context, source, index) => {
let linesCount = 0
let lastNewLinePos = -1;
for (let i = 0; i < index; i++) {
if (source.charCodeAt(i) == 10) {
linesCount++; // 计算走了多少行
lastNewLinePos = i; // 记录换行的首个位置
}
}
context.offset += index; // 更新偏移量
context.line += linesCount; // 更新行号
context.column = lastNewLinePos === -1 ? context.column + index : index - lastNewLinePos
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
解析结果:
const {baseCompile} = VueCompilerDOM;
console.log(baseCompile(`zfjg`))
1
2
2
content: "zfjg"
loc:
end: {column: 5, line: 1, offset: 4}
source: "zfjg"
start: {column: 1, line: 1, offset: 0}
type: 2
1
2
3
4
5
6
2
3
4
5
6
2.解析表达式
获取花括号中的内容
function parseInterpolation(context) {
const closeIndex = context.source.indexOf('}}', '{{');
const start = getCursor(context);
advanceBy(context, 2);
const innerStart = getCursor(context);// 获取内部的开始
const innerEnd = getCursor(context);
const rawContentLength = closeIndex - 2; // 内容结束位置
// 去空格前的内容 和 去空格后的内容
const preTrimContent = parseTextData(context, rawContentLength);
const content = preTrimContent.trim();
const startOffset = preTrimContent.indexOf(content);
if (startOffset > 0) { // 根据标签开始位置修改innerStart
advancePositionWithMutation(innerStart, preTrimContent, startOffset)
}
const endOffset = content.length + startOffset;
// 根据标签结束位置修改innerStart
advancePositionWithMutation(innerEnd, preTrimContent, endOffset);
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
解析结果:
const { baseCompile } = VueCompilerDOM;
console.log(baseCompile(`{{ name }}`))
1
2
2
content:
isStatic: false
loc: {start: {…}, end: {…}, source: "name"}
type: 4
loc:
end: {column: 13, line: 1, offset: 12}
source: "{{ name }}"
start: {column: 1, line: 1, offset: 0}
type: 5
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
3.解析元素
获取标签名属性
function isEnd(context) {
const s = context.source;
if (s.startsWith('</')) { // 遇到闭合标签
return true;
}
return !s;
}
}
function parseElement(context) {
const element: any = parseTag(context);
const children = parseChildren(context); // 11.解析儿子 最后
if (context.source.startsWith('</')) {
parseTag(context)
}
element.children = children
element.loc = getSelection(context,element.loc.start)
return element
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function advanceSpaces(context) {
const match = /^[\t\r\n\f ]+/.exec(context.source)
if (match) {
advanceBy(context, match[0].length)
}
}
function parseTag(context) {
const start = getCursor(context);
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1];
advanceBy(context, match[0].length);
advanceSpaces(context);
let isSelfClosing = context.source.startsWith('/>');
advanceBy(context,isSelfClosing?2:1);
return {
type:NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc:getSelection(context,start)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
解析结果:
const { baseCompile } = VueCompilerDOM;
console.log(baseCompile(`<div><p></p></div>`))
1
2
2
children: Array(1)
children: []
isSelfClosing: false
loc: {start: {…}, end: {…}, source: "<p></p>"}
tag: "p"
type: 1
isSelfClosing: false
loc: {start: {…}, end: {…}, source: "<div><p></p></div>"}
tag: "div"
type: 1
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
4.解析属性
在开始标签解析完毕后解析属性
function parseTag(context) {
const start = getCursor(context); // 获取开始位置
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1];
advanceBy(context, match[0].length);
advanceSpaces(context)
let props = parseAttributes(context);
// ...
return {
type: NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc: getSelection(context, start),
props
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function parseAttributes(context) {
const props: any = [];
while (context.source.length > 0 && !startsWith(context.source, '>')) {
const attr = parseAttribute(context)
props.push(attr);
advanceSpaces(context); // 解析一个去空格一个
}
return props
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
function parseAttribute(context) {
const start = getCursor(context);
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0];
advanceBy(context, name.length);
let value
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context);
}
const loc = getSelection(context, start)
if (/^(:|@)/.test(name)) { // :xxx @click
let dirName = name.slice(1)
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: value.loc
},
loc
}
}
return {
type: NodeTypes.ATTRIBUTE,
name,
value: {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function parseAttributeValue(context) {
const start = getCursor(context);
const quote = context.source[0];
let content
const isQuoteed = quote === '"' || quote === "'"; // 解析引号中间的值
if (isQuoteed) {
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
content = parseTextData(context, endIndex);
advanceBy(context, 1);
}
return { content, loc: getSelection(context, start) }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
对文本节点稍做处理
function parseChildren(context) {
const nodes: any = [];
while (!isEnd(context)) {
//....
}
for(let i = 0 ;i < nodes.length; i++){
const node = nodes[i];
if(node.type == NodeTypes.TEXT){ // 如果是文本 删除空白文本,其他的空格变为一个
if(!/[^\t\r\n\f ]/.test(node.content)){
nodes[i] = null
}else{
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
}
return nodes.filter(Boolean)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
5.处理多个根节点
export function baseParse(content) {
// 创建解析上下文,在整个解析过程中会修改对应信息
const context = createParserContext(content);
// 解析代码
const start = getCursor(context);
return createRoot(
parseChildren(context),
getSelection(context,start)
)
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
将解析出的节点,再次进行包裹
ast.ts
export function createRoot(children,loc){
return {
type:NodeTypes.ROOT,
children,
loc
}
}
1
2
3
4
5
6
7
2
3
4
5
6
7