前言
- 文分【思路篇】和【实现篇】,本文为实现篇第二版,文末有第一版链接,建议看两个窗口同步阅读,或请先阅读-》Vue|思路篇|实现ast
实现template生成render函数
目标
// <div>姓名 {{name}} <span>111</span></div> =>>
// {
// tag: 'div',
// parent: null,
// attrs: [],
// children: [{
// tag: null,
// parent: 父div,
// text: "姓名 {{name}}"
// },{
// tag: 'span',
// parent: 父div,
// attrs: [],
// children: [{
// tag:null,
// parent: 父div,
// text: 111
// }]
// }]
// }
定义正则
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 以 a-z A-Z _ 开头 后面可以接\-\.0-9_a-zA-Z] // 基础正则,匹配标签名:
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // 命名正则 用于匹配类似<my:select>情况
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 类似</div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配 mustatine 语法
通过html字符串生成ast语法树
遍历html字符串,获取标签名、属性、内容等信息
compiler/index.js
export function compilerToFunction(template){
let ast = parseHTML(template);
}
function parseHTML(html) {...}
定义“前进”函数,用于对html进行截取
// 将字符串进行截取操作,再更新字符串
function advance(n) {
html = html.substring(n)
}
正则提取有效信息,获取后对原html字符串进行截取然后继续循环处理,直到html为空
- 处理起始标签
<div id="11">
- 处理标签名
- 处理属性
function parseHTML(html) {
while (html){
// 1. 以<开头的必是 标签
let textEnd = html.indexOf('<');
if(textEnd == 0){
// 处理标签名
const startTagMatch = parseStartTag();
if(startTagMatch){
// 将获取到的信息传入对应处理函数
start(startTagMatch.tagName,startTagMatch.attrs);
continue;
}
...
}
}
function parseStartTag(){
// match 匹配返回对象文末扩展
const start = html.match(startTagOpen);
if(start){
const match = {
tagName: start[1],
attrs:[]
}
advance(start[0].length);
let attr,end;
// 1. 不是结尾(通过startTagClose进行判断) 2. 存在属性 则对属性进行处理 将匹配到的属性传入返回对象的attrs中
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))){
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;
}
}
}
....
}
- 处理结尾标签
function parseHTML(html) {
// debugger;
while (html){
// 1. 以<开头的必是 标签
let textEnd = html.indexOf('<');
if(textEnd == 0){
...
// 如果是结束标签 对字符串进行截取并将获取到的信息传入对应处理函数
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);// 将结束标签传入
continue;
}
}
...
}
}
- 处理文本
function parseHTML(html) {
// debugger;
while (html){
...
let text;
// 如果是文本,获取文本
if(textEnd >= 0){
text = html.substring(0,textEnd);
}
// 文本存在则对字符串进行截取并将获取到的信息传入对应处理函数
if(text){
advance(text.length);
chars(text);
}
}
}
- 对应信息处理函数
function start(tagName,attrs) {
console.log(tagName,attrs,"======开始标签 属性");
}
function end(tagName){
console.log(tagName,"======结束标签");
}
function chars(text){
console.log(text,"======文本");
}
树+栈 数据结构 生成ast树
生成树型结构
处理获取信息,生成ast树
由一个根节点不断发散,且子规模(树叶结构)相同,联想树结构
注意到这种场景很适合一种数据结构--树,即由一个根节点不断发散,且子规模(树叶结构)相同;我们就可以采用树形结构对获取的信息描述,即:根据模板生成ast树
compiler/index.js
定义树单元结构:
- 标签对应
{ tag: tagName, //标签名type: 1, // 标签类型children:[], // 孩子列表 attrs, // 属性集合parent:null // 父级元素}
; - 文本对应
{type:3,text:text}
// 标签元素 生成ast树单元
function createASTElement(tagName,attrs) {
return {
tag: tagName, //标签名
type: 1, // 标签类型
children:[], // 孩子列表
attrs, // 属性集合
parent:null // 父级元素
}
}
// 处理文本时直接存入此结构
{
type: 3,
text
}
根据获取信息生成对应树单元
- 处理函数start中创建对应ast元素
// 信息处理函数
function start(tagName,attrs) {
console.log(tagName,attrs,"======开始标签 属性");
let element = createASTElement(tagName,attrs);
if(!root){
root = element
}
currentParent = element;
stack.push(element)
}
采用栈结构记录处理标签过程,处理开始标签时将标签信息对象入栈,在处理闭合标签时
function end(tagName){
console.log(tagName,"======结束标签");
// 首先 对栈进行出栈操作,同时记录标签的父级信息对象(即出栈元素的parent属性指向栈顶元素)
let element = stack.pop();
// 其次 将出栈元素存入栈顶元素的children属性中,从而构建了父子结构
currentParent = stack[stack.length - 1];
// 在标签闭合时记录标签的父级
if(currentParent){
element.parent = currentParent;
currentParent.children.push(element);
}
// 最后,处理结束时判断栈是否为空,不为空说明匹配异常
}
最后,返回树根root,ast树完成
根据ast生成render函数
ast通过深度遍历然后字符串拼接可以实现转字符串
入口函数 接收树根,返回字符串
"_c('div','{...}','[]')"
compiler/generate.js
export function generate(el) {
let children = genChildren(el);
let code = `_c('${el.tag}',
${el.attrs.length ? `${genProps(el.attrs)}` : 'undefine'}
${
children ? `,${children}`: ''
}
)`;
return code;
}
递归处理子节点
- 如果是标签元素,直接调用generate函数
- 如果是文本,要处理
{{}}
变量问题,调用_s方法将变量转为字符串
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // mustatine 语法
function genChildren(el) {
const children = el.children;
if(children){
return children.map(child => gen(child)).join(',');
}
}
function gen(node) {
if(node.type == 1){
return generate(node);
}else {
let text = node.text;
if(!defaultTagRE.test(text)){
// 如果是普通文本
return `_v(${JSON.stringify(text)})`
}else {
// 存放每一段的代码
let tokens = [];
let lastIndex = defaultTagRE.lastIndex = 0; // 如果正则是全局模式 需要每次使用前将索引置为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 genProps(attrs) {
console.log(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)},`
}
console.log(str);
return `{${str.slice(0,-1)}}`;
}
通过new Function可以实现字符串转函数且通过with可以实现指定函数内的全局变量
compiler/index.js
let code = generate(ast);
let render = new Function(`with(this){return ${code}}`);
最后
在用户未传render时,将此render挂载上去,复用之前渲染(patch)逻辑
至此,ast完成
最终实现
- 仓库地址:git@github.com:Sympath/blingSpace.git
- 直接访问地址:github.com/Sympath/bli…