Vue-ast|实现篇第二版|实现ast/vnode及页面渲染

1,429 阅读4分钟

前言

  • 文分【思路篇】和【实现篇】,本文为实现篇第二版,文末有第一版链接,建议看两个窗口同步阅读,或请先阅读-》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完成

最终实现

相关链接