首页 > 其他分享 >前端开发系列117-进阶篇之ast && render

前端开发系列117-进阶篇之ast && render

时间:2022-12-17 13:45:49浏览次数:60  
标签:name render ast text value 进阶篇 let attrs 节点

title:  前端开发系列117-进阶篇之AST && render
tags:
categories: []
date: 2019-04-13 01:00:08
本文将讨论 AST 转换为 渲染字符串并最终调整为 render 渲染函数的具体过程,这样的讨论有益于我们加深对常见的模板引擎其工作机制以及Vue等前端框架的理解 。

在上图中简单画出了这篇文章的代码要完成的主要工作,即把模板编译得到的 AST 抽象语法树处理为 render字符串继而包装为render渲染函数

假设我们要编译的模板字符串为:

<div id="app" title="标题"><p>hello</p><span>{{msg}}</span></div>

那么编译为 AST 语法树后大概应该长成下面这样。

 { tag: 'div',
  attrs:
   [ { name: 'id', value: 'app' }, { name: 'title', value: '标题' } ],
  children:
   [ { tag: 'p',
       attrs: [],
       children: [Array],
       parent: [Circular],
       nodeType: 1 },
     { tag: 'span',
       attrs: [],
       children: [Array],
       parent: [Circular],
       nodeType: 1 } ],
  parent: null,
  nodeType: 1 
}

我们需要通过代码来得到的renderString应该是下面这样的字符串结构(忽略换行)。

'_createElement("div",
    {id:"app",title:"标题"},
    _createElement("p",null,_v("hello")),
    _createElement("span",null,_v(_s(msg))))'

在得到的整个字符串中,主要包含的要素有:_createElement这是创建函数的名称,divp等这是对应标签的名称,{id:"app",title:"标题"}这部分是对应标签的属性节点,如果当前标签存在子标签,那么应该以递归的方式来进行处理。因为整个过程比较复杂,所有下面分成 属性节点处理标签(子)节点处理两个部分。

属性节点的处理

属性节点的处理要求把attrs:[ { name: 'id', value: 'app' }, { name: 'title', value: '标题' } ]这样的对象结构转换为{id:"app",title:"标题"}字符串,难度不大。

function generateAttrs(attrs) { 

    /* 1.初始化空字符 */
    let str = '';
    /* 2.遍历属性节点数组,并按既定格式拼接 */
    attrs.forEach((attr, idx) => {
        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }); /* 循环后:str === id:"app",title:"标题", */
    
    /* 3.拼接上外层的{},并去掉{}中最后一个逗号(,)*/
    str = `{ ${str.slice(0, -1)} }`;
    return str;
}

let attrs = [{ name: 'id', value: 'app' }, { name: 'title', value: '标题' }];
let attrsString = generateAttrs(attrs);
console.log(attrsString);  /* { id:"app",title:"标题" } */

在上面代码中封装了的generateAttrs函数,虽然能够解决标签中简单属性节点但还需要注意一种特殊的属性节点,那就是style,我们在给标签设置行内样式的时候,是可以给 style设置多个样式的,比如宽度和高度。

console.log( generateAttrs([name:"style",value:"color:red;background:#000"]));

/*执行上面的代码,得到打印结果为*/
'{ style:"color:red;background:#000" }'
/* 我想要的结果 */
'{ style:{"color":"red","background":"#000"} }`

调整generateAttrs函数的实现。

function generateAttrs(attrs) {
    /* 1.初始化空字符 */
    let str = '';
    /* 2.遍历属性节点数组,并按既定格式拼接 */
    attrs.forEach((attr, idx) => {
        /* 2.1 如果属性节点名称为 style那么则对 value进行中间处理 */
        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)},`
    }); /* 循环后:str === id:"app",title:"标题", */

    /* 3.拼接上外层的{},并去掉{}中最后一个逗号(,)*/
    str = `{ ${str.slice(0, -1)} }`;
    return str;
}
标签(子)节点的处理

标签(子)节点的处理因为涉及到标签嵌套(标签可能存在多个子标签)所以会稍显复杂。

这里我们暂且不考虑标签的属性节点,假设我们有模板字符串为<p>hello</p>,它转换之后的结果应该为_createElement("p",null,_v("hello")),这里_createElement为固定的函数名字,第一个参数p表示标签的类型(名称),第二个参数用来放置属性节点( 如果没有属性节点那么显示为 null ),第三个参数_v("hello")表示 p标签的文本内容hello,此处如果标签中的内容为类似{{msg}}的插值语法,那么还需要处理为_createElement("span",null,_v(_s(msg))))结构,做额外的处理。

那么怎么转换呢?

function generateText(node) {
    let text = JSON.stringify(node.text); 
    return `_v(${text})`;
}

console.log(generateText({ text: "hello" }));
console.log(generateText({ text: "My name is {{name}}" }));

/* 上述代码的执行结果 */
/* _v("hello") */
/* _v("My name is {{name}}") */

在上面的代码中,我封装了一个专门用来处理标签内容(字符串)的函数generateText,内部的逻辑非常简单只是字符串的无脑拼接而已。但是_v("My name is {{name}}")只能算是半成品,因为我们在真正渲染的时候,插值语法{{xx}}中的变量是需要用真正的实例数据来进行替换的,因此我们需要进一步处理为_v("My name is "+_s(name))这样的结构。那要怎么做呢?

要处理这个问题无疑是个挑战,因为当我们面对"My name is {{name}} "这样内容的时候,首先应该先把普通字符串和插值语法的部分区分开来,然后对插值语法的部分单独处理成_s(name)结构,最后再拼接。

无疑,字符串插值语法部分的匹配需要用到正则表达式,下面试着给出对应的代码。

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

function generateText(node) {
    let tokens = [];
    let match, index;

    /* 获取文本内容 */
    let text = node.text;

    /*如果是全局匹配 那么每次匹配的时候都需要将 lastIndex 调整到0*/
    let lastIndex = defaultTagRE.lastIndex = 0;

    /* 正则匹配(匹配插值语法部分的内容) */
    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)))
    }
    console.log("tokens", tokens); /* tokens [ '"My name is "', '_s(name)' ] */
    return `_v(${tokens.join('+')})`;
}

console.log(generateText({ text: "hello" }));
console.log(generateText({ text: "My name is {{name}} biubiubiu @" }));
/* 打印结果 */
/* _v("hello")  */
/* _v("My name is "+_s(name)+" biubiubiu @")*/

此外,我们还需要考虑到标签的嵌套,这个问题我们可以通过函数的递归调用来实现。

最后一步,我们还需要完成RenderString->RenderFunction,即把拼接好的字符串转换为函数,这个过程需要用到两个小技巧。我们可以通过 new Function来创建函数并将字符串转换为函数体内容,此外插值语法(如 {{name}} )中的name变量应该通过作用域绑定的方式来进行处理,因此这里还用到了with特性

下面给出整个过程的完整代码。

/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;
/* 形如:<div   匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);
/* 匹配开始标签的右半部分(>) 形如`>`或者`  >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;
/* 匹配闭合标签:形如 </div> */
const endTag = new RegExp(`^<\\/${q_nameCapture}[^>]*>`);
/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att =/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/
/* 匹配插值语法:形如 {{msg}} */
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

/* 标记节点类型(文本节点) */
let NODE_TYPE_TEXT = 3;
/* 标记节点类型(元素节点) */
let NODE_TYPE_ELEMENT = 1;

let stack = []; /* 数组模拟栈结构 */
let root = null;
let currentParent;

function compiler(html) {

    /* 推进函数:每处理完一部分模板就向前推进删除一段 */
    function advance(n) {
        html = html.substring(n);
    }

    /* 解析开始标签部分:主要提取标签名和属性节点 */
    function parser_start_html() {
        /* 00-正则匹配 <div id="app" title="标题">模板结构*/
        let start = html.match(startTagOpen);
        if (start) {

            /* 01-提取标签名称 形如 div */
            const tagInfo = {
                tag: start[1],
                attrs: []
            };

            /* 删除<div部分 */
            advance(start[0].length);

            /* 02-提取属性节点部分 形如:id="app" title="标题"*/
            let attr, end;
            while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
                tagInfo.attrs.push({
                    name: attr[1],
                    value: attr[3] || attr[4] || attr[5]
                });
                advance(attr[0].length);
            }

            /* 03-处理开始标签 形如 >*/
            if (end) {
                advance(end[0].length);
                return tagInfo;
            }
        }
    }

    while (html) {
        let textTag = html.indexOf('<');

        /* 如果以<开头 */
        if (textTag == 0) {
            /* (1) 可能是开始标签 形如:<div id="app"> */
            let startTagMatch = parser_start_html();
            if (startTagMatch) {
                start(startTagMatch.tag, startTagMatch.attrs);
                continue;
            }

            /* (2) 可能是结束标签 形如:</div>*/
            let endTagMatch = html.match(endTag);
            if (endTagMatch) {
                advance(endTagMatch[0].length);
                end(endTagMatch[1]);
                continue;
            }
        }

        /* 文本内容的处理 */
        let text;
        if (textTag >= 0) {
            text = html.substring(0, textTag);
        }
        if (text) {
            advance(text.length);
            chars(text);
        }
    }
    return root;
}

/* 文本处理函数:<span>  hello <span> => text的值为 " hello "*/
function chars(text) {
    /* 1.先处理文本字符串中所有的空格,全部替换为空 */
    text = text.replace(/\s/g, '');

    /* 2.把数据组织成{text:"hello",type:3}的形式保存为当前父节点的子元素 */
    if (text) {
        currentParent.children.push({
            text,
            nodeType: NODE_TYPE_TEXT
        })
    }
}

function start(tag, attrs) {
    let element = createASTElement(tag, attrs);
    if (!root) {
        root = element;
    }
    currentParent = element;
    stack.push(element);
}

function end(tagName) {
    let element = stack.pop();
    currentParent = stack[stack.length - 1];
    if (currentParent) {
        element.parent = currentParent;
        currentParent.children.push(element);
    }
}

function createASTElement(tag, attrs) {
    return {
        tag,
        attrs,
        children: [],
        parent: null,
        nodeType: NODE_TYPE_ELEMENT
    }
}

/* ****************** */
function generateAttrs(attrs) {
    /* 1.初始化空字符 */
    let str = '';
    /* 2.遍历属性节点数组,并按既定格式拼接 */
    attrs.forEach((attr, idx) => {
        /* 2.1 如果属性节点名称为 style那么则对 value进行中间处理 */
        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)},`
    }); /* 循环后:str === id:"app",title:"标题", */

    /* 3.拼接上外层的{},并去掉{}中最后一个逗号(,)*/
    str = `{ ${str.slice(0, -1)} }`;
    return str;
}

function generateChildren(el) {
    let children = el.children;
    return (children && children.length > 0)
     ? `${children.map(c => generate(c)).join(',')}` 
     : false;
}

function generate(node) {
    /* 如果是子标签那么就递归调用 */
    return node.nodeType == 1 ? generateRenderString(node) : generateText(node);
}

function generateText(node) {
    let tokens = [];
    let match, index;

    /* 获取文本内容 */
    let text = node.text;

    /*如果是全局匹配 那么每次匹配的时候都需要将 lastIndex 调整到0*/
    let lastIndex = defaultTagRE.lastIndex = 0;

    /* 正则匹配(匹配插值语法部分的内容) */
    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 generateRenderString(el) {
    let children = generateChildren(el);
    return `_createElement("${el.tag}",${el.attrs.length ? generateAttrs(el.attrs) : 'null'}${ children ? `,${children}` : ''})`;
}

function compilerToFunction(template) {
    /* Html->AST */
    let root = compiler(template);
    
    /* AST->RenderString */
    let renderString = generateRenderString(root);

    /* RenderString->RenderFunction */
    let renderFn = new Function(`with(this){ return ${renderString}}`);
    console.log("renderString", renderString,'renderFn', renderFn);
}

// const template = `<div><span class="span-class">Hi 夏!</span></div>`;
// const template = `<div id="app" title="标题"><p>hello</p><span>vito</span></div>`
 const template = `<a id="app" title="标题"><p>hello</p><span>My name is {{name}} dududu!!!</span></a>`;
    
compilerToFunction(template);

最后,给出上述代码的测试结果。

renderString 
    _createElement("a",
        { id:"app",title:"标题" },
        _createElement("p",null,_v("hello")),
        _createElement("span",null,_v("My name is"+_s(name)+"dududu!!!"))) 

renderFn 
    function anonymous() {
        with(this){ 
            return _createElement("a",
                { id:"app",title:"标题" },
                _createElement("p",null,_v("hello")),
                _createElement("span",null,_v("My name is"+_s(name)+"dududu!!!")))
        }
    }

标签:name,render,ast,text,value,进阶篇,let,attrs,节点
From: https://www.cnblogs.com/wendingding/p/16988902.html

相关文章

  • 前端开发系列116-进阶篇之模板引擎的实现方式
    title:前端开发系列116-进阶篇之模板引擎的实现方式tags:categories:[]date:2019-04-0523:24:08本文以ejs为例,简单介绍模板引擎的实现原理。模板引擎(ejs)的使用......
  • 前端开发系列115-进阶篇之对象和数组的读写劫持
    title:前端开发系列115-进阶篇之对象和数组的读写劫持tags:categories:[]date:2019-04-0523:00:08本文讨论如何监听对象中所有属性的读和写操作,以及对于数组的劫......
  • fastjson全局日期序列化设置导致JSONField无效
    问题描述fastjson通过代码指定全局序列化返回时间格式,导致使用JSONField注解标注属性的特殊日期返回格式失效使用版本应用名称版本springboot2.0.0.RELEASE......
  • ElasticSearch系列---【Es的快速入门文档】
    Es的快速入门文档1.对比数据库理解ElasticSearch是面向文档型数据库,一条数据在这里就是一个文档。 注意:从ElasticSearch6.X开始,一个Index下只能包含一个Type,因此,在Ela......
  • tuning+asterisk+server
    ​提高asterisk服务器并发的几个关键点。SystemHardwareOptimizationsOperatingSystemTuning NetworkStackTuning,:tuningkernaltcp/ipstack.IO......
  • 毕设之 asterisk
     1。Asterisk体系结构Asterisk系统的体系结构非常明晰,他不同于传统的PSTN交换机,更多的时候,Asterisk被当做一个连通电话的中间件,Asterisk的可扩展性使其可以应用的各个......
  • Asterisk Kernel analysis 1
    一、内核初始化。 从main入口。Asterisk.c 进入内核。  对于重启,记录上一次main函数传过来的命令, /*Rememberoriginalargsforrestart*/     if(argc>......
  • CF992E Nastya and King-Shamans 题解
    传送门分析由于满足\(a_i\ge0\),所以\(s_i\)单调不减。当我们找到一个\(i\)时,不管\(i\)是否满足,下一个可能的一定大于等于\(a_i+s_{i-1}\)。而且\(a_i+s_{i-1}......
  • 【FastDFS】分布式文件系统FastDFS
    一、参考资料​​FastDFS海量小文件存储解决之道-知乎​​​​FastDFS实战视频教程-分布式文件系统FastDFS详解-FastDFS从基础到集群实践_哔哩哔哩_bilibili​​​​芋道......
  • Elasticsearch解决问题之道——请亮出你的DSL!
    Elasticsearch最少必要知识实战教程直播回放#0、引言在业务开发中,我们往往会陷入开发的细枝末节之中,而忽略了事物的本源。经常有同学问到:1,业务代码实现结果和kibana......