专栏名称: 美团技术团队
10000+工程师,如何支撑中国领先的生活服务电子商务平台?数亿消费者、数百万商户、2000多个行业、几千亿交易额背后是哪些技术在支撑?这里是美团、大众点评、美团外卖、美团配送、美团优选等技术团队的对外窗口。
目录
相关文章推荐
51好读  ›  专栏  ›  美团技术团队

前端渲染引擎doT.js解析

美团技术团队  · 公众号  · 架构  · 2017-03-24 19:00

正文

请到「今天看啥」查看全文



正则替换是doT.js的核心设计思路,本文不对正则表达式做扩充讲解,仅分析doT.js的设计思路。先来看一下doT.js中用到的正则:

templateSettings: {
   evaluate:    /\{\{([\s\S]+?(\}?)+)\}\}/g, //表达式
   interpolate: /\{\{=([\s\S]+?)\}\}/g, // 插入的变量
   encode:      /\{\{!([\s\S]+?)\}\}/g, // 在这里{{!不是用来做判断,而是对里面的代码做编码
   use:         /\{\{#([\s\S]+?)\}\}/g,
   useParams:   /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
   define:      /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,// 自定义模式
   defineParams:/^\s*([\w$]+):([\s\S]+)/, // 自定义参数
   conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, // 条件判断
   iterate:     /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g, // 遍历
   varname:   "it", // 默认变量名
   strip:    true,
   append:       true,
   selfcontained: false,
   doNotSkipEncoded: false // 是否跳过一些特殊字符
}


源码中将正则定义写到一起,这样方便了维护和管理。在早期版本的doT.js中,处理条件表达式的方式和tmpl一样,采用直接替换成可执行语句的形式,在最新版本的doT.js中,修改成仅一条正则就可以实现替换,变得更加简洁。


doT.js源码中对模板中语法正则替换的流程如下:



代码解析重点2:new Function()运用


函数定义时,一般通过Function关键字,并指定一个函数名,用以调用。在JavaScript中,函数也是对象,可以通过函数对象(Function Object)来创建。正如数组对象对应的类型是Array,日期对象对应的类型是Date一样,如下所示:

var funcName = new Function(p1,p2,...,pn,body);


参数的数据类型都是字符串,p1到pn表示所创建函数的参数名称列表,body表示所创建函数的函数体语句,funcName就是所创建函数的名称(可以不指定任何参数创建一个匿名函数)。


下面的定义是等价的。


例如:

// 一般函数定义方式
function func1(a,b){
    return a+b;
}
// 参数是一个字符串通过逗号分隔
var func2 = new Function('a,b','return a+b');
// 参数是多个字符串
var func3 = new Function('a','b','return a+b');
// 一样的调用方式
console.log(func1(1,2));
console.log(func2(2,3));
console.log(func3(1,3));
// 输出
3 // func1
5 // func2
4 // func3


从上面的代码中可以看出,Function的最后一个参数,被转换为可执行代码,类似eval的功能。eval执行时存在浏览器性能下降、调试困难以及可能引发XSS(跨站)攻击等问题,因此不推荐使用eval执行字符串代码,new Function()恰好解决了这个问题。回过头来看doT代码中的"new Function(c.varname, str)",就不难理解varname是传入可执行字符串str的变量。


具体关于new Fcuntion的定义和用法,详细请阅读 Function详细介绍


性能之因


读到这里可能会产生一个疑问:doT.js的性能为什么在众多引擎如此突出?通过阅读其他引擎源代码,发现了它们核心代码段中都存在这样那样的问题。


jQuery-tmpl


function buildTmplFn( markup ) {
        return new Function("jQuery","$item",
            // Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
            "var $=jQuery,call,__=[],$data=$item.data;" +

            // Introduce the data as local variables using with(){}
            "with($data){__.push('" +

            // Convert the template into pure JavaScript
            jQuery.trim(markup)
                .replace( /([\\'])/g, "\\$1" )
                .replace( /[\r\t\n]/g, " " )
                .replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
                .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
                function( all, slash, type, fnargs, target, parens, args ) {
                    //省略部分模板替换语句,若要阅读全部代码请访问:https://github.com/BorisMoore/jquery-tmpl
                }) +
            "');}return __;"
        );
    }


在上面的代码中看到,jQuery-teml同样使用了new Function()的方式编译模板,但是在性能对比中jQuery-teml性能相比doT.js相差甚远,出现性能瓶颈的关键在于with语句的使用。


with语句为什么对性能有这么大的影响?我们来看下面的代码:

var datas = {persons:['李明','小红','赵四','王五','张三','孙行者','马婆子'],gifts:['平民','巫师','狼','猎人','先知']};
function go(){
    with(datas){
        var personIndex = 0,giftIndex = 0,i=100000;
        while(i){
            personIndex = Math.floor(Math.random()*persons.length);
            giftIndex = Math.floor(Math.random()*gifts.length)
            console.log(persons[personIndex] +'得到了新的身份:'+ gifts[giftIndex]);
            i--;
        }
    }
}


上面代码中使用了一个with表达式,为了避免多次从datas中取变量而使用了with语句。这看起来似乎提升了效率,但却产生了一个性能问题:在JavaScript中执行方法时会产生一个执行上下文,这个执行上下文持有该方法作用域链,主要用于标识符解析。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了,一个新的可变对象将被创建,它包含指定对象的所有属性。此对象被插入到作用域链的最前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,这样访问datas的属性非常快,但是访问局部变量的速度却变慢了,所以访问代价更高了,如下图所示。







请到「今天看啥」查看全文