模板编译 前端
art-template为什么快

前言

1. 模板引擎是更方便拼接字符串,对段代码更清晰,维护更容易

2. art有cache:render过程,对同一份模板二次渲染可以利用缓存下来的函数直接执行

腾讯:高性能javascript模板引擎原理解析

上述分析里,【预编译】和【+=替代push】是快的其他原因

为什么是接近js性能呢? 可以理解为只要通过编译一次的代价就=js手写代码了,而不是当时的每次渲染都需要替换整个模板

以下是调试简单分析过程:

用例

分析demo: 码云

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>art-template为什么快</title>
    <script src="./template-web.js"></script>
</head>

<body>
    <div id="wrap"></div>
    <script type="text/html" id="tpl">
        {{if user}}
        <div>{{ user.name }}</div>
        {{/if}}
    </script>   
    <button id="redo">rerender</button>
    <script>
        // 基于art-template@4.13.2
        const user = {
            name: '你的名字'
        }
        const str = template('tpl', {user})
        document.querySelector('#wrap').innerHTML = str
        document.querySelector('#redo').addEventListener('click', ()=>{
            user.name = '我的名字'
            const str = template('tpl', {user})
            document.querySelector('#wrap').innerHTML = str
        })
    </script>
</body>
</html>

过程

1. 模板引擎内部维护了一个缓存,cache了对应模板的rende函数,如果发现已经缓存过,则直接使用缓存

if (cache && filename) {
    var _render = caches.get(filename);
    if (_render) {
       return _render;
    }
}// 匹配缓存

2. document.getElementById读取模板字符串(很可能多个换行),可以看到script:type=text/html是为了浏览器更好隐藏且不执行(nodejs中为读取文件内容:fs.readFileSync)

var loader = function loader(filename /*, options*/) {
    /* istanbul ignore else  */
    if (detectNode) {
        var fs = __webpack_require__(3);
        return fs.readFileSync(filename, 'utf8');
    } else {
        var elem = document.getElementById(filename);
        return elem.value || elem.innerHTML;
    }
};

3. html-minifier 出名的压缩,webpack默认也是这个库?    

var htmlMinifier = function htmlMinifier(source, options) {
    if (isNodejs) { // art这里设计为仅在node端生效,可能是为了省流?
        // ...
        source = _htmlMinifier(source, htmlMinifierOptions);
    }
    return source;
};

4. Compiler重头戏:

4.1  /{{([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*}}/ vs {{([\w\W]*?)}}  一开始没看出有什么不同?,调试到后面才发现,是想匹配出来组:[是否@关键字,是否有\if结束 ]

var artRule = {
    test: /{{([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*}}/,
    use: function use(match, raw, close, code) {} // raw就是关键字,close就是/if里的/
}

4.2 词法分析里逐行分析,把所有情况分类,比如表达式:

第一行:

line1-1: {{if user}} 拿表达式出来: if user

line1-2: 词法分析为[if, ' ', user]

line1-3: [{type: 'name', value: 'if'}, {t:'string'|'whitespace'?}, {t: 'name'}] => [{t: 命中关键字,变keyword},..,..]

line1-4: if命中了, 抽出后者所有,包装成code:`if (user) {`

---

第二行:

line2-1:{{user.name}} 拿表达式出来: user.name  

line-2-2:词法分析为[user, ., name]

line-2-3: [{type: 'name', value: 'user'}, {t:"punctuator"}, {t: 'name'}]

line-2-4:  啥也不是,包装成code:`user.name`

第三行                                    

词法分析为[/if] 是结束的意思

[{type: 'keyword', value: 'if'}]

是结束意思,包装成code:`}`

--

全部匹配完,得到tokens集合:

4.3: tokens组合成codes过程:

    var esToken = this.getEsTokens(code);
    this.getVariables(esToken).forEach(function (name) {
        return _this3.importContext(name);
    });

第一行: 直接输出code: $$out+='   \n'

第二行:

把【code:`if (user) {`】解析为[{"type":"keyword","value":"if"},{"type":"punctuator","value":"("},{"type":"name","value":"user"},{"type":"punctuator","value":")"},{"type":"punctuator","value":"{"}]

获取变量列表: 即便有user.name也取user(遇到.停止filter)

 function getVariables(esTokens) {
            var ignore = false;
            return esTokens.filter(function (esToken) {
                return esToken.type !== 'whitespace' && esToken.type !== 'comment';
            }).filter(function (esToken) {
                if (esToken.type === 'name' && !ignore) {
                    return true;
                }

                ignore = esToken.type === 'punctuator' && esToken.value === '.'; //遇到.停止

                return false;
            }).map(function (tooken) {
                return tooken.value;
            });
}

注入上下文:contextMap[user] = "$data.user"

第三行:检测有output,是escape: 输出code: "$$out+=$escape(user.name)"

把【code: "$$out+=$escape(user.name)"】解析为

'[{"type":"name","value":"$$out"},{"type":"punctuator","value":"+="},{"type":"name","value":"$escape"},{"type":"punctuator","value":"("},{"type":"name","value":"user"},{"type":"punctuator","value":"."},{"type":"name","value":"name"},{"type":"punctuator","value":")"}]':

获取变量列表:`[{"name":"$$out","value":"''"},{"name":"user","value":"$data.user"},{"name":"$escape","value":"$imports.$escape"}]`

注入上下文:$$out 和$escape(user前面注入过了 可以不注入)

4.4 tokens和codes组合成script, script生成render

4.4.1 stacks.push('function(' + DATA + '){'); 首行注入数据(模拟填充)

4.4.2 上下文变更:name=$data.name,遇到compileDebug则最外层包一层trycatch

4.4.3 $out是输出用的,其余都是变量,最后加上一行: return $out

4.4.4 最后用\n拼起来 fn=new Function

4.5:用render闭包保住fn,用caches.set('tpl', render):

function render(data, blocks) {
    try {
        return fn(data, blocks);
    } catch (error) {}
}
// ..
if (cache && filename) {
    caches.set(filename, render);
}

5: render执行,输出$out

6:点击更换数据,发现4.5步已经缓存过此模板的render了,因此返回到1步

快!


总结:

对比vue的模板编译,art的render函数生成很类似:都通过词法分析生成,通过new Function注入js内存=手写函数(不过当前技术已经更多用的构建工具如webpack来提前生成render而不是运行时(浏览器执行)生成)

不同的是一个是简单的正则提取+预注入上下文和变量;一个是通过ast提取指令,dom等为后续diff,静态提取等做优化

产物也不同:art是输出string,vue是输出dom。

这里可以理解为目标不同,art是旨在渲染dom,下游即是用户了;vue是输出vdom,为后来的diff对比以及需要响应绑定如执行watch等提供上游

最后回答为什么快?1. 有函数维度的缓存  2. 字符串处理+=比数组push更好

日期:2024-01-07 21:24 | 阅读:83 | 评论:0