模板编译 Vue中对template属性会编译成render方法。在线模板编译器

增添新的包compiler-core/package.json

{
    "name": "@vue/compiler-core",
    "version": "1.0.0",
    "description": "@vue/compiler-core",
    "main": "index.js",
    "module": "dist/compiler-core.esm-bundler.js",
    "buildOptions": {
        "name": "VueCompilerCore",
        "compat": true,
        "formats": [
            "esm-bundler",
            "cjs"
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们将开发环境下的打包入口改为 compile-core,这里我们先提供所需要ast的节点类型

export function compile(template){
    // 1.将模板转化成ast语法树
    const ast = baseParse(template);
    // 2.对ast语法树进行转化
    transform(ast);
    // 3.生成代码
    return generate(ast)
}
1
2
3
4
5
6
7
8

生成ast语法树

准备语法树相关type

export const enum NodeTypes {
  ROOT, // 根节点 Fragment
  ELEMENT, // 元素
  TEXT, // 文本
  COMMENT, // 注释
  SIMPLE_EXPRESSION, // 表达式的值
  INTERPOLATION,  // 插值
  ATTRIBUTE, // 属性
  DIRECTIVE, // 指令
  // containers
  COMPOUND_EXPRESSION, // 复合表达式
  IF, 
  IF_BRANCH,
  FOR,
  TEXT_CALL,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建解析上下文

创建解析上下文,并且根据类型做不同的处理解析。 ast转化

function createParserContext(content) {
    return {
        line: 1,
        column: 1,
        offset: 0,
        source: content, // source会不停的被截取
        originalSource: content // 原始内容
    }
}
function isEnd(context) {
    const source = context.source;
    return !source;
}
function parseChildren(context) {
    const nodes = [];
    while (!isEnd(context)) {
        const s = context.source;
        let node;
        if (s.startsWith('{{')){ // 处理表达式类型
        }else if(s[0] === '<'){ // 标签的开头
            if(/[a-z]/i.test(s[1])){} // 开始标签
        }
        if(!node){ // 文本的处理
            
        }
        nodes.push(node);
    }
    return nodes;
}
function baseParse(template){
    const context =  createParserContext(template);
    return parseChildren(context);
}
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

处理文本节点

采用假设法获取文本结束位置

function parseText(context) { // 123123{{name}}</div>
    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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

处理文本内容,删除匹配到的结果,计算最新上下文位置信息

function parseText(context) {
    // ...
    let start = getCursor(context); // 1.获取文本开始位置
    const content = parseTextData(context, endIndex); // 2.处理文本数据

    return {
        type: NodeTypes.TEXT,
        content,
        loc: getSelection(context, start) // 3.获取全部信息
    }
}
1
2
3
4
5
6
7
8
9
10
11
function getCursor(context) { // 获取当前位置
    let { line, column, offset } = context;
    return { line, column, offset }
}
function parseTextData(context, endIndex) {
    const rawText = context.source.slice(0, endIndex);
    advanceBy(context, endIndex); // 截取内容
    return rawText
}
function advanceBy(context, endIndex) {
    let s = context.source;
    advancePositionWithMutation(context, s, endIndex) // 更改位置信息
    context.source = s.slice(endIndex);
}
function advancePositionWithMutation(context, s, endIndex) { // 更新最新上下文信息
    let linesCount = 0; // 计算行数
    let linePos = -1; // 计算其实行开始位置
    for (let i = 0; i < endIndex; i++) {
        if (s.charCodeAt(i) === 10) { // 遇到\n就增加一行
            linesCount++;
            linePos = i; // 记录换行后的字节位置
        }
    }
    context.offset += endIndex; // 累加偏移量
    context.line += linesCount; // 累加行数
    // 计算列数,如果无换行,则直接在原列基础 + 文本末尾位置,否则 总位置减去换行后的字节位置
    context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos
}
function getSelection(context,start){
    const end = getCursor(context);
    return {
        start,
        end,
        source:context.originalSource.slice(start.offset,end.offset)
    }
}
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

转化成最终ast节点结果,标记ast节点类型

处理表达式节点

获取表达式中的变量,计算表达式的位置信息

function parseInterpolation(context) { 
    const start = getCursor(context); // 获取表达式的开头位置
    const closeIndex = context.source.indexOf('}}', '{{'); // 找到结束位置
    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 startOffest = preTrimContent.indexOf(content);
    if (startOffest > 0) { // 有空格
        advancePositionWithMutation(innerStart, preTrimContent, startOffest); // 计算表达式开始位置
    }
    const endOffset = content.length + startOffest;
    advancePositionWithMutation(innerEnd, preTrimContent, endOffset)
    advanceBy(context, 2);
    return {
        type: NodeTypes.INTERPOLATION,
        content: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            isStatic: false,
            content,
            loc: getSelection(context, innerStart, innerEnd) // 需要修改getSelection方法
        },
        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

处理元素节点

处理标签

获取标签名称,更新标签位置信息

function advanceSpaces(context){
    const match = /^[ \t\r\n]+/.exec(context.source);
    if(match){
        advanceBy(context,match[0].length);
    }
}
function parseTag(context){
    const start = getCursor(context); // 获取开始位置
    const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source); // 匹配标签名
    const tag = match[1];
    advanceBy(context,match[0].length); // 删除标签
    advanceSpaces(context); // 删除空格
    const isSelfClosing = context.source.startsWith('/>'); // 是否是自闭合
    advanceBy(context,isSelfClosing?2:1); // 删除闭合 /> >
    return {
        type:NodeTypes.ELEMENT,
        tag,
        isSelfClosing,
        loc:getSelection(context,start) 
    }
}
function parseElement(context) {
    // 1.解析标签名 
    let ele = parseTag(context);
    if(context.source.startsWith('</')){
        parseTag(context); // 解析标签,标签没有儿子,则直接更新标签信息的结束位置
    }
    ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
    return ele;
}
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

处理子节点

递归处理子节点元素

function isEnd(context) {
    const source = context.source;
    if(context.source.startsWith('</')){ // 如果遇到结束标签说明没有子节点
        return true;
    }
    return !source;
}
function parseElement(context) {
    let ele = parseTag(context);
    const children = parseChildren(context); // 因为结尾标签, 会再次触发parseElement,这里如果是结尾需要停止
    if(context.source.startsWith('</')){
        parseTag(context); 
    }
    ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
    (ele as any).children = children; // 添加children
    return ele;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

处理属性

在处理标签后处理属性

function parseTag(context){
    const start = getCursor(context); 
    const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source); 
    const tag = match[1];
    advanceBy(context,match[0].length); 
    advanceBySpaces(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
function parseAttributes(context) {
    const props: any = [];
    while (context.source.length > 0 && !context.source.startsWith('>')) {
        const attr = parseAttribute(context)
        props.push(attr);
        advanceSpaces(context); // 解析一个去空格一个
    }
    return props
}
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)
    return {
        type: NodeTypes.ATTRIBUTE,
        name,
        value: {
            type: NodeTypes.TEXT,
            content: value.content,
            loc: value.loc
        },
        loc
    }
}
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
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

处理空节点

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

创建根节点

将解析出的节点,再次进行包裹,这样可以支持模板下多个根节点的情况, 也是我们常说的 Fragment

export function createRoot(children,loc){
   return {
       type:NodeTypes.ROOT,
       children,
       loc
   }
}
function baseParse(template) {
   // 标识节点的信息  行 列 偏移量
   const context = createParserContext(template);
   const start = getCursor(context);
   return createRoot(
       parseChildren(context),
       getSelection(context,start)
   )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16