<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--
模板编译原理 通过AST语法树描述出html语法结构 ,在把这个语法转换成JS语法,包上with 生成render方法 ,调用这个方法返回虚拟dom, 再把虚拟dom变成真实dom渲染到页面上。
注意:虚拟dom和ast语法树不一样,虚拟dom是表示dom对象的,ast语法树这里表示html结构
1. 将模板变成一个render函数,
2. 去当前的的实例上取值
3. 虚拟dom(说白了就是一个对象)它的好处是可以描述DOM结构,diff算法,可以比对哪些属性变化了哪些属性没有变化,把需要变化的进行更新
4. 生成真实dom
-->
<div id="app" a=1 b=2 style="color:red;font-size:12px;">
<span style="color:red;">{{name}} aa {{age}} haha<a>hello</a></span>
</div>
<script src="/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'f',
age: 18
}
}
})
// console.log(vm)
</script>
</body>
</html>
// src/compiler/index.js
import { generate } from "./generate";
import { parseHTML } from "./parse";
export function compileToFunctions(template) {
console.log(template);
const ast = parseHTML(template)
// 生成代码
const code = generate(ast)
// console.log('🚀 root', root);
}
import { compileToFunctions } from './compiler/index';
import { initState } from './state';
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
vm.$options = options; // 实例上有个属性$options表示的是用户传入的属性,可以通过vm.$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;
el = document.querySelector(el);
// 如果有render就直接用render,没有render,看看有没有template属性,如果也没有template属性的话,就直接找外部模板
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
// 将模板编译成render函数
const render = compileToFunctions(template);
options.render = render;
}
};
}
# 解析标签和内容
// src/compiler/parse.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
// console.log('<div:aa>'.match(startTagOpen)) // ["<div:aa", "div:aa", index: 0, input: "<div:aa>", groups: undefined]
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
// console.log('</div>'.match(endTag)) // ["</div>", "div", index: 0, input: "</div>", groups: undefined]
// 如 style="xxx" style='xxx' style=xxx
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
// console.log(`style="xxx"`.match(attribute)) // ["style="xxx"", "style", "=", "xxx", undefined, undefined, index: 0, input: "style="xxx"", groups: undefined]
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
// console.log(`>`.match(startTagClose)) // [">", "", index: 0, input: ">", groups: undefined]
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配双花括号
// console.log(`{{name}}`.match(defaultTagRE)) // ["{{name}}"]
/*
<div id="app">
<div style="color:red;">
<span>{{name}}</span>
</div>
</div>
ast 语法树
{
tag:'div',
type:1,
attrs: [{style:'color:red'}],
children: [
{
tag:'span',
type:1,
atttrs:[],
parent
}
],
parent: null
}
*/
export function parseHTML(html) {
while (html) {
let textEnd = html.indexOf('<');
if (textEnd == 0) {
// 处理开始标签
const startTagMatch = parseStartTag();
/*
'
<div style="color:red;">
<span>{{name}}</span>
</div>
</div>'
*/
// console.log('ddd', html)
if (startTagMatch) {
console.log('🚀 开始标签', startTagMatch.tagName);
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
// 处理结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
console.log('🚀 结束标签', endTagMatch[1]);
end(endTagMatch[1]);
continue;
}
}
/*
处理文本
'
<div style="color:red;">
<span>{{name}}</span>
</div>
</div>'
*/
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd);
console.log('🚀 文本标签', text);
}
if (text) {
advance(text.length);
chars(text);
}
}
function advance(n) {
html = html.substring(n);
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
// 标签名
tagName: start[1],
// 标签属性
attrs: []
};
// 将匹配到部分删除
advance(start[0].length);
let attr, end;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
// 匹配到一个属性,将其删除,对剩余部分进行查找
advance(attr[0].length);
match.attrs.push({ name: attr[1], value: attr[3] });
}
// 匹配到开始标签末尾 >
if (end) {
advance(end[0].length);
// 返回匹配结果
return match;
}
}
}
return root
}
# 生成ast语法树
// src/compiler/parse.js
let root;
let currentParent;
let stack = [];
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
};
}
function start(tagName, attrs) {
console.log(tagName, attrs);
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element); // 例如:[div, div, span, /span]
}
function end(tagName) {
console.log(tagName);
let element = stack.pop(); // 当遇到span结尾的时候就把span删掉 [div, div],让这个span记住它的parent是谁
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
function chars(text) {
console.log(text);
text = text.replace(/\s/g, '');
if (text) {
currentParent.children.push({
type: TEXT_TYPE,
text
});
}
}
# 生成代码
// src/compiler/generate.js
/*
测试例子:
<div id="app" a=1 b=2 style="color:red;font-size:12px;">
<span style="color:red;">{{name}} aa {{age}} haha<a>hello</a></span>
</div>
整理结果:
_c('div',{id:"app",a:"1",b:"2",style:{"color":"red","font-size":"12px"}},_c('span',{style:{"color":"red"}},_v(_s(name)+"aa"+_s(age)+"haha"),_c('a',undefined,_v("hello"))))
*/
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
export function generate(el) {
console.log('elll', el);
const children = getChildren(el);
const code = `_c('${el.tag}',${el.attrs.length > 0 ? genProps(el.attrs) : 'undefined'}${children
? ',' + children
: ''})`;
console.log(code);
return code;
}
// 区分是元素还是文本
function gen(node) {
// 元素节点
if (node.type == 1) {
return generate(node);
} else {
// 文本节点
/*
有普通文本 {{}}
混合文本{{aa}}aaa
*/
let text = node.text;
// 文本中不包含花括号
if (!defaultTagRE.test(text)) {
// JSON.stringify用于加双引号
return `_v(${JSON.stringify(text)})`;
}
// 因为上面已经用过正则了,lastIndex的位置已经发生改变,所以需要重新复位
let lastIndex = (defaultTagRE.lastIndex = 0);
// 存放解析结果
let tokens = [];
let match, index;
/*
入参:{{name}} aa {{age}} haha 输出 _v(_s(name) + 'aa' + _s(age) + 'haha')
*/
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('+')})`; // _v(_s(name)+"aa"+_s(age)+"haha")
}
}
function getChildren(el) {
// 生成儿子节点
const children = el.children;
if (children) {
return `${children.map((c) => gen(c)).join(',')}`;
} else {
return false;
}
}
// 生成属性
/*
将这里的属性<div id="app" a=1 b=2 style="color:red;font-size:12px;">整理成如下格式
{id:"app",a:"1",b:"2",style:{"color":"red","font-size":"12px"}
*/
function genProps(attrs) {
let str = '';
attrs &&
attrs.forEach((attr) => {
if (attr.name == 'style') {
const obj = {};
attr.value.split(';').forEach((item) => {
const [ key, value ] = item.split(':');
obj[key] = value;
console.log(key, value, obj);
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
});
return `{${str.slice(0, -1)}}`;
}
/*
<div id="app" a=1 b=2>
<span style="color:red;">{{name}}<a>hello</a></span>
</div>
render函数执行后的结果才是虚拟dom
v表示vnode
s字符串
render(){
return _c(
'div', {id:'app', a:1, b:2},
_c(
'span',
{style:{color:'red'}},
_s(_v(name)),
_c(
'a',
{},
_v('hello')
)
)
)
}
*/
# 生成render函数
// src/compiler/index.js
import { generate } from './generate';
import { parseHTML } from './parse';
export function compileToFunctions(template) {
console.log(template);
const ast = parseHTML(template);
// 生成代码
const code = generate(ast);
const render = `with(this){return ${code}}`;
const fn = new Function(render); // 让字符串变成一个函数
console.log('fn', fn);
/*
输出结果
(function anonymous() {
with(this){
return _c('div',{id:"app",a:"1",b:"2",style:{"color":"red","font-size":"12px"}},_c('span',{style:{"color":"red"}},_v(_s(name)+"aa"+_s(age)+"haha"),_c('a',undefined,_v("hello"))))
}
})
*/
return fn;
// console.log('🚀 root', root);
}
← Vue响应式原理 创建渲染Watcher →