Research on source code of underscore.js (5)

Overview

I have wanted to study the underscore source code for a long time. Although the underscore.js library is a bit outdated, I still want to learn about the library's architecture, functional programming and the writing of common methods, but there is nothing else to study. , so let's end the wish to study the underscore source code.

underscore.js source code research (1)
underscore.js source code research (2)
underscore.js source code research (3)
underscore.js source code research (4)
underscore.js source code research (5)
underscore.js source code research (6)
underscore. js source code research (7)
underscore.js source code research (8)

References: official notes of underscore.js , undersercore source code analysis , undersercore source code analysis segmentfault

template engine

I have been exposed to template engines before , such as template.js, handlebars.js, jade.js, nunjucks.js, etc. Later, when I learned react, I also came into contact with jsx. At that time, I felt incredible. It was really amazing to be able to insert variables and even statements in js into html. When I looked at the source code of underscore.js today, I also found that there is a template engine in it, so I came to study the template engine.

Implement variable substitution

When it comes to template engines, one of the most basic features is the ability to insert js variables into html code. Let's implement this effect.

We need to achieve this effect:

//定义一个模板
const tpl = 'hello {{name}}';

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

//渲染,最后content是name被替换过的html代码
const output = render(tpl, data);

In fact, after carefully arranging the process of the effect, I feel that it is very simple to achieve this effect, that is, use a regular replacement , and replace {{ name }}the value in it with the data in the data.

The implementation code is as follows:

//定义替换的正则表达式
const rule = /{{([\s\S]+?)}}/g;

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

Note that since there is only one parenthesis in rule, there is only p1 but no p2 in the function that replaces the second parameter.

Variable substitution improvements

For readability, we need templates that can be written in the following form. (there are spaces on both sides of the name)

//定义一个模板
const tpl = 'hello {{ name }}';

So we add a function to remove spaces, the whole code is as follows:

//定义替换的正则表达式
const rule = /{{([\s\S]+?)}}/g;

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

Note: The trim function is only compatible with IE9. If you want to be compatible with IE9 or lower, you need pollyfill.

Support statement

Almost all template engines support write statements , such as the following:

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);

We want the above code to output:

Students: haha  yaya

It looks very complicated, but it feels a bit simple when we break down the execution process in pseudo code:

//首先输出Students:
//然后执行下面的代码
for(i = 0; i < data.students.length; i++) {
//这里输出students[i].name
}
//完毕

It actually looks like this:

//定义要输出的内容
content = '';
content += 'Students:';
for(i = 0; i < data.students.length; i++) {
    content += 'data.students[i].name';
}
//输出整个content
return content

As you can see, there are two main points above:

  1. The content of the variable needs to be added to the content, but the content of the statement does not need to be added to the content.
  2. The content that is not a template needs to record the location, and then add it to the content through this location.

So to sort it out, we first need the regular expression of the statement, and then use this regular expression to replace it according to the above rules, the code is as follows:

//为了方便,我们把规则封装在一个对象里面
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);
}

The code of the renderFunc function it generates is shown in the following figure:

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

You can see that one disadvantage is that the for loop does not have curly braces , which causes it to only execute the following statement. If the parentheses are to be used, additional rules are required, which we will not discuss here.

So adding up all the code above would look like this:

//为了方便,我们把规则封装在一个对象里面
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));

It can be seen that the whole process is actually splicing and replacing strings, and then generating functions using the situation where Function accepts strings, and nothing else.

In the next blog post we will optimize this little template engine.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325128252&siteId=291194637