JavaScript > Juicer.js源碼解讀

Juicer.js源碼解讀

Version: 0.6.9-stablehtml

Date: 8th of Aug, 2015node

我的能力有限,若有分析不當的地方,懇請指正!git

第一部分: 參數配置

方法與參數github

參數配置方法是 juicer.set,該方法接受兩個參數或一個參數:api

  1. 當傳入兩個參數時,如 juicer.set('cache',false) ,便是設置 cachefalse數組

  2. 當傳入一個參數時,該參數應爲一個對象,如 juicer.set({cache:false}),系統將遍歷這個對象的屬性來設值瀏覽器

能夠配置的內容緩存

咱們能夠配置一些參數選項,包括 cachestriperrorhandlingdetection;其默認值都是true;咱們還能夠修改模板的語法邊界符,如 tag::operationOpen 等。具體可配置的項能夠參看其源代碼。閉包

工做原理app

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 方法將不會過濾下劃線。

相關文章
相關標籤/搜索