Vue模板编译原理浅析

184 阅读3分钟

1. 概念

 模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?这就归功于模板编译功能。

 Vue 的模板编译是在 mount 的过程中进行的,在 mount的时候执行了 compile 这个方法来将 template 里的内容转换成真正的 HTML 代码。complie 最终生成 render 函数,等待调用。

 模板编译的作用是生成渲染函数,通过执行渲染函数生成最新的vnode,最后根据vnode进行渲染。

2.将模板编译成渲染函数

2.1 两个步骤,三部分内容

两个步骤:

  • 模板解析成AST(Abstract Syntax Tree,抽象语法树,用对象描述节点)
  • 使用AST生成渲染函数

*** 由于静态节点不需要总是重新渲染,所以在生成AST之后,生成渲染函数之前,需要遍历AST标记静态节点

三部分内容分别抽象出三个模块:

  • 解析器:模板解析成AST
  • 优化器:遍历AST标记静态节点
  • 代码生成器:使用AST生成渲染函数

2.2 解析器

2.2.1 解析器的作用

将模板字符串解析成AST

<div>
  <p>{{name}}</p>
</div>

上述模板转化成AST后

{
    tag: "div",
    type: 1,
    staticRoot: false,
    static: false,
    plain: true,
    parent: undefined,
    attrsList: [],
    attrsMap: {},
    children: [{
        tag: "p",
        type: 1,
        staticRoot: false,
        static: false,
        plain: true,
        parent: {tag:"div", ... },
        attrsList: [],
        attrsMap: {},
        children: [{
            type:2,
            text:"{{name}}",
            static:false,
            expression:"_s(name)"
        }]
    }]
}

2.2.2 解析器内部运行原理

解析器包括:HTML解析器,文本解析器,过滤解析器

HTML解析器:HTML解析器的作用是解析HTML,它在解析的过程中会触发不同的钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数

2.2.2.1 钩子函数

伪代码如下:

parseHTML(template,{
    start(start){
    // 每当解析到标签的开始位置时,触发该函数
    },
    end(){
    // 每当解析到标签的结束位置时,触发该函数
    },
    chars(text){
    // 每当解析到文本时,触发改函数
    },
    comment(text){
    // 每当解析到注释时,触发该函数
    }
})

例:

<div>
  <p>{{name}}</p>
</div>

触发钩子函数的顺序是:start(),start(),chars(),end(),end()

2.2.1.2 层级关系

用栈来记录层级关系

触发start函数-->将节点推入栈中

触发end函数-->将节点弹出栈中

<div>
    <p>
    <span>栈</span>层级关系</p>
</div>

对于当前节点span来说:

根节点父节点
divp

2.3 HTML解析器

解析HTML模板的过程就是循环的过程

代码查看解析过程

// 匹配开始标签的开头<
const tagStartReg = /^<([a-zA-Z_][\w\-]*)/;
// 匹配开始标签的结尾,是否是自闭和标签 />
const tagStartCloseReg = /^\s*(\/?)>/;
// 匹配标签的属性
const tagAttrReg = /^\s*([a-zA-Z_@:][\w\-\:]*)(?:(?:=)(?:(?:"([^"]*)")|(?:'([^']*)')))?/;
// 匹配结束标签 </as>
const tagEndReg = /^<\/([a-zA-Z_][\w\-]*)>/;
// 匹配文本,不是以<开头
const textReg = /^[^<]*/;
// 全局匹配 {{}}  不是{}的字符
const dataReg = /\{\{([^{}]+)\}\}/g;

let stack = [];
let currentAst = {};
let root = null;
let index = 0; // 模板解析指针位置

let template = `<div id="index"><p>hello, {{msg}}</p>vue编译</div>`
parseHTML(template, {
    start,
    end,
    chars
});

// template to ast
function parseHTML(templates, hooks) {
    template = templates
    while (template) {
        let tagStart = template.indexOf('<');
        if (tagStart === 0) {
            let start = template.match(tagStartReg); // ["<div", "div", index: 0, input: "<div id="index"><p>hello, {{msg}}</p>vue编译</div>", groups: undefined]
            if (start) {
                hooks.start(start);
            };
            let end = template.match(tagEndReg);
            if (end) {
                step(end[0].length);
                // 结束标签,触发结束钩子函数
                hooks.end();
            };
        } else {
            let text = template.match(textReg);
            step(text[0].length);
            // 文本标签,触发文本钩子函数
            let ast = {
                type: 'text',
                value: hooks.chars(text[0].replace(/\s+/, ' '))
            }
            currentAst.children.push(ast);
            ast.parent = currentAst;
        }
    }
    console.log('root', root);
    return root;
}

// 解析到开始标签的位置时,触发的钩子函数
function start(start) {
    // 是开始标签,触发钩子函数start
    let ast = {
        type: 'tag',
        name: start[1],
        attrs: [],
        children: []
    };
    // 指针向后移动
    step(start[0].length);
    // 匹配属性
    let end, attr;
    while (!(end = template.match(tagStartCloseReg)) && (attr = template.match(tagAttrReg))) {
        ast.attrs.push({
            key: attr[1],
            value: attr[2] || attr[3] // 双引号时是attr[2],单引号时是attr[3]
        });
        step(attr[0].length);
    }
    if (end) {
        step(end[0].length);
        // 不是自闭和标签
        if (!end[1]) {
            if (!root) {
                root = ast;
            } else {
                currentAst.children.push(ast);
                ast.parent = currentAst;
            }
            stack.push(ast);
            currentAst = ast;
        }
    };
}

// 解析到结束标签的位置时,触发的钩子函数
function end() {
    // 结束标签弹出栈中
    stack.pop();
    currentAst = stack[stack.length - 1];
}

// 解析到文本的位置时,触发的钩子函数
function chars(text) {
    const result = []
    let lastIndex = 0
    let match, index
    // 是否存在变量{{}}
    while ((match = dataReg.exec(text))) {
        index = match.index
        if (index > lastIndex) {
            result.push(JSON.stringify(text.slice(lastIndex, index)))
        }
        let data = match[1].trim()
        result.push(`${data === null ? '' : data}`)
        lastIndex = index + match[0].length
    }
    if (lastIndex < text.length) {
        result.push(JSON.stringify(text.slice(lastIndex)))
    }
    return result.join('+')
}

// 截取模板
function step(length) {
    index += length;
    template = template.slice(length); // 'id="index"><p>hello, {{msg}}</p>vue编译</div>'
}