<!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);
}