Vue 模版编译原理

241 阅读1分钟

一、模版编译过程

  • vue 模版编译的主要作用是将模版(template)编译为渲染函数(render)
  • vue 模版编译整体逻辑主要分为三步
    1. 将模板变成ast语法树 —— 解析器
    2. 对ast进行静态节点标记,主要用来做虚拟DOM的渲染优化 —— 优化器
    3. 使用ast生成渲染函数 —— 代码生成器
  • ast语法树(抽象语法书)是用来描述语法的,描述语法本身,描述成一个树结构

二、代码解析

在 vue 中有三种方式来创建 HTML ,分别是 template、render、jsx,在编译入口会对这三种情况分别做判断

2-1 模板编译入口

// init.js
Vue.prototype._init = function (options) {
        const vm = this;
        
        // 把用户的选项放到 vm 上,这样在其他方法中都可以获取到 options 了 
        // 为了后续扩展的方法,所有实例对象都可以获取 $options 选项
        // options 中是用户传入的数据 el ,data......
        vm.$options = options;

        // 初始化数据 state
        initState(vm);
        
        // 要将数据挂载到页面上
        if (vm.$options.el) {
            vm.$mount(vm.$options.el);
        }
    }
    
    // new Vue({el}) 等价于 new Vue().$mount
    Vue.prototype.$mount = function (el) {
        const vm = this;
        const opts = vm.$options;
        el = document.querySelector(el); // 获取真实的元素
        vm.$el = el; // 页面真实元素

        // 如果不存在render属性
        if (!opts.render) {
   
            let template = opts.template;
            
            // 如果不存在render和template,但是存在el属性
            if (!template) {
                // 直接将模板赋值到 el 所在的外层html结构(就是el本身 并不是父元素)
                template = el.outerHTML;
            }
            // 最终需要把tempalte模板转化成render函数
            let render = compileToFunction(template)
            opts.render = render;

        }

        // 把 render 渲染到 el 上
        mountComponent(vm)
    }

2-2 解析标签和内容,生成ast语法树

// parser.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 匹配标签名的  aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //  aa:aa-xxx  
const startTagOpen = new RegExp(`^<${qnameCapture}`); //  此正则可以匹配到标签名 匹配到结果的第一个(索引第一个) [1]
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>  [1]
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的

// [1]属性的key   [3] || [4] ||[5] 属性的值  a=1  a='1'  a=""
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的  />    > 
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{   xxx  }}  

// vue3的编译原理比vue2里好很多,没有这么多正则了
// 生成 ast 模版语法树
export function parserHTML(html) {
    // 可以不停的截取模板,直到把模板全部解析完毕 
    let stack = [];  // 栈
    let root = null; // 树根
    function createASTElement(tag, attrs, parent = null) {
        return {
            tag,
            type: 1, // 元素
            children: [],
            parent,
            attrs
        }
    }
    function start(tag, attrs) { // [div,p]
        // 遇到开始标签 就取栈中的最后一个作为父节点
        let parent = stack[stack.length - 1];
        let element = createASTElement(tag, attrs, parent);
        if (root == null) { // 说明当前节点就是根节点
            root = element
        }
        if (parent) {
            element.parent = parent; // 跟新p的parent属性 指向parent
            parent.children.push(element);
        }
        stack.push(element)
    }
    function end(tagName) {
        let endTag = stack.pop();
        if (endTag.tag != tagName) {
            console.log('标签出错')
        }
    }
    function text(chars) {
        let parent = stack[stack.length - 1];
        chars = chars.replace(/\s/g, "");
        if (chars) {
            parent.children.push({
                type: 2,
                text: chars
            })
        }
    }

    // 删除解析后的字符
    function advance(len) {
        html = html.substring(len);
    }

    // 解析到开始标签后,进入该方法
    function parseStartTag() {
        const start = html.match(startTagOpen);  // 4.30 继续
        if (start) {
            const match = {
                tagName: start[1],
                attrs: []
            }
            advance(start[0].length);
            let end;
            let attr;
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 1要有属性 2,不能为开始的结束标签 <div>
                match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] });
                advance(attr[0].length);
            } // <div id="app" a=1 b=2 >
            if (end) {
                advance(end[0].length);
            }
            return match;
        }
        return false;
    }

    // 解析入口
    while (html) {
        // 解析标签和文本   
        let index = html.indexOf('<');
        if (index == 0) {
            // 解析开始标签 并且把属性也解析出来  </div>
            const startTagMatch = parseStartTag()
            if (startTagMatch) { // 开始标签
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue;
            }
            let endTagMatch;
            if (endTagMatch = html.match(endTag)) { // 结束标签
                end(endTagMatch[1]);
                advance(endTagMatch[0].length);
                continue;
            }
        }
        // 文本
        if (index > 0) { // 文本
            let chars = html.substring(0, index) //<div></div>
            text(chars);
            advance(chars.length)
        }
    }

    return root;

}

解析的模版

<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-3 生成render函数中的代码字符串

// generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{   xxx  }}  

// 本质上是拼接字符串
function genProps(attrs) {
    // {key:value,key:value,}
    let str = '';
    for (let i = 0; i < attrs.length; i++) {

        let attr = attrs[i];

        // 将样式转成对象
        if (attr.name === 'style') { // {name:id,value:'app'}
            let styles = {}
            attr.value.replace(/([^;:]+):([^;:]+)/g, function () {
                styles[arguments[1]] = arguments[2];
            })
            attr.value = styles
        }

        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }
    return `{${str.slice(0, -1)}}`
}

function gen(el) {
    if (el.type == 1) {
        return generate(el); // 如果是元素就递归的生成
    } else {
        // 如果元素为文本
        let text = el.text;

        // 如果是普通文本
        if (!defaultTagRE.test(text)) return `_v('${text}')`;

        // 如果包含 {{name}},说明有表达式,需要做一个表达式和普通值的拼接 ['aaaa',_s(name),'bbb'].join('+)
        // _v('aaaa'+_s(name) + 'bbb')
        let lastIndex = defaultTagRE.lastIndex = 0;
        let tokens = []; // <div> aaa{{bbb}} aaa </div>
        let match;

        // ,每次匹配的时候 lastIndex 会自动向后移动
        while (match = defaultTagRE.exec(text)) { // 如果正则 + g 配合exec 就会有一个问题 lastIndex的问题
            let index = match.index;
            if (index > lastIndex) {
                tokens.push(JSON.stringify(text.slice(lastIndex, index)));
            }
            tokens.push(`_s(${match[1].trim()})`);
            lastIndex = index + match[0].length;
        }
        if (lastIndex < text.length) {
            tokens.push(JSON.stringify(text.slice(lastIndex)));
        }
        return `_v(${tokens.join('+')})`; // webpack 源码 css-loader  图片处理
    }
}

function genChildren(el) {
    let children = el.children;
    if (children) {
        return children.map(item => gen(item)).join(',')
    }
    return false;
}

// _c(div,{},c1,c2,c3,c4)
export function generate(ast) {
    let children = genChildren(ast)
    let code = `_c('${ast.tag}',${ast.attrs.length ? genProps(ast.attrs) : 'undefined'
        }${children ? `,${children}` : ''
        })`
    return code;
}

ast语法树生成的render函数代码字符串

'with(this){return _c('div',{attr:{"id":"el"}},[_v("hello"+ _s(name))])}'

//格式化之后
with(this) {
    return _c(
    "div",
    {attrs:{"id":"el"}},
    [_v("Hello"+_s(name))]
    )
}

render 渲染函数之所以可以生成vnode,是因为代码字符串中包含了很多函数调用。_c是createElement 的别名,渲染函数其实是执行了 createElement ,而 createElement 可以创建一个 vnode ,每处理一个AST节点,就会生成一个对应类型的代码字符串,进行嵌套

  • 元素AST节点 —— _c(, , )
  • 文本AST节点 —— _v(“Hello”+_s(name))
  • 注释节点 —— _e(text)

2-4 将代码字符串转化为render函数

import { generate } from "./generate";
import { parserHTML } from "./parser";

// 编译入口
export function compileToFunction(template) {

    // 1.将模板变成ast语法树
    let ast = parserHTML(template);

    // 代码优化 标记静态节点 (暂时不讲)

    // 2.代码生成(生成render函数)
    let code = generate(ast);
    let render = new Function(`with(this){return ${code}}`);
    console.log(render.toString())

}