4

Juicer.js源码解读

Version: 0.6.9-stable

Date: 8th of Aug, 2015

个人能力有限,如有分析不当的地方,恳请指正!

第一部分: 参数配置

方法与参数

参数配置方法是 juicer.set,该方法接受两个参数或一个参数:

  1. 当传入两个参数时,如 juicer.set('cache',false) ,即是设置 cachefalse

  2. 当传入一个参数时,该参数应为一个对象,如 juicer.set({cache:false}),系统将遍历这个对象的属性来设值

可以配置的内容

我们可以配置一些参数选项,包括 cachestriperrorhandlingdetection;其默认值都是true;我们还可以修改模板的语法边界符,如 tag::operationOpen 等。具体可配置的项可以参看其源代码。

工作原理

juicer.options = {
    // 是否缓存模板编译结果
    cache: true,
    // 是否清除空白
    strip: true,
    // 是否处理错误
    errorhandling: true,
    // 是否检测变量是否定义
    detection: true,
    // 自定义函数库
    _method: __creator({
        __escapehtml: __escapehtml,
        __throw: __throw,
        __juicer: juicer
    }, {})
};

选项解析如下:

  1. cache 是否缓存编译结果(引擎对象)。缓存的结果存于 juicer.__cache
  2. strip 是否清除模板中的空白,包括换行、回车等
  3. errorhandling 是否处理错误
  4. detection 开启后,如果变量未定义,将用空白字符串代替变量位置,否则照常输出,所以如果关闭此项,有可能造成输出 undefined
  5. _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;
};

计数器的作用已经在注释中说明,其命名方式是字符串 ij 加数字。

因而,如下模板就可能错误:

// 如下模板的写法是不推荐的
// 应当避免遍历中的变量名与计数器创建的变量名冲突
/**
    // 模板
    ${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.templatevar that = this;that 就是指向这个被创建出来的模板引擎对象的,和 _that 起一样的作用。

那为什么要用 _that 或者 that 来替代 this 呢?我想是为了尽可能保证渲染正常。如果我们如此使用:

var render = juicer('#:-.').render;
render({});

代码是可以正常运行的。

但如果我们把语句改为 return this._render.call(this, _, _method);,则会报错,因为这时候,render 作为全局上下文中的变量,函数中的 this 指针指向了全局对象,而全局对象是没有渲染方法的。

第五部分 辅助函数

最后分析下 juicer 里的一些辅助性函数。

用于转义的对象

var __escapehtml = {
    // 转义列表
    escapehash: {
        '<': '&lt;',
        '>': '&gt;',
        '&': '&amp;',
        '"': '&quot;',
        "'": '&#x27;',
        '/': '&#x2f;'
    },
    // 获取要转义的结果,如传入`<`返回`&lt;`
    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 方法将不会过滤下划线。


残阳映枫红
6.1k 声望638 粉丝