Skip to content

三、模板编译

两种页面挂载方式

js
// 1. 参数中挂载
new Vue({ el: '#app'})

// 2. 手动挂载
vm.$mount("#app")

0、定义挂载函数-获取模板

  1. 默认先找render方法
  2. 没有render方法会查找template
  3. 没有template会找当前el指定的元素中的内容来进行渲染
js
// src\init.js

import { compileToFunction } from "./compiler/index"

// 初始化方法 
Vue.prototype._init = function(options) {
	// 拿到当前实例
	const vm = this
	vm.$options = options
	// 初始化状态
	initState(vm)
	// 如果当前有el属性,需要进行模板渲染
	if (vm.$options.el) {
		vm.$mount(vm.$options.el);
	}
}

// 挂载函数
Vue.prototype.$mount = function(el) {
	// 拿到当前实例
	const vm = this;
	const options = vm.$options;
	// 获取dom对象
	el = document.querySelector(el);

	// 1. 如果没有render方法,需要将template转化为render方法
	if (!options.render) {
		// 判断是否配置了模板
		let template = options.template;
		// 如果没有模板但是有el,就获取整个外部HTML
		if (!template && el) {
			template = el.outerHTML;
		}
		// 编译原理:将模板编译成render函数
		const render = compileToFunction(template);
		options.render = render
	}
	// 2. 有render方法
	// 渲染最终用的都是这个render方法

	// 需要挂载这个组件
	// mountComponent(vm, el);
}

1、解析模板-标签和内容

  • ast 抽象语法树,用对象来描述语言本身
  • 虚拟dom,用对象来描述节点
js
// src\compiler\index.js

import { parseHTML } from "./parse";

export function compileToFunction(template) {
	// 1、将模板转为ast
    let ast = parseHTML(template);
    console.log(ast);
}
js
// src\compiler\parse.js

// 思路:利用正则匹配字符串,匹配到了就截取字符串放到相应位置,一直截取完成就转化为了ast了

// 模板解析正则
// 匹配标签名,aaa-123aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 匹配命名空间标签 <my:xxx></my:xxx>,捕获的内容是标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 匹配标签开头
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性,三种写法:aaa="aaaa" | aaa = 'aaaa' | aaa = aaa
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 >
const startTagClose = /^\s*(\/?)>/; 
// 匹配双大括号,{{ xxx }}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

// 解析函数
export function parseHTML(html) {

    function start(tagName, attrs) {
        console.log(tagName, attrs, '------开始---');
    }

    function end(tagName) {
        console.log(tagName, '------结束---');
    }

    function chars(text) {
        console.log(text, '-------文本---');
    }

    // 循环解析:只要html不为空字符串就一直解析
    while (html) {
		// 匹配开始|结束标签,尖括号开头
        let textEnd = html.indexOf('<');
        if (textEnd == 0) {
            // 1、处理开始标签:开始标签匹配结果,获得标签名称和属性
            const startTagMatch = parseStartTag();
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue;
			}
			
            // 2、处理结束标签:匹配结束标签
            const endTagMatch = html.match(endTag);
            if (endTagMatch) {
                advance(endTagMatch[0].length);
                end(endTagMatch[1]);
                continue;
            }
        }

        // 3、处理文本
        let text;
        if (textEnd > 0) {
            // 截取文本
            text = html.substring(0, textEnd);
        }
        if (text) {
            // 处理文本
            advance(text.length);
            chars(text);
        }
    }

    // 字符串进行截取操作,再更新html内容
    function advance(n) {
        html = html.substring(n);
    }

	// 处理开始标签函数
    function parseStartTag() {
        const start = html.match(startTagOpen);
        if (start) {
            //  ["<div", "div", index: 0, input: "<div id="app">...</div>", groups: undefined]
            const match = {
				tagName: start[1], // 标签名
				attrs: [] // 属性
			};
			// 删除开始标签
            advance(start[0].length);

            let end;
            let attr;
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                // 不是结束标签,并且有属性,就进行属性取值
                // [" id="app"", "id", "=", "app", undefined, undefined, index: 0, ...]
                match.attrs.push({
                    name: attr[1], // 属性名称
                    value: attr[3] || attr[4] || attr[5] // 属性值
                });
                // 删除属性
                advance(attr[0].length);
            }
            // > 没有属性就表示为 结束的闭合标签
            if (end) {
                // 删除结束标签
                advance(end[0].length);
                return match;
            }
        }
    }

    return root;
}

2、生成ast语法树

js
// ast语法树模板

// <div>hello {{name}} <span>world</span></div>
{
	tag: 'div',
	parent: null,
	type: 1,
	attrs: [],
	children: [
		{
			tag: null,
			parent: '父div对象',
			attrs: [],
			text: hello {{name}} 
		}
	]
}
js
// src\compiler\parse.js

// 开始标签依次存入stack中,在结束标签的时候取出建立父子关系

export function parseHTML(html) {

	let root; // 根节点,也是树根
    let currentParent; // 当前父元素
	let stack = []; // 栈
	const ELEMENT_TYPE = 1; // 元素类型
	const TEXT_TYPE = 3; // 文本类型

	// 创建ast对象
	function createASTElement(tagName, attrs) {
		return {
			tag: tagName, // 标签名
			type: ELEMENT_TYPE, // 元素类型
			children: [], // 孩子列表
			attrs, // 属性集合
			parent: null // 父元素
		}
	}
	
    // 标签是否符合预期
    // <div><span></span></div>
	// 处理开始标签
    function start(tagName, attrs) {
        // 创建一个元素,作为根元素
        let element = createASTElement(tagName, attrs);
        if (!root) {
            root = element;
        }
        // 当前解析标签保存起来
        currentParent = element;
        // 将生产的ast元素放到栈中
        stack.push(element);
    }

    // 在结尾标签处,创建父子关系
    // <div><p><span></span></p></div>  [div, p, span]
    function end(tagName) {
        let element = stack.pop(); // 取出栈中的最后一个
        currentParent = stack[stack.length - 1]; // 倒数第二个是父亲
        if (currentParent) {
            // 闭合时可以知道这个标签的父亲是谁,儿子是谁
            element.parent = currentParent;
            currentParent.children.push(element);
        }
    }

	// 处理文本
    function chars(text) {
		// 去除空格
        text = text.replace(/\s/g, '');
        if (text) {
            currentParent.children.push({
                type: TEXT_TYPE,
                text
            });
        }
	}
	
	return root;
}

3、生成代码

template模板转化为render函数示例

js
// src\compiler\generate.js

// 编写:
<div id="app" style="color:red">hello {{name}} <span>hello</span></div>

// 结果:
render() {
	return _c('div', {id: 'app', style: {color: 'red'}}, _v('hello'+_s(name)),_c('span',null,_v('hello')))
}

代码生成

js

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

// 生成单个儿子节点
function gen(node) {
    // 判断元素还是标签
    if (node.type === 1) {
		// 递归进行元素节点字符串的生成
        return generate(node);
    } else {
        let text = node.text; // 获取文本
        // 如果是普通文本,不带{{}}
        if (!defaultTagRE.test(text)) {
			//_v('hello {{name}} world {{msg}}') => _v('hello' + _s(name))
            return `_v(${JSON.stringify(text)})`;
        }
        // 存放每一段代码
        let tokens = [];
        // 如果正则是全局模式,需要每次使用前置为0
        let lastIndex = defaultTagRE.lastIndex = 0;
        // 每次匹配到的结果
        let match, index;

        while (match = defaultTagRE.exec(text)) {
            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('+')})`;
    }
}

// 生成儿子节点
function genChildren(el) {
    const children = el.children;
    if (children) {
        // 将所有转化后的儿子用都好拼接起来
        return children.map(child => gen(child)).join(',');
	}
	return false;
}

// 生成属性
function genProps(attrs) {
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') {
			// 如果是样式需要特殊处理下
            let obj = {};
            attr.value.split(';').forEach(item => {
                let [key, value] = item.split(':');
                obj[key] = value;
            })
            attr.value = obj;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    return `{${str.slice(0, -1)}}`;
}

// 语法层面的转义:ast树转化为code字符串代码
export function generate(el) {
    // 儿子的生成
    let children = genChildren(el);

	// 拼接代码:元素和儿子
    let code = `_c('${el.tag}',${
		el.attrs.length ? `${genProps(el.attrs)}` : undefined
	}${
		children ? `,${children}` : ''
	})`;

    return code;
}

let code = generate(ast);

4、生成render函数

js
// src\compiler\index.js

import { parseHTML } from "./parse";
import { generate } from "./generate";

export function compileToFunctions(template) {
	// 1、模板转为ast
	let ast = parseHTML(template);
	// 2、ast转为code代码字符串
	let code = generate(ast);
	// 3、通过new Function + with的方式:将字符串变成函数
	// 原理:通过with来限制取值范围,后续调用render函数改变this就可以取到结果了
    let render = `with(this){return ${code}}`;
	let renderFn = new Function(render);
	
    return renderFn
}

Released under the MIT License.