underscore.js源碼研究(5)

概述

很早就想研究underscore源碼了,雖然underscore.js這個庫有些過期了,可是我仍是想學習一下庫的架構,函數式編程以及經常使用方法的編寫這些方面的內容,又剛好沒什麼其它要研究的了,因此就告終研究underscore源碼這一心願吧。html

underscore.js源碼研究(1)
underscore.js源碼研究(2)
underscore.js源碼研究(3)
underscore.js源碼研究(4)
underscore.js源碼研究(5)
underscore.js源碼研究(6)
underscore.js源碼研究(7)
underscore.js源碼研究(8)react

參考資料:underscore.js官方註釋undersercore 源碼分析undersercore 源碼分析 segmentfaultgit

模板引擎

以前就接觸過模板引擎,好比說template.js、handlebars.js、jade.js、nunjucks.js等等。後來學react的時候也接觸過jsx,當時我就感到很難以想象,居然可以把js中的變量甚至語句插入到html裏面去,真的十分神奇。今天看underscore.js的源碼的時候也發現裏面居然有模板引擎,因而我就來研究研究模板引擎。正則表達式

實現變量替換

說到模板引擎,一個最基本的特性就是能在html代碼中插入js的變量,下面咱們來實現這種效果。編程

咱們須要實現這種效果:segmentfault

//定義一個模板
const tpl = 'hello {{name}}';

//定義值
const data = {name: 'haha'};

//渲染,最後content是name被替換過的html代碼
const output = render(tpl, data);

其實仔細理了一下效果的流程以後,感受實現這個效果挺簡單的,就是用一個正則替換,把{{ name }}裏面的值替換爲data裏面的數據就好了。api

實現代碼以下:架構

//定義替換的正則表達式
const rule = /{{([\s\S]+?)}}/g;

//render函數
function render(tpl, data) {
    return tpl.replace(rule, (matcher, p1) => {
        return data[p1];
    })
}

注意,因爲rule裏面只有一個括號,因此replace第二個參數的函數裏面只有p1沒有p2。函數式編程

變量替換改進

爲了便於閱讀,咱們須要模板能夠寫成下面的形式。(name兩邊有空格)函數

//定義一個模板
const tpl = 'hello {{ name }}';

因此咱們加一個去空格的函數,整個代碼以下:

//定義替換的正則表達式
const rule = /{{([\s\S]+?)}}/g;

//render函數
function render(tpl, data) {
    return tpl.replace(rule, (matcher, p1) => {
        return data[p1.trim()];
    })
}

注意:trim函數只兼容IE9,若是要兼容IE9如下的話就須要pollyfill了。

支持語句

幾乎全部的模板引擎都支持寫入語句,好比像下面的寫法:

const tpl = 'Students:' +
    //注意這裏只有一個大括號!!!
    '{ for(i = 0; i < data.students.length; i++) }' +
    '{{ data.students[i].name }}';
const data = {
    students: [{
        id: 1,
        name: ' haha '
    },{
        id: 2,
        name: ' yaya '
    }]
};
const content = render(tpl, data);

咱們但願上述代碼輸出:

Students: haha  yaya

看起來很是複雜,但是咱們用僞代碼分解一下執行過程就感受有點簡單了:

//首先輸出Students:
//而後執行下面的代碼
for(i = 0; i < data.students.length; i++) {
//這裏輸出students[i].name
}
//完畢

實際寫起來是這樣的:

//定義要輸出的內容
content = '';
content += 'Students:';
for(i = 0; i < data.students.length; i++) {
    content += 'data.students[i].name';
}
//輸出整個content
return content

能夠看到,上面有這2個要點:

  1. 變量的內容須要添加到content裏面,可是語句的內容不須要添加到content裏面。
  2. 不是模板的內容要記錄位置,而後再經過這個位置添加到content裏面。

因此好好整理一下,咱們首先須要語句的正則表達式,而後經過這個正則表達式按照上述規則進行替換,代碼以下:

//爲了方便,咱們把規則封裝在一個對象裏面
const rules = {
    //插值,對應變量
    interpolate: /{{([\s\S]+?)}}/,
    //邏輯,對應語句
    evaluate: /{([\s\S]+?)}/
};
//2個正則合在一塊兒,先替換變量,再替換語句
const matcher = new RegExp([
    rules.interpolate.source,
    rules.evaluate.source
].join('|'), 'g');

//render函數
function render(tpl, data) {
    let concating = 'let content = "";\n';
    let index = 0;
    //仍然是replace裏面的第二個參數是函數的形式
    tpl.replace(matcher, (match, interpolate, evaluate, offset) => {
        //添加非模板的內容
        if (tpl.slice(index, offset)) {
            concating += 'content += "' + tpl.slice(index, offset) + '";\n';
        }
        //記錄偏移量
        index = offset + match.length;
        //變量須要添加到content裏面
        if (interpolate) {
            concating += 'content +=' + interpolate + ';\n';
        //語句不須要添加到content裏面,並且不要分號
        } else if (evaluate) {
            concating += evaluate + '\n';
        }
    })
    concating += 'return content;';
    //以concating爲內容,定義一個函數,參數是obj
    const renderFunc = new Function('obj', concating);
    return renderFunc(data);
}

它生成的renderFunc函數的代碼以下圖所示:

(function(obj
/*``*/) {
let content = "";
content += "Students:";
 for(i = 0; i < data.students.length; i++) 
content += data.students[i].name ;
return content;
})

能夠看到有一個缺點,就是for循環沒有大括號,這就致使它只執行下面的那條語句。若是要加大括號的話,就須要額外的規則,咱們這裏不討論。

因此把上面全部的代碼加起來就是這樣的:

//爲了方便,咱們把規則封裝在一個對象裏面
const rules = {
    //插值,對應變量
    interpolate: /{{([\s\S]+?)}}/,
    //邏輯,對應語句
    evaluate: /{([\s\S]+?)}/
};

//2個正則合在一塊兒,先替換變量,再替換語句
const matcher = new RegExp([
    rules.interpolate.source,
    rules.evaluate.source
].join('|'), 'g');

//定義模板和數據
const tpl = 'Students:' +
    //注意這裏只有一個大括號!!!
    '{ for(i = 0; i < data.students.length; i++) }' +
    '{{ data.students[i].name }}';
const data = {
    students: [{
        id: 1,
        name: ' haha '
    },{
        id: 2,
        name: ' yaya '
    }]
};

//render函數
function render(tpl, data) {
    let concating = 'let content = "";\n';
    let index = 0;
    //仍然是replace裏面的第二個參數是函數的形式
    tpl.replace(matcher, (match, interpolate, evaluate, offset) => {
        //添加非模板的內容
        if (tpl.slice(index, offset)) {
            concating += 'content += "' + tpl.slice(index, offset) + '";\n';
        }
        //記錄偏移量
        index = offset + match.length;
        //變量須要添加到content裏面
        if (interpolate) {
            concating += 'content +=' + interpolate + ';\n';
        //語句不須要添加到content裏面,並且不要分號
        } else if (evaluate) {
            concating += evaluate + '\n';
        }
    })
    concating += 'return content;';
    //以concating爲內容,定義一個函數,參數是obj
    const renderFunc = new Function('obj', concating);
    return renderFunc(data);
}

//輸出,結果爲Students: haha  yaya 
console.log(render(tpl, data));

能夠看到,整個過程其實是在拼接和替換字符串,而後利用Function接受字符串的情形生成函數,沒有其餘的任何內容。

在下一篇博文中咱們會對這個小的模板引擎進行優化。

相關文章
相關標籤/搜索