从零手写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

三.生成AST语法树

创建解析上下文,开始进行解析

function baseParse(content) {
    // 创建解析上下文,在整个解析过程中会修改对应信息
    const context = createParserContext(content);
    // 解析代码
    return parseChildren(context);
}
1
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

对不同内容类型进行解析

解析节点的类型有:

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
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

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

用于获取当前解析的位置

function getCursor(context) { // 获取当前位置信息
    const { column, line, offset } = context;
    return { column, line, offset };
}
1
2
3
4
function parseTextData(context, endIndex) { // 截取文本部分,并删除文本
    const rawText = context.source.slice(0, endIndex);
    advanceBy(context, endIndex);
    return rawText;
}
1
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

解析结果:

const {baseCompile} = VueCompilerDOM;
console.log(baseCompile(`zfjg`))
1
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.解析表达式

获取花括号中的内容

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

解析结果:

 const { baseCompile } = VueCompilerDOM;
 console.log(baseCompile(`{{  name  }}`))
1
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

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
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

解析结果:

const { baseCompile } = VueCompilerDOM;
console.log(baseCompile(`<div><p></p></div>`))
1
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

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
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
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
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

对文本节点稍做处理

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

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

将解析出的节点,再次进行包裹

ast.ts

export function createRoot(children,loc){
    return {
        type:NodeTypes.ROOT,
        children,
        loc
    }
}
1
2
3
4
5
6
7