Underscore.js template()函数全解析

模板解析引擎有很多,比如mustache.js等。在我们解析从服务端获取的JSON数据的时候使用模板有很大的便利。下面我们分析一下Underscore.js的template()模板渲染函数。

官网API介绍


将 JavaScript 模板编译为可以用于页面呈现的函数, 对于通过JSON数据源生成复杂的HTML并呈现出来的操作非常有用。 模板函数可以使用 <%= … %>插入变量, 也可以用<% … %>执行任意的 JavaScript 代码。 如果您希望插入一个值, 并让其进行HTML转义,请使用<%- … %>。 当你要给模板函数赋值的时候,可以传递一个含有与模板对应属性的data对象 。 如果您要写一个一次性的, 您可以传对象 data 作为第二个参数给模板 template来直接呈现, 这样页面会立即呈现而不是返回一个模板函数. 参数 settings 是一个哈希表包含任何可以覆盖的设置 _.templateSettings.


var compiled = _.template("hello: <%= name %>");
compiled({name: 'moe'});
=> "hello: moe"


var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>";
_.template(list, {people: ['moe', 'curly', 'larry']});
=> "<li>moe</li><li>curly</li><li>larry</li>"


var template = _.template("<b><%- value %></b>");
template({value: '<script>'});
=> "<b>&lt;script&gt;</b>"
您也可以在JavaScript代码中使用 print. 有时候这会比使用 <%= ... %> 更方便.


var compiled = _.template("<% print('Hello ' + epithet); %>");
compiled({epithet: "stooge"});
=> "Hello stooge"
如果ERB式的分隔符您不喜欢, 您可以改变Underscore的模板设置, 使用别的符号来嵌入代码. 定义一个 interpolate 正则表达式来逐字匹配 嵌入代码的语句, 如果想插入转义后的HTML代码 则需要定义一个 escape 正则表达式来匹配, 还有一个 evaluate 正则表达式来匹配 您想要直接一次性执行程序而不需要任何返回值的语句. 您可以定义或省略这三个的任意一个. 例如, 要执行 Mustache.js 类型的模板:


_.templateSettings = {
  interpolate: /\{\{(.+?)\}\}/g
};


var template = _.template("Hello {{ name }}!");
template({name: "Mustache"});
=> "Hello Mustache!"
默认的, template 通过 with 语句 来取得 data 所有的值. 当然, 您也可以在 variable 设置里指定一个变量名. 这样能显著提升模板的渲染速度.


_.template("Using 'with': <%= data.answer %>", {answer: 'no'}, {variable: 'data'});
=> "Using 'with': no"
预编译模板对调试不可重现的错误很有帮助. 这是因为预编译的模板可以提供错误的代码行号和堆栈跟踪, 有些模板在客户端(浏览器)上是不能通过编译的 在编译好的模板函数上, 有 source 属性可以提供简单的预编译功能.


<script>
  JST.project = <%= _.template(jstText).source %>;
</script>


源码解读

我对源码进行了详细的注释,也搞明白了其中的逻辑。原理比较简单,大家可细读以下代码。
// By default, Underscore uses ERB-style template delimiters, change the
  // following template settings to use alternative delimiters.
  // Underscore默认使用标签格式的模板分隔符,改变下面的模板设置项可以使用你自己设置的模板分隔符。
  _.templateSettings = {
    evaluate    : /<%([\s\S]+?)%>/g,
    interpolate : /<%=([\s\S]+?)%>/g,
    escape      : /<%-([\s\S]+?)%>/g
  };

  // When customizing `templateSettings`, if you don't want to define an
  // interpolation, evaluation or escaping regex, we need one that is
  // guaranteed not to match.
  // 当定制了'templateSettings'设置项后,如果你不想定义interpolation,evaluation或者escaping的正则,
  // 我们需要一个保证在某个属性项(evaluate,interpolate,escape)没有的情况下的正则。
  var noMatch = /(.)^/;

  // Certain characters need to be escaped so that they can be put into a string literal.
  // 对特定字符进行转码(前面会加上"\"),这样放在Function的字符串字面量的函数体中才能正常运行(类似于正则中我们想要对\符号的匹配一样)
  var escapes = {
    "'":      "'",
    '\\':     '\\',
    '\r':     'r',
    '\n':     'n',
    '\u2028': 'u2028',
    '\u2029': 'u2029'
  };
  // 获取escapes属性部分的匹配的正则
  var escaper = /\\|'|\r|\n|\u2028|\u2029/g;

  var escapeChar = function(match) {
    return '\\' + escapes[match];
  };

  // JavaScript micro-templating, similar to John Resig's implementation.
  // Underscore templating handles arbitrary delimiters, preserves whitespace,
  // and correctly escapes quotes within interpolated code.
  // NB: `oldSettings` only exists for backwards compatibility.
  // JavaScript mini模板引擎,类似于John Resig的实现。Underscore的模板可以处理任意的定界符,保留空格,并且可以在插入的代码里正确的转义引号。
  // 注意:'oldSetting'的存在只是为了向后兼容。
  // Underscore模板解析流程:
  // 1、准备要对整个字符串进行匹配的正则表达式;
  // 2、组装要执行的函数体主要部分(source变量,通过对整个模板进行正则匹配来实现);
  // 3、组装整个函数体执行部分;
  // 4、使用Function实例化出一个生成最终字符串的函数(对该函数传入要渲染的参数即可获得最终渲染字符串);
  // 5、提供预编译的source参数,方便调试与错误追踪
  _.template = function(text, settings, oldSettings) {
    if (!settings && oldSettings) settings = oldSettings;
	// 使用defaults方法来给settings参数赋默认值(如果evaluate、interpolate、escape任一属性有值则不做覆盖)
    settings = _.defaults({}, settings, _.templateSettings);
	
    // Combine delimiters into one regular expression via alternation.
	// 将界定符组合成一个正则
	// 用户如果没有设置界定符则以下正则是:/<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

    // Compile the template source, escaping string literals appropriately.
    var index = 0;
	// 记录编译成的函数字符串,可通过_.template(tpl).source获取
    var source = "__p+='";
	/**
		replace()函数的各项参数意义:
		1、第一个参数为每次匹配的全文本($&)。
		2、中间参数为子表达式匹配字符串,也就是括号中的东西,个数不限
		3、倒数第二个参数为匹配文本字符串的匹配下标位置。
		4、最后一个参数表示字符串本身。
	*/
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
	  // 将要编译的模板中正则匹配到非分解符部分的加到source上面去,这里做了字符串转义处理
      source += text.slice(index, offset).replace(escaper, escapeChar);
	  // 将index跳至当前匹配分解符的结束的地方
      index = offset + match.length;
	  // 界定符内匹配到的内容(TODO:进一步解释)
      if (escape) {
		// 需要转码的字符串部分的处理
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      } else if (interpolate) {
		// 对象属性部分的处理
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) {
		// 代码执行部分的处理
        source += "';\n" + evaluate + "\n__p+='";
      }

      // Adobe VMs need the match returned to produce the correct offest.
	  // 将匹配到的内容原样返回(Adobe VMs需要返回match来使得offset能够正常,一般网页并不需要)
      return match;
    });
    source += "';\n";
	

    // If a variable is not specified, place data values in local scope.
	// 如果没有在第二个参数里指定variable变量,那么将数据值置于局部变量中执行
    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
	
	// 将组装好的source重新组装,成为真正可以执行的js代码字符串。(print相当于等号,但是比=号要方便)
	// Array.prototype.join.call(arguments,'');是将所有的参数(如果是对象则调用toString()方法转化为字符串)以''合并在一起
    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + 'return __p;\n';
	  
	// 防止在没有传settings.variable作为with的作用域的时候,render函数的第一个参数名字为obj(此时render函数格式:function(obj,_) {source}),
	// obj为在没有传递setting.variable的时候source代码的作用域
    try {
	  // underscore的根对象也作为一个变量传入了函数
	  // Function传参:前面是执行函数时的参数,最后是执行函数体字符串字面量
      var render = new Function(settings.variable || 'obj', '_', source);
    } catch (e) {
      e.source = source;
      throw e;
    }
	// 传进去的data相当于obj
    var template = function(data) {
	  // this一般都是指向window
      return render.call(this, data, _);
    };

    // Provide the compiled source as a convenience for precompilation.
	// 提供编译的source,方便预编译(据官方文档,这么做可以对错误进行跟踪定位)
    var argument = settings.variable || 'obj';
    template.source = 'function(' + argument + '){\n' + source + '}';
	// 将函数返回(对函数传入要渲染的数据即可获得最终渲染字符串)
    return template;
  };


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值