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
方法将不会过滤下划线。