日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

前端渲染引擎doT.js解析

發布時間:2024/7/5 HTML 50 豆豆
生活随笔 收集整理的這篇文章主要介紹了 前端渲染引擎doT.js解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

前端渲染有很多框架,而且形式和內容在不斷發生變化。這些演變的背后是設計模式的變化,而歸根到底是功能劃分邏輯的演變:MVC—>MVP—>MVVM(忽略最早混在一起的寫法,那不稱為模式)。近幾年興起的React、Vue、Angular等框架都屬于MVVM模式,能幫我們實現界面渲染、事件綁定、路由分發等復雜功能。但在一些只需完成數據和模板簡單渲染的場合,它們就顯得笨重而且學習成本較高了。

例如,在美團外賣的開發實踐中,前端經常從后端接口取得長串的數據,這些數據擁有相同的樣式模板,前端需要將這些數據在同一個樣式模板上做重復渲染操作。

解決這個問題的模板引擎有很多,doT.js(出自女程序員Laura Doktorova之手)是其中非常優秀的一個。下表將doT.js與其他同類引擎做了對比:

| 框架 | 大小 | 壓縮版本大小 | 迭代 | 條件表達式 | 自定義語法 | | ———— | ————- | ———— | ———— | | doT.js | 6KB | 4KB | ? | ? | ? | | mustache | 18.9 KB | 9.3 KB | ? | ? | ? | | Handlebars | 512KB | 62.3KB | ? | ? | ? | | artTemplate(騰訊) | - | 5.2KB | ? | ?| ? | | BaiduTemplate(百度) | 9.45KB | 6KB | ? | ? | ? | | jQuery-tmpl | 18.6KB | 5.98KB | ? | ? | ? |

可以看出,doT.js表現突出。而且,它的性能也很優秀,本人在Mac Pro上的用Chrome瀏覽器(版本為:56.0.2924.87)上做100條數據10000次渲染性能測試,結果如下:

從上可以看出doT.js更值得推薦,它的主要優勢在于: 1. 小巧精簡,源代碼不超過兩百行,6KB的大小,壓縮版只有4KB; 2. 支持表達式豐富,涵蓋幾乎所有應用場景的表達式語句; 3. 性能優秀; 4. 不依賴第三方庫。

本文主要對doT.js的源碼進行分析,探究一下這類模板引擎的實現原理。

如何使用

如果之前用過doT.js,可以跳過此小節,doT.js使用示例如下:

<script type="text/html" id="tpl"><div><a>name:{{= it.name}}</a><p>age:{{= it.age}}</p><p>hello:{{= it.sayHello() }}</p><select>{{~ it.arr:item}}<option {{?item.id == it.stringParams2}}selected{{?}} value="{{=item.id}}">{{=item.text}}</option>{{~}}</select></div> </script> <script>$("#app").html(doT.template($("#tpl").html())({name:'stringParams1',stringParams1:'stringParams1_value',stringParams2:1,arr:[{id:0,text:'val1'},{id:1,text:'val2'}],sayHello:function () {return this[this.name]}})); </script>

可以看出doT.js的設計思路:將數據注入到預置的視圖模板中渲染,返回HTML代碼段,從而得到最終視圖。

下面是一些常用語法表達式對照表:

項目JavaScript語法對應語法案例
輸出變量={{= 變量名}}{{=it.name }}
條件判斷if{{? 條件表達式}}{{? i > 3}}
條件轉折else/else if{{??}}/{{?? 表達式}}{{?? i ==2}}
循環遍歷for{{~ 循環變量}}{{~ it.arr:item}}…{{~}}
執行方法funcName(){{= funcName() }}{{= it.sayHello() }}

源碼分析及實現原理

和后端渲染不同,doT.js的渲染完全交由前端來進行,這樣做主要有以下好處:

  • 脫離后端渲染語言,不需要依賴后端項目的啟動,從而降低了開發耦合度、提升開發效率;
  • View層渲染邏輯全在JavaScript層實現,容易維護和修改;
  • 數據通過接口得到,無需考慮后端數據模型變化,只需關心數據格式。
  • doT.js源碼核心:

    ... // 去掉所有制表符、空格、換行 str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ").replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str).replace(/'|\\/g, "\\$&").replace(c.interpolate || skip, function(m, code) {return cse.start + unescape(code,c.canReturnNull) + cse.end;}).replace(c.encode || skip, function(m, code) {needhtmlencode = true;return cse.startencode + unescape(code,c.canReturnNull) + cse.end;})// 條件判斷正則匹配,包括if和else判斷.replace(c.conditional || skip, function(m, elsecase, code) {return elsecase ?(code ? "';}else if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}else{out+='") :(code ? "';if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}out+='");})// 循環遍歷正則匹配.replace(c.iterate || skip, function(m, iterate, vname, iname) {if (!iterate) return "';} } out+='";sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"+vname+"=arr"+sid+"["+indv+"+=1];out+='";})// 可執行代碼匹配.replace(c.evaluate || skip, function(m, code) {return "';" + unescape(code,c.canReturnNull) + "out+='";})+ "';return out;")...try {return new Function(c.varname, str);//c.varname 定義的是new Function()返回的函數的參數名} catch (e) {/* istanbul ignore else */if (typeof console !== "undefined") console.log("Could not create a template function: " + str);throw e;} ...

    這段代碼總結起來就是一句話:用正則表達式匹配預置模板中的語法規則,將其轉換、拼接為可執行HTML代碼,作為可執行語句,通過new Function()創建的新方法返回。

    代碼解析重點1:正則替換

    正則替換是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 JavaScriptjQuery.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的屬性非常快,但是訪問局部變量的速度卻變慢了,所以訪問代價更高了,如下圖所示。

    這個插件在GitHub上面介紹時,作者Boris Moore著重強調兩點設計思路:

  • 模板緩存,在模板重復使用時,直接使用內存中緩存的模板。在本文作者看來,這是一個雞肋的功能,在實際使用中,無論是直接寫在String中的模板還是從Dom獲取的模板都會以變量的形式存放在內存中,變量使用得當,在頁面整個生命周期內都能取到這個模板。通過源碼分析之后發現jQuery-tmpl的模板緩存并不是對模板編譯結果進行緩存,并且會造成多次執行渲染時產生多次編譯,再加上代碼with性能消耗,嚴重拖慢整個渲染過程。
  • 模板標記,可以從緩存模板中取出對應子節點。這是一個不錯的設計思路,可以實現數據改變只重新渲染局部界面的功能。但是我覺得:模板將渲染結果交給開發者,并渲染到界面指定位置之后,模板引擎的工作就應該結束了,剩下的對節點操作應該靈活的掌握在開發者手上。
  • 不改變原來設計思路基礎之上,嘗試對源代碼進行性能提升。

    先保留提升前性能作為對比:

    首先來我們做第一次性能提升,移除源碼中with語句。

    第一次提升后:

    接下來第二部提升,落實Boris Moore設計理念中的模板緩存:

    優化后的這一部分代碼段被我們修改成了:

    function buildTmplFn( markup ) {if(!compledStr){// Convert the template into pure JavaScriptcompledStr = jQuery.trim(markup).replace( /([\\'])/g, "\\$1" ).replace( /[\r\t\n]/g, " " ).replace( /\$\{([^\}]*)\}/g, "{{= $1}}" ).replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,//省略部分模板替換語句}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(){}"__.push('" + compledStr +"');return __;") }

    在doT.js源碼中沒有用到with這類消耗性能的語句,與此同時doT.js選擇先將模板編譯結果返回給開發者,這樣如要重復多次使用同一模板進行渲染便不會反復編譯。

    僅25行的模板:tmpl

    (function(){var cache = {};this.tmpl = function (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;}; })();

    閱讀這段代碼會驚奇的發現,它更像是baiduTemplate精簡版。相比baiduTemplate而言,它移除了baiduTemplate的自定義語法標簽的功能,使得代碼更加精簡,也避開了替換用戶語法標簽而帶來的性能消耗。對于doT.js來說,性能問題的關鍵是with語句。

    綜合上述我對tmpl的源碼進行移除with語句改造:

    改造之前性能:

    改造之后性能:

    如果讀者對性能對比源碼比較感興趣可以訪問 https://github.com/chen2009277025/TemplateTest 。

    總結

    通過對doT.js源碼的解讀,我們發現:

  • doT.js的條件判斷語法標簽不直觀。當開發者在使用過程中條件判斷嵌套過多時,很難找到對應的結束語法符號,開發者需要自己嚴格規范代碼書寫,否則會給開發和維護帶來困難。
  • doT.js限制開發者自定義語法標簽,相比較之下baiduTemplate提供可自定義標簽的功能,而baiduTemplate的性能瓶頸恰好是提供自定義語法標簽的功能。
  • 很多解決我們問題的插件的代碼往往簡單明了,那些龐大的插件反而存在負面影響或無用功能。技術領域有一個軟件設計范式:“約定大于配置”,旨在減少軟件開發人員需要做決定的數量,做到簡單而又不失靈活。在插件編寫過程中開發者應多注意使用場景和性能的有機結合,使用恰當的語法,盡可能減少開發者的配置,不求迎合各個場景。

    作者簡介

    • 建輝,美團外賣高級前端研發工程師,2015年加入美團外賣事業部。目前在前端業務增長組,主要負責運營平臺搭建,主導運營活動業務。

    總結

    以上是生活随笔為你收集整理的前端渲染引擎doT.js解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。