1. 程式人生 > >JavaScript 模板引擎實現原理解析

JavaScript 模板引擎實現原理解析

轉自這裡:http://www.cnblogs.com/huansky/p/6073104.html

感覺講的很好,可惜看的不是很懂……

1、入門例項

首先我們來看一個簡單模板:

複製程式碼
  <script type="template" id="template">
    <h2>
      <a href="{{href}}">
        {{title}}
      </a>
    </h2>
    <img src="{{imgSrc}}" alt="{{title}}">
  </script>
複製程式碼

其中被{{ xxx }}包含的就是我們要替換的變數。
接著我們可能通過ajax或者其他方法獲得資料。這裡我們自己定義了資料,具體如下:

複製程式碼
var data = [
    {
      title: "Create a Sticky Note Effect in 5 Easy Steps with CSS3 and HTML5",
      href: "http://net.tutsplus.com/tutorials/html-css-techniques/create-a-sticky-note-effect-in-5-easy-steps-with-css3-and-html5/",
      imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/771_sticky/sticky_notes.jpg"
    },
    {
      title: 
"Nettuts+ Quiz #8", href: "http://net.tutsplus.com/articles/quizzes/nettuts-quiz-8-abbreviations-darth-sidious-edition/", imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/989_quiz2jquerybasics/quiz.jpg" } ];
複製程式碼

ok,現在的問題就是我們怎麼把資料匯入到模板裡面呢?

第一種大家會想到的就是採用replace直接替換裡面的變數:

複製程式碼
template = document.querySelector('#template').innerHTML,
result 
= document.querySelector('.result'), i = 0, len = data.length, fragment = ''; for ( ; i < len; i++ ) { fragment += template .replace( /\{\{title\}\}/, data[i].title ) .replace( /\{\{href\}\}/, data[i].href ) .replace( /\{\{imgSrc\}\}/, data[i].imgSrc ); } result.innerHTML = fragment;
複製程式碼

第二種的話,相對第一種比較靈活,採用的是正則替換,對於初級前端,很多人對正則掌握的並不是很好,一般也用的比較少。具體實現如下:

複製程式碼
template = document.querySelector('#template').innerHTML,
result = document.querySelector('.result'),
attachTemplateToData;
 
// 將模板和資料作為引數,通過資料裡所有的項將值替換到模板的標籤上(注意不是遍歷模板標籤,因為標籤可能不在資料裡存在)。
attachTemplateToData = function(template, data) {
        var i = 0,
            len = data.length,
            fragment = '';
 
        // 遍歷資料集合裡的每一個項,做相應的替換
        function replace(obj) {
            var t, key, reg;
       
       //遍歷該資料項下所有的屬性,將該屬性作為key值來查詢標籤,然後替換
            for (key in obj) {
                reg = new RegExp('{{' + key + '}}', 'ig');
                t = (t || template).replace(reg, obj[key]);
            }
 
            return t;
        }
 
        for (; i < len; i++) {
            fragment += replace(data[i]);
        }
 
        return fragment;
    };
 
result.innerHTML = attachTemplateToData(template, data);
複製程式碼

 與第一種相比較,第二種程式碼看上去多了,但是功能實則更為強大了。第一種我們需要每次重新編寫變數名,如果變數名比較多的話,會比較麻煩,且容易出錯。第二種的就沒有這些煩惱。

2、模板引擎相關知識

通過上面的例子,大家對模板引擎應該有個初步的認識了,下面我們來講解一些相關知識。

2.1 模板存放

模板一般都是放置到 textarea/input 等表單控制元件,或者 script 等標籤中。比如上面的例子,我們就是放在 script 標籤上的。

2.2 模板獲取

一般都是通過ID來獲取,document.getElementById(“ID”):

//textarea或input則取value,其它情況取innerHTML
var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;

上面的是通用的模板獲取方法,這樣不管你是放在 textarea/input 還是 script 標籤下都可以獲取到。

2.3 模板函式

一般都是templateFun("id", data);其中id為存放模板字串的元素id,data為需要裝載的資料。

2.4 模板解析編譯

模板解析主要是指將模板中 JavaScript 語句和 html 分離出來,編譯的話將模板字串編譯成最終的模板。上面的例子比較簡單,還沒有涉及到模板引擎的核心。

2.5 模板分隔符

要指出的是,不同的模板引擎所用的分隔符可能是不一樣,上面的例子用的是{{ }},而Jquery tmpl 使用的是<%  %>。

3、jQuery tmpl 實現原理解析

 jQuery tmpl是由jQuery的作者寫的,程式碼短小精悍。總共20多行,功能卻比我們上面的強大很多。我們先來看一看原始碼:

複製程式碼
(function(){
  var cache = {};
 
  this.tmpl = function tmpl(str, data){
   
    var fn = !/\W/.test(str) ? 
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
    
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +
       
        "with(obj){p.push('" +
       
        str
          .replace(/[\r\t\n]/g, " ") 
          .split("<%").join("\t") 
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")  
          .split("\t").join("');")  
          .split("%>").join("p.push('") 
          .split("\r").join("\\'")
      + "');}return p.join('');");
   
    return data ? fn( data ) : fn;
  };
})();
複製程式碼

初看是不是覺得有點懵,完全不能理解的程式碼。沒事,後面我們會對原始碼進行解釋的,我們還是先看一下所用的模板

  <ul>
    <% for ( var i = 0; i < users.length; i++ ) { %>
         <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
    <% } %>
  </ul>

可以發現,這個模板比入門例子的模板更為複雜,因為裡面還夾雜著 JavaScript 程式碼。JavaScript 程式碼採用 <%  %> 包含。而要替換的變數則是用 <%=   %> 分隔開的。

下面我再來對程式碼做個註釋。不過即使看了註釋,你也不一定能很快理解,最好的辦法是自己實際動手操作一遍。

複製程式碼
// 程式碼整個放在一個立即執行函式裡面
(function(){
  // 用來快取,有時候一個模板要用多次,這時候,我們直接用快取就會很方便
  var cache = {};
  
  // tmpl繫結在this上,這裡的this值得是window
  this.tmpl = function tmpl(str, data){
   
    // 只有模板才有非字母數字字元,用來判斷傳入的是模板id還是模板字串,
    // 如果是id的話,判斷是否有快取,沒有快取的話呼叫tmpl;
    // 如果是模板的話,就呼叫new Function()解析編譯
    var fn = !/\W/.test(str) ? 
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
    
      new Function("obj",
     // 注意這裡整個是字串,通過 + 號拼接
        "var p=[],print=function(){p.push.apply(p,arguments);};" +
        "with(obj){p.push('" +
       
        str
      // 去除換行製表符\t\n\r
          .replace(/[\r\t\n]/g, " ") 
      
      // 將左分隔符變成 \t
          .split("<%").join("\t") 
      
      // 去掉模板中單引號的干擾
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
      
      // 為 html 中的變數變成 ",xxx," 的形式, 如:\t=users[i].url%> 變成  'users[i].url,'  
      // 注意這裡只有一個單引號,還不配對 
          .replace(/\t=(.*?)%>/g, "',$1,'")  
      
      // 這時候,只有JavaScript 語句前面才有 "\t",  將  \t  變成   '); 
      // 這樣就可把 html 標籤新增到陣列p中,而javascript 語句 不需要 push 到裡面。
      .split("\t").join("');") 
      
      // 這時候,只有JavaScript 語句後面才有 "%>", 將 %> 變成  p.push(' 
      // 上一步我們再 html 標籤後加了 ');, 所以要把 p.push(' 語句放在 html 標籤放在前面,這樣就可以變成 JavaScript 語句
          .split("%>").join("p.push('") 
         
      // 將上面可能出現的干擾的單引號進行轉義
       .split("\r").join("\\'")
    // 將陣列 p 變成字串。
      + "');}return p.join('');");
   
    return data ? fn( data ) : fn;
  };
})();
複製程式碼

上面程式碼中,有一個要指出的就是new Function 的使用 方法。給 new Function() 傳一個字串作為函式的body來構造一個 JavaScript函式。程式設計中並不經常用到,但有時候應該是很有用的。

下面是 new Function 的基本用法:

// 最後一個引數是函式的 body(函式體),型別為 string; 
// 前面的引數都是 索要構造的函式的引數(名字) 
var myFunction = new Function('users', 'salary', 'return users * salary'); 

最後的字串就是下面這種形式:

複製程式碼
  var p = [],
    print = function() {
      p.push.apply(p, arguments);
    };
  with(obj) {
    p.push('     <ul>     ');
    for (var i = 0; i < users.length; i++) {
      p.push('          <li><a href="', users[i].url, '">', users[i].name, '</a></li>     ');
    }
    p.push('   </ul> ');
  }
  return p.join('');
複製程式碼

裡面的 print 函式 在我們的模板裡面是沒有用到的。

要指出的是,採用 push 的方法在 IE6-8 的瀏覽器下會比 += 的形式快,但是在現在的瀏覽器裡面, += 是拼接字串最快的方法。實測表明現代瀏覽器使用 += 會比陣列 push 方法快,而在 v8 引擎中,使用 += 方式比陣列拼接快 4.7 倍。所以 目前有些更高階的模板引擎會 根據 javascript 引擎特性採用了兩種不同的字串拼接方式。

下面的程式碼是摘自騰訊的 artTemplate 的, 根據瀏覽器的型別來選擇不同的拼接方式。功能越強大,所考慮的問題也會更多。

    var isNewEngine = ''.trim;// '__proto__' in {}
    var replaces = isNewEngine
    ? ["$out='';", "$out+=", ";", "$out"]
    : ["$out=[];", "$out.push(", ");", "$out.join('')"];

挑戰:有興趣的可以改用 += 來實現上面的程式碼。


總結

模板引擎原理總結起來就是:先獲取html中對應的id下得innerHTML,利用開始標籤和關閉標籤進行字串切分,其實是將模板劃分成兩部份內容,一部分是html部分,一部分是邏輯部分,通過區別一些特殊符號比如each、if等來將字串拼接成函式式的字串,將兩部分各自經過處理後,再次拼接到一起,最後將拼接好的字串採用new Function()的方式轉化成所需要的函式。

目前模板引擎的種類繁多,功能也越來越強大,不同模板間實現原理大同小異,各有優缺,請按需選擇。