underscore.js源码学习——_.template()

前言:

随着前端业务量的增长,页面的需求量越来越大。但是很多时候,页面的结构很多时候大同小异,区别在于数据量不同。因此借用mvc的思想,渲染模版和数据分离,即为前端的模版话。因此前端的模版在于,定义模版结构,和数据源。解析模版,把数据昂儒需要的地方,生成正常的html文档,然后插入到页面之中。

一句话就是:

模版+数据 ——(模版引擎) = html文档。


引擎原理:

  引擎要做的事情就是,1、识别模版数据的占位符,即哪些地方是需要放入需要的数据的 2、数据源的分类 3、将两者拼接起来。

结合着三点来看underscore.js中_.template()做的事情。

1、如何获取模版中的占位符?

     使用正则匹配:setting可以设置解析不同内容的正则表达式。

                             <%   %>中的内容是js表达式。遇到这里面的内容,则将其拼接,最终包装成function。

                             <%= %>中的内容是插入变量,这里如果不指定score(通过settings.variable来指定),则是从obj中获取。

                             <%-  %>中的内容是html文档,理论上这一段可以直接拿来用。但是为了防止xss攻击,最好的做法还是过滤输入,按照白订单中的tag重新构造一遍。

2、数据源是什么?

     通过setting.variable来指定。如果不指定,则默认从obj这个对象来获取。由于模版和数据是分离的。模版在编译的时候,并不会执行。也就是说,上一步会将模版包装在一个function里面然后返回,当调用这个函数的时候,再根据传入的数据进行解析。


3、如何将模版和数据拼装起来

    第一步已经得到了一个function,等待接收数据,然后生成模版。当调用template的时候,将接收数据作为argument,然后执行模版函数,返回一段html。然后根据需要append到需要的地方。


源码分析:

默认配置:

 _.templateSettings = {
    evaluate: /<%([\s\S]+?)%>/g, // js
    interpolate: /<%=([\s\S]+?)%>/g, // varaible
    escape: /<%-([\s\S]+?)%>/g //html
  };


step1:解析模版

if (!settings && oldSettings) settings = oldSettings;
    settings = _.defaults({}, settings, _.templateSettings); // 获取可以解析的内容。如果没有提供的话,用默认的配置

    // Combine delimiters into one regular expression via alternation.
    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g'); // 将占位符构造为正则表达式。获取可以解析的全部部分。evaluate是js,interpolate是变量,escape是html

    // Compile the template source, escaping string literals appropriately.
    var index = 0;
    var source = "__p+='"; //开始构造function 的内容。
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { // 依次读取模版的内容,然后把匹配到的内容抽取出来。
      source += text.slice(index, offset).replace(escapeRegExp, escapeChar);//需要做二次过滤,因为模版中可能有js不能执行的部分,如换行符等。
      index = offset + match.length;

      if (escape) { // 这一块 是对html做过滤。如果是escape,那么调用_.escape方法。
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      } else if (interpolate) {// 如果是变量,那么直接得到_t = interpolate
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) {// 如果是js,可以直接执行。
        source += "';\n" + evaluate + "\n__p+='";
      }

      // Adobe VMs need the match returned to produce the correct offset.
      return match;
    });
    source += "';\n"; // 分析完模版,获取可执行的source

   拼接的核心在于模拟字符串拼接。所以如果是可执行对象evaluate匹配上了,说明这部分是可执行代码,直接连接上。如果是如果是interpolate,说明这部分是变量或者模版里的html代码,则把她们包装在一个字符串__p里面。如果是html,要先做一步过滤才能包装到__p里面。

   注:看这段代码的时候replace函数出了一些问题。在这里稍微记一下replace。

          params1:正则表达式,

          params2:function(match,$!, $2, $3.., offset). match,表示匹配到的字符串, $1,$2..表示是由哪个括号匹配到的,offset是匹配项到开头的偏移量。所以在function

里面。index是从上次匹配结束的位置开始,否则会跳过模版里面的html结构。escape表示,匹配到的html部分,interpolate是匹配到的变量部分,evaluate是匹配到的js部分。然后在function里面实现字符串的拼接过程。


   二次过滤的部分:

需要获取命中部分是否有‘, \\  \r, \n, \u2028 和\u2029(行分隔符 和段落分隔符)。如果有的话,需要做一步转义

  var escapes = {
    "'": "'",
    '\\': '\\',
    '\r': 'r',
    '\n': 'n',
    '\u2028': 'u2028',
    '\u2029': 'u2029'
  };

  var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;

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

 step2: 指定数据源:

if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; // 如果没有指定varable,那么从obj中取数据
    // 否则从前面拼装一段 取arguments的过程。 在_p 前面 先获取arguments。然后再执行source
    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + 'return __p;\n';


step3: 封装函数。

前面的操作已经得到了需要执行的js 内容,存储在一个字符串中。这里要做的事情,就是把这一段string封装为function。并制定arguments。然后把封装好的function返回。

 var render;
    try {
      render = new Function(settings.variable || 'obj', '_', source); // 封装函数
    } catch (e) {
      e.source = source;
      throw e;
    }

    var template = function(data) {
      return render.call(this, data, _); //封装方法
    };

    // Provide the compiled source as a convenience for precompilation.
    var argument = settings.variable || 'obj'; // 指定arguments
    template.source = 'function(' + argument + '){\n' + source + '}'; // 把模版内容保存在source属性里面。

    return template;

  执行source还可以用eval()来执行,但是本身eval执行效率很低。先包装为一个function,再调用apply,效率会提升很多。


猜你喜欢

转载自blog.csdn.net/u013237862/article/details/68484288