Appearance
三、模板编译
两种页面挂载方式
js
// 1. 参数中挂载
new Vue({ el: '#app'})
// 2. 手动挂载
vm.$mount("#app")0、定义挂载函数-获取模板
- 默认先找render方法
- 没有render方法会查找template
- 没有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
}