Version: 0.6.9-stablehtml
Date: 8th of Aug, 2015node
我的能力有限,若有分析不當的地方,懇請指正!git
方法與參數github
參數配置方法是 juicer.set
,該方法接受兩個參數或一個參數:api
當傳入兩個參數時,如 juicer.set('cache',false)
,便是設置 cache
爲 false
數組
當傳入一個參數時,該參數應爲一個對象,如 juicer.set({cache:false})
,系統將遍歷這個對象的屬性來設值瀏覽器
能夠配置的內容緩存
咱們能夠配置一些參數選項,包括 cache
、strip
、errorhandling
、detection
;其默認值都是true
;咱們還能夠修改模板的語法邊界符,如 tag::operationOpen
等。具體可配置的項能夠參看其源代碼。閉包
工做原理app
juicer.options = { // 是否緩存模板編譯結果 cache: true, // 是否清除空白 strip: true, // 是否處理錯誤 errorhandling: true, // 是否檢測變量是否認義 detection: true, // 自定義函數庫 _method: __creator({ __escapehtml: __escapehtml, __throw: __throw, __juicer: juicer }, {}) };
選項解析以下:
cache
是否緩存編譯結果(引擎對象)。緩存的結果存於 juicer.__cache
strip
是否清除模板中的空白,包括換行、回車等errorhandling
是否處理錯誤detection
開啓後,若是變量未定義,將用空白字符串代替變量位置,不然照常輸出,因此若是關閉此項,有可能形成輸出 undefined
_method
存儲的是用戶註冊的自定義函數,系統內部建立的自定義函數或對象有 __escapehtml
處理HTML轉義、__throw
拋出錯誤、__juicer
引用 juicer
。__creator
方法本文最末講解在 Node.js 環境中,cache
默認值是 false
,請看下面代碼
if(typeof(global) !== 'undefined' && typeof(window) === 'undefined') { juicer.set('cache', false); }
這段代碼在結尾處能夠找到。
此外,還有一個屬性是 juicer.options.loose
,默認值爲 undefined
(沒有設置),當其值不爲 false
(此亦系統默認)時,將對 {@each}
、{@if}
、{@else if}
、${}
、{@include}
等中的變量名和自定義函數名進行校驗,給其中使用到的變量、函數定義並添加到模板的開頭,以保證可以順利使用。
因此,若是咱們更改此設置,可能形成系統錯誤
// 這些操做應當避免,不然會形成系統錯誤 // 將`juicer.options.loose`設爲`false` // juicer.set('loose',false);
下面來看 juicer.set
方法的源代碼
juicer.set = function(conf, value) { // 引用`juicer` var that = this; // 反斜槓轉義 var escapePattern = function(v) { // 匹配 $ ( [ ] + ^ { } ? * | . * // 這些符號都須要被轉義 return v.replace(/[\$\(\)\[\]\+\^\{\}\?\*\|\.]/igm, function($) { return '\\' + $; }); }; // 設置函數 var set = function(conf, value) { // 語法邊界符匹配 var tag = conf.match(/^tag::(.*)$/i); if(tag) { // 因爲系統這裏沒有判斷語法邊界符是不是系統所用的 // 因此必定要拼寫正確 that.tags[tag[1]] = escapePattern(value); // 從新生成匹配正則 // `juicer.tagInit`解析見下面 that.tagInit(); return; } // 其餘配置項 that.options[conf] = value; }; // 若是傳入兩個參數,`conf`表示要修改的屬性,`value`是要修改的值 if(arguments.length === 2) { set(conf, value); return; } // 若是傳入一個參數,且是對象 if(conf === Object(conf)) { // 遍歷該對象的自有屬性設置 for(var i in conf) { if(conf.hasOwnProperty(i)) { set(i, conf[i]); } } } };
註釋裏面已經提示,經過 juicer.set
方法能夠覆蓋任何屬性。
若是修改了語法邊界符設定,將會從新生成匹配正則,下面看匹配正則的源代碼
juicer.tags = { // 操做開 operationOpen: '{@', // 操做閉 operationClose: '}', // 變量開 interpolateOpen: '\\${', // 變量閉標籤 interpolateClose: '}', // 禁止對其內容轉義的變量開 noneencodeOpen: '\\$\\${', // 禁止對其內容轉義的變量閉 noneencodeClose: '}', // 註釋開 commentOpen: '\\{#', // 註釋閉 commentClose: '\\}' }; juicer.tagInit = function() { /** * 匹配each循環開始,如下都是OK的 * `each VAR as VALUE`, 如 {@each names as name} * `each VAR as VALUE ,INDEX`,如 {@each names as name,key} * `each VAR as`,如 {@each names as} * 須要說明後兩種狀況: * `,key` 是一塊兒被捕獲的,因此在編譯模板的時候,系統會用`substr`去掉`,` * as 後沒有指定別名的話,默認以`value`爲別名,因此 * {@each names as} 等價於 {@each names as value} */ var forstart = juicer.tags.operationOpen + 'each\\s*([^}]*?)\\s*as\\s*(\\w*?)\\s*(,\\s*\\w*?)?' + juicer.tags.operationClose; // each循環結束 var forend = juicer.tags.operationOpen + '\\/each' + juicer.tags.operationClose; // if條件開始 var ifstart = juicer.tags.operationOpen + 'if\\s*([^}]*?)' + juicer.tags.operationClose; // if條件結束 var ifend = juicer.tags.operationOpen + '\\/if' + juicer.tags.operationClose; // else條件開始 var elsestart = juicer.tags.operationOpen + 'else' + juicer.tags.operationClose; // eles if 條件開始 var elseifstart = juicer.tags.operationOpen + 'else if\\s*([^}]*?)' + juicer.tags.operationClose; // 匹配變量 var interpolate = juicer.tags.interpolateOpen + '([\\s\\S]+?)' + juicer.tags.interpolateClose; // 匹配不對其內容轉義的變量 var noneencode = juicer.tags.noneencodeOpen + '([\\s\\S]+?)' + juicer.tags.noneencodeClose; // 匹配模板內容註釋 var inlinecomment = juicer.tags.commentOpen + '[^}]*?' + juicer.tags.commentClose; // for輔助循環 var rangestart = juicer.tags.operationOpen + 'each\\s*(\\w*?)\\s*in\\s*range\\(([^}]+?)\\s*,\\s*([^}]+?)\\)' + juicer.tags.operationClose; // 引入子模板 var include = juicer.tags.operationOpen + 'include\\s*([^}]*?)\\s*,\\s*([^}]*?)' + juicer.tags.operationClose; // 內聯輔助函數開始 var helperRegisterStart = juicer.tags.operationOpen + 'helper\\s*([^}]*?)\\s*' + juicer.tags.operationClose; // 輔助函數代碼塊內語句 var helperRegisterBody = '([\\s\\S]*?)'; // 輔助函數結束 var helperRegisterEnd = juicer.tags.operationOpen + '\\/helper' + juicer.tags.operationClose; juicer.settings.forstart = new RegExp(forstart, 'igm'); juicer.settings.forend = new RegExp(forend, 'igm'); juicer.settings.ifstart = new RegExp(ifstart, 'igm'); juicer.settings.ifend = new RegExp(ifend, 'igm'); juicer.settings.elsestart = new RegExp(elsestart, 'igm'); juicer.settings.elseifstart = new RegExp(elseifstart, 'igm'); juicer.settings.interpolate = new RegExp(interpolate, 'igm'); juicer.settings.noneencode = new RegExp(noneencode, 'igm'); juicer.settings.inlinecomment = new RegExp(inlinecomment, 'igm'); juicer.settings.rangestart = new RegExp(rangestart, 'igm'); juicer.settings.include = new RegExp(include, 'igm'); juicer.settings.helperRegister = new RegExp(helperRegisterStart + helperRegisterBody + helperRegisterEnd, 'igm'); };
具體語法邊界符的用法請參照官方文檔:http://www.juicer.name/docs/docs_zh_cn.html
通常地,不建議對默認標籤進行修改。固然,若是默認語法邊界符規則與正在使用的其餘語言語法規則衝突,修改 juicer
的語法邊界符就頗有用了。
須要注意,{@each names as}
等價於 {@each names as value}
,儘管咱們仍要保持正確書寫的規則,避免利用系統自動糾錯機制
// 以下模板的寫法是不推薦的 /** {@each list as} <a href="${value.href}">${value.title}</a> {@/each} */
上面說,juicer.options._method
存儲了用戶的自定義函數,那麼咱們如何註冊以及如何使用自定義函數呢?
註冊/銷自定義函數
juicer.register
方法用來註冊自定義函數
juicer.unregister
方法用來註銷自定義函數
// `fname`爲函數名,`fn`爲函數 juicer.register = function(fname, fn) { // 自定義函數均存儲於 `juicer.options._method` // 若是已經註冊了該函數,不容許覆蓋 if(_method.hasOwnProperty(fname)) { return false; } // 將新函數註冊進入 return _method[fname] = fn; }; juicer.unregister = function(fname) { var _method = this.options._method; // 沒有檢測是否註銷的是系統自定義函數 // 用戶不要註銷錯了 if(_method.hasOwnProperty(fname)) { return delete _method[fname]; } };
自定義函數都是存儲在juicer.options._method
中的,所以如下方法能夠跳過函數是否註冊的檢驗強行更改自定義函數,這些操做很危險:
// 這些操做應當避免,不然會形成系統錯誤 // 改變`juicer.options._method` // juicer.set('_method',{}); // juicer.unregister('__juicer'); // juicer.unregister('__throw'); // juicer.unregister('__escapehtml');
先看下 juicer
的定義部分。
var juicer = function() { // 將傳遞參數(僞數組)切成數組後返回給`args`,以便調用數組的方法 var args = [].slice.call(arguments); // 將`juicer.options`推入`args`,表示渲染使用當前設置 args.push(juicer.options); /** * 下面將獲取模板內容 * 模板內容取決於咱們傳遞給`juicer`函數的首參數 * 能夠是模板節點的id屬性值 * 也能夠是模板內容本 */ // 首先會試着匹配,匹配成功就先看成id處理 // 左右兩側的空白會被忽略 // 若是是`#`開頭,後面跟着字母、數字、下劃線、短橫線、冒號、點號均可匹配 // 因此這麼寫都是能夠的:`id=":-."` if(args[0].match(/^\s*#([\w:\-\.]+)\s*$/igm)) { // 若是傳入的是模板節點的id,會經過`replace`方法準確匹配並獲取模板內容 // 回調函數的首參`$`是匹配的所有內容(首參),$id是匹配的節點id args[0].replace(/^\s*#([\w:\-\.]+)\s*$/igm, function($, $id) { // node.js環境沒有`document`,因此會先判斷`document` var _document = document; // 找尋節點 var elem = _document && _document.getElementById($id); // 若是該節點存在,節點的`value`或`innerHTML`就是模板內容 // 便是說,存放模板的內容節點只要有`value`或`innerHTML`屬性便可 // <script>能夠,<div>能夠,<input>也能夠 // 若是沒有節點,仍是把首參值做爲模板內容 args[0] = elem ? (elem.value || elem.innerHTML) : $; }); } // 若是是瀏覽器環境 if(typeof(document) !== 'undefined' && document.body) { // 先編譯`document.body.innerHTML`一次 juicer.compile.call(juicer, document.body.innerHTML); } // 若是隻傳入了模板,僅返回編譯結果,而不會當即渲染 if(arguments.length == 1) { return juicer.compile.apply(juicer, args); } // 若是傳入了數據,編譯以後當即渲染 if(arguments.length >= 2) { return juicer.to_html.apply(juicer, args); } };
juicer.compile
方法是模板內容編譯入口,其返回一個編譯引擎對象,引擎對象的 render
方法將執行渲染.
juicer.to_html
方法就是執行 juicer.compile
後當即執行 render
。咱們在向 juicer
函數傳入兩個參數的時候,就會當即執行這一方法。
先看 juicer.to_html
juicer.to_html = function(tpl, data, options) { // 若是沒有傳入設置或者有新設置,先從新生成設置 if(!options || options !== this.options) { options = __creator(options, this.options); } // 渲染 return this.compile(tpl, options).render(data, options._method); };
下面看 juicer.compile
是如何編譯模板內容的
juicer.compile = function(tpl, options) { // 若是沒有傳入設置或者有新設置,先從新生成設置 if(!options || options !== this.options) { options = __creator(options, this.options); } try { // 構造引擎對象,若是已經緩存則優先使用緩存 var engine = this.__cache[tpl] ? this.__cache[tpl] : new this.template(this.options).parse(tpl, options); // 除非設定`juicer.options.cache`爲`false`,不然緩存引擎對象 if(!options || options.cache !== false) { this.__cache[tpl] = engine; } // 返回引擎對象 return engine; } catch(e) { // 拋出錯誤,此方法在本文末介紹 __throw('Juicer Compile Exception: ' + e.message); // 返回一個新對象,該對象仍有`render`方法,但操做爲空 return { render: function() {} }; } };
juicer.compile
方法在正常狀況下會返回模板引擎對象,繼而執行該對象的 render
方法就能夠獲得咱們的模板編譯結果(HTML)。那引擎對象是如何被構造出來的呢?
看這句 new this.template(this.options).parse(tpl, options);
由此,咱們進入了 juicer
的核心構造函數,juicer.template
。因爲該構造函數篇幅很長,咱們先看下簡略版的結構,而後拆開來分析。
juicer.template = function(options) { // 因爲`juicer.template`是做爲構造器使用的 // 所以`this`引用的是`juicer.template`構造的實例 var that = this; // 引用選項配置`juicer.options` this.options = options; // 變量解析方法 this.__interpolate = function(_name, _escape, options) {}; // 模板解析方法 this.__removeShell = function(tpl, options) {}; // 根據`juicer.options.strip`判斷是否清除多餘空白 // 然後調用`juicer.template.__convert` this.__toNative = function(tpl, options) {}; // 詞法分析,生成變量和自定義函數定義語句 this.__lexicalAnalyze = function(tpl) {}; // 爲`juicer.template.__toNative`所調用 // 將模板解析爲可執行的JavaScript字符串 this.__convert = function(tpl, strip) {}; // 渲染模板的入口 this.parse = function(tpl, options) {}; };
好,下面咱們一點點地看
juicer.template.__interpolate
this.__interpolate = function(_name, _escape, options) { /** * `_define` 切割`_name` * `_fn`爲變量名,這裏先暫取值爲 `_define[0]` * 當傳入的首參沒有`|`分割變量和函數時 * `_fn` === `_define[0]` === `_name` * 代表是 ${name} 形式 * 當有`|`分割時,`_fn`的初始值會被覆蓋 * 形式是 ${name|function} 或 ${name|function,arg1,arg2} * `_cluster`爲函數及傳參 */ var _define = _name.split('|'), _fn = _define[0] || '', _cluster; // 若是有`|`分割,即有函數和傳參 // 舉個例子: `VAR|FNNAME,FNVAR,FNVAR2 if(_define.length > 1) { // VAR _name = _define.shift(); // [FNNAME,FNVAR,FNVAR2] _cluster = _define.shift().split(','); // `[_name].concat(_cluster)`是數組會自動調用`toString()`方法 // 結果就是:_metod.FNNAME.call({},VAR,FNVAR,FNVAR2) _fn = '_method.' + _cluster.shift() + '.call({}, ' + [_name].concat(_cluster) + ')'; } /** * 返回結果 * 若是`_escape`爲真,將轉義內容 * 若是`juicer.options.detection`爲真,將檢測變量是否認義 * 返回結果舉例(轉義內容且檢測變量定義) * <%=_method.__escapehtml.escaping(_method.__escapehtml.detection(`_fn`))%> */ return '<%= ' + (_escape ? '_method.__escapehtml.escaping' : '') + '(' + (!options || options.detection !== false ? '_method.__escapehtml.detection' : '') + '(' + _fn + ')' + ')' + ' %>'; };
這個方法用來分析變量的。這也容許咱們去使用自定義函數。如咱們建立自定義函數
// 經過`juicer.register`直接建立 juicer.register('echoArgs',function(a,b){ return a + b; }); // 或者在模板內經過內聯輔助函數間接建立 // 本質仍然是使用了`juicer.register` {@helper echoArgs} function(a,b){ return a+b; } {@/helper}
咱們在模板裏就能夠這麼用了:
// 使用自定義函數 ${value.href|echoArgs|value.title}
juicer.template.__removeShell
this.__removeShell = function(tpl, options) { // 計數器 // 利用計數器避免遍歷時建立的臨時變量與其餘變量衝突 var _counter = 0; // 解析模板內容 tpl = tpl // 解析模板裏的內聯輔助函數並註冊 .replace(juicer.settings.helperRegister, function($, helperName, fnText) { // `annotate`函數返回形參名稱和函數語句數組,本文末介紹 var anno = annotate(fnText); // 內聯輔助函數參數 var fnArgs = anno[0]; // 內斂輔助函數語句 var fnBody = anno[1]; // 構造內聯輔助函數 var fn = new Function(fnArgs.join(','), fnBody); // 註冊到自定義函數庫`juicer.options._method` juicer.register(helperName, fn); // 沒有清除{@helper}{@/helper} return $; }) /** * 解析each循環語句 * 舉個例子: {@each names as name,index} * `_name` => names * `alias` => name * `key` => ,index 注意正則匹配後前面有逗號 */ .replace(juicer.settings.forstart, function($, _name, alias, key) { // `alias` 若是木有,取爲`value`,如 {@each names as} 狀況 // `key` 若是須要屬性名,取之 var alias = alias || 'value', key = key && key.substr(1); // 避免重複 var _iterate = 'i' + _counter++; /** * 返回替換結果,舉例以下 * <% ~function(){ for(var i0 in names){ if(names.hasOwnProperty(i0)){ var name = names[i0]; var index = i0; %> */ return '<% ~function() {' + 'for(var ' + _iterate + ' in ' + _name + ') {' + 'if(' + _name + '.hasOwnProperty(' + _iterate + ')) {' + 'var ' + alias + '=' + _name + '[' + _iterate + '];' + (key ? ('var ' + key + '=' + _iterate + ';') : '') + ' %>'; }) // 解析each循環結束 .replace(juicer.settings.forend, '<% }}}(); %>') // 解析if條件開始 .replace(juicer.settings.ifstart, function($, condition) { return '<% if(' + condition + ') { %>'; }) // 解析if條件結束 .replace(juicer.settings.ifend, '<% } %>') // 解析else條件 .replace(juicer.settings.elsestart, function($) { return '<% } else { %>'; }) // 解析else if條件 .replace(juicer.settings.elseifstart, function($, condition) { return '<% } else if(' + condition + ') { %>'; }) // 解析禁止對其內容轉義的變量 .replace(juicer.settings.noneencode, function($, _name) { return that.__interpolate(_name, false, options); }) // 解析變量 .replace(juicer.settings.interpolate, function($, _name) { return that.__interpolate(_name, true, options); }) // 清除評論 .replace(juicer.settings.inlinecomment, '') // 解析輔助循環 .replace(juicer.settings.rangestart, function($, _name, start, end) { var _iterate = 'j' + _counter++; return '<% ~function() {' + 'for(var ' + _iterate + '=' + start + ';' + _iterate + '<' + end + ';' + _iterate + '++) {{' + 'var ' + _name + '=' + _iterate + ';' + ' %>'; }) // 載入子模板 .replace(juicer.settings.include, function($, tpl, data) { // 若是是node.js環境 if(tpl.match(/^file\:\/\//igm)) return $; // 返回 <% _method.__juicer(tpl,data);%> return '<%= _method.__juicer(' + tpl + ', ' + data + '); %>'; }); // 當`juicer.options.errorhandling`不爲`false` if(!options || options.errorhandling !== false) { tpl = '<% try { %>' + tpl; tpl += '<% } catch(e) {_method.__throw("Juicer Render Exception: "+e.message);} %>'; } return tpl; };
計數器的做用已經在註釋中說明,其命名方式是字符串 i
或 j
加數字。
於是,以下模板就可能錯誤:
// 以下模板的寫法是不推薦的 // 應當避免遍歷中的變量名與計數器建立的變量名衝突 /** // 模板 ${i0} {@each data as value} {# i0 可能會由於上面的遍歷建立的臨時變量而被替換} ${i0} {@/each} ${i0} ${j1} {@each i in range(1,5)} {# j1 可能會由於上面的循環建立的臨時變量而被替換} ${j1} {@/each} ${j1} // 數據 { data: { temp1: 'value1', temp2: 'value2' }, i0: 'i0', j1: 'j1' } // 結果 i0 temp1 temp2 i0 j1 1 2 3 4 j1 */
書寫變量在遍歷中出現的變量時候,必定要避免與系統創造的臨時變量重名。不過,因爲在編譯模板的時候,遍歷是在一個閉包中執行的,於是臨時變量不會影響到遍歷外的變量。
此外,推薦使用 juicer.register
註冊自定義函數,而非使用{@helper}{/@helper}
。由於內聯函數的代碼在生成HTML的時候沒有被清除。
若是要清除之,須將第 #297 行
return $;
更改成
return '';
juicer.template.__toNative
this.__toNative = function(tpl, options) { // 當`juicer.options.strip`不爲`false`時清除多餘空白 return this.__convert(tpl, !options || options.strip); };
juicer.template.__toNative
調用 juicer.template.__convert
方法,juicer.template.__convert
就不做分析了,經過不斷替換切割等組成函數語句。
juicer.__lexicalAnalyze
this.__lexicalAnalyze = function(tpl) { // 變量 var buffer = []; // 方法,已經存儲到`juicer.options.__method`才能被採用 var method = []; // 返回結果 var prefix = ''; // 保留詞彙,由於這些詞彙不能用做變量名 var reserved = [ 'if', 'each', '_', '_method', 'console', 'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'finally', 'for', 'function', 'in', 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'null', 'typeof', 'class', 'enum', 'export', 'extends', 'import', 'super', 'implements', 'interface', 'let', 'package', 'private', 'protected', 'public', 'static', 'yield', 'const', 'arguments', 'true', 'false', 'undefined', 'NaN' ]; // 查找方法 var indexOf = function(array, item) { // 若是在數組中查找,直接用數組的`indexOf`方法 if (Array.prototype.indexOf && array.indexOf === Array.prototype.indexOf) { return array.indexOf(item); } // 若是在僞數組中查找,遍歷之 for(var i=0; i < array.length; i++) { if(array[i] === item) return i; } return -1; }; // 變量名分析函數 var variableAnalyze = function($, statement) { statement = statement.match(/\w+/igm)[0]; // 若是沒有分析過,而且非保留字符 if(indexOf(buffer, statement) === -1 && indexOf(reserved, statement) === -1 && indexOf(method, statement) === -1) { // 跳過window內置函數 if(typeof(window) !== 'undefined' && typeof(window[statement]) === 'function' && window[statement].toString().match(/^\s*?function \w+\(\) \{\s*?\[native code\]\s*?\}\s*?$/i)) { return $; } // 跳過node.js內置函數 if(typeof(global) !== 'undefined' && typeof(global[statement]) === 'function' && global[statement].toString().match(/^\s*?function \w+\(\) \{\s*?\[native code\]\s*?\}\s*?$/i)) { return $; } // 若是是自定義函數 if(typeof(juicer.options._method[statement]) === 'function' || juicer.options._method.hasOwnProperty(statement)) { // 放進 `method` method.push(statement); return $; } // 存爲變量 buffer.push(statement); } return $; }; // 分析出如今for/變量/if/elseif/include中的變量名 tpl.replace(juicer.settings.forstart, variableAnalyze). replace(juicer.settings.interpolate, variableAnalyze). replace(juicer.settings.ifstart, variableAnalyze). replace(juicer.settings.elseifstart, variableAnalyze). replace(juicer.settings.include, variableAnalyze). replace(/[\+\-\*\/%!\?\|\^&~<>=,\(\)\[\]]\s*([A-Za-z_]+)/igm, variableAnalyze); // 遍歷要定義的變量 for(var i = 0;i < buffer.length; i++) { prefix += 'var ' + buffer[i] + '=_.' + buffer[i] + ';'; } // 遍歷要建立的函數表達式 for(var i = 0;i < method.length; i++) { prefix += 'var ' + method[i] + '=_method.' + method[i] + ';'; } return '<% ' + prefix + ' %>'; };
juicer.template.parse
this.parse = function(tpl, options) { // 指向構造的引擎實例 // `that`和`_that`都是一個引用,暫不明爲什麼這樣寫 var _that = this; // `juicer.options.loose` 不爲 `false` if(!options || options.loose !== false) { tpl = this.__lexicalAnalyze(tpl) + tpl; } // 編譯模板,獲得可執行的JavaScript字符串 tpl = this.__removeShell(tpl, options); tpl = this.__toNative(tpl, options); // 構造爲函數 this._render = new Function('_, _method', tpl); // 渲染方法 this.render = function(_, _method) { // 檢查自定義函數 if(!_method || _method !== that.options._method) { _method = __creator(_method, that.options._method); } // 執行渲染 return _that._render.call(this, _, _method); }; // 返回實例,方便鏈式調用 return this; };
_that
引發了我疑惑。爲何不直接用 that
?在 juicer.template
分析時我指出,做爲構造器被使用的 juicer.template
,var that = this;
的 that
就是指向這個被建立出來的模板引擎對象的,和 _that
起同樣的做用。
那爲何要用 _that
或者 that
來替代 this
呢?我想是爲了儘量保證渲染正常。若是咱們如此使用:
var render = juicer('#:-.').render; render({});
代碼是能夠正常運行的。
但若是咱們把語句改成 return this._render.call(this, _, _method);
,則會報錯,由於這時候,render
做爲全局上下文中的變量,函數中的 this
指針指向了全局對象,而全局對象是沒有渲染方法的。
最後分析下 juicer
裏的一些輔助性函數。
用於轉義的對象
var __escapehtml = { // 轉義列表 escapehash: { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', '/': '/' }, // 獲取要轉義的結果,如傳入`<`返回`<` escapereplace: function(k) { return __escapehtml.escapehash[k]; }, // 對傳參進行轉義 escaping: function(str) { return typeof(str) !== 'string' ? str : str.replace(/[&<>"]/igm, this.escapereplace); }, // 檢測,若是傳參是undefined,返回空字符串,不然返回傳參 detection: function(data) { return typeof(data) === 'undefined' ? '' : data; } };
拋出錯誤方法
// 接受的參數是`Error`構造的實例 var __throw = function(error) { // 若是控制檯可用 if(typeof(console) !== 'undefined') { // 若是控制檯能夠拋出警告 if(console.warn) { console.warn(error); return; } // 若是控制檯能夠記錄 if(console.log) { console.log(error); return; } } // 除此以外都直接拋出錯誤 throw(error); };
合併對象方法
傳入兩個對象,並返回一個對象,這個新對象同時具備兩個對象的屬性和方法。因爲 o
是引用傳遞,所以 o
會被修改
var __creator = function(o, proto) { // 若是`o`不是對象,則新建空對象 o = o !== Object(o) ? {} : o; // 僅在一些高級瀏覽器中有用 if(o.__proto__) { o.__proto__ = proto; return o; } // 空函數 var empty = function() {}; // 使用原型模式建立新對象 var n = Object.create ? Object.create(proto) : new(empty.prototype = proto, empty); // 將`o`的自有屬性賦給新對象 for(var i in o) { if(o.hasOwnProperty(i)) { n[i] = o[i]; } } // 返回新對象 return n; };
字符串形式函數解析方法
傳入字符串形式的函數或函數,若是是函數,會利用函數tostring
方法便可得到其字符串形式,繼而解析提取函數的參數名和函數代碼塊內的語句。
var annotate = function(fn) { // 匹配函數括號裏的參數名稱 var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; // 匹配逗號,用來分割參數名 var FN_ARG_SPLIT = /,/; // 匹配參數,若是開頭有下劃線結尾也得有下劃線 // 所以自定義函數應避免使用`_X_`形式做爲形參 var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // 匹配函數的代碼塊里語句 var FN_BODY = /^function[^{]+{([\s\S]*)}/m; // 匹配註釋 var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; // 函數的參數 var args = [], // 函數字符串形式 fnText, // 函數代碼塊內的語句 fnBody, // 函數的形式參數匹配結果 // 不是直接的參數名稱,以後會經過`replace`操做將真正的名稱推入`args` argDecl; // 若是傳入是函數且函數接收參數,`toString`轉成字符串 if (typeof fn === 'function') { if (fn.length) { fnText = fn.toString(); } // 若是傳入的是字符串,即函數字符串形式 } else if(typeof fn === 'string') { fnText = fn; } // 清除兩邊空白 // 低版本瀏覽器沒有 `String.prototype.trim` fnText = fnText.trim(); // 獲取函數參數名稱數組 argDecl = fnText.match(FN_ARGS); // 獲取函數語句 fnBody = fnText.match(FN_BODY)[1].trim(); // `argDecl[1].split(FN_ARG_SPLIT)` 就是函數的參數名 // 遍歷函數參數名稱數組 for(var i = 0; i < argDecl[1].split(FN_ARG_SPLIT).length; i++) { // 賦值爲參數名稱 var arg = argDecl[1].split(FN_ARG_SPLIT)[i]; // 經過替換操做來將正確的函數名稱推入`arg` arg.replace(FN_ARG, function(all, underscore, name) { // 過濾下劃線前綴 args.push(name); }); } // 返回形參名稱和函數語句 return [args, fnBody]; };
若是使用 {@helper}
建立自定義函數,_X_
形參將被過濾爲 X
,即
// 模板 {@helper userFunc} function (_X_){ } {@/helper} // 編譯結果 juicer.options._method.userFunc = function(X){ };
不過,使用 juicer.register
方法將不會過濾下劃線。