Handwritten js template compiler

Preface

At present, modern front-end frameworks all adopt the MVVMarchitecture. Developers only need to define some state data on the page and perform related modifications to achieve the purpose of rendering the page. This feat completely liberates the hands of front-end engineers and no longer costs as before. A lot of time was stuck in domthe quagmire of operations.

Template compilation MVVMplays an important role in the implementation process. Only through template compilation can the data state and htmlstring be combined and returned to the browser for rendering. This article extracts the template compilation separately to explain its core code, which can peek into the bottom layer. What method was used to achieve it. The requirements are as follows:

 const data = {
      num: 1,
      price: [
        {
          value: 60,
        },
      ]
 };
 
 const template = '<div>{
   
   { -1*price[0].value && 12+(8*!6-num)*10+2 }}</div>';
 
 const compiler = new Parser(template);
  
 console.log(compiler.parse(data)); //<div>4</div>

Assuming that there is a template string template, please write a Parserclass that will be datapassed into the instance method as a state parameter, and expect the final output result to be <div>4</div>.

Many students think that this is actually very simple, just withbuild the code in the expression with a statement in a function block and output it.

This shortcut method is certainly possible, because many frameworks on the market use this method to compile. But it withhas its limitations. The withexecution of the wrapped statement will cause the calculation speed to be greatly reduced. Because of AngularJsthe inspiration of the source code, this article Another method will be used to compile the expression (the complete code is posted at the end).

Principle explanation

{ {...}}The expression inside is the most important point to pay attention to. The expression can list many operation symbols, such as arithmetic operators '+','-','*','/','%'.

In addition, there are logical operators to support, including '&&','||','=='. In addition, there are parentheses and decimal points '()','[]','.'processing.

In addition to the above symbols, how are the variables in the expression datarelated to the state inside, and '()','[]','.'how to deal with the variables if they are combined with them.

From the above description, it is very difficult to think of a comprehensive solution to support all situations from the beginning. Let's change our thinking, from simple to complex, and do it one symbol by symbol, starting with the simplest requirements.

String cutting

Before officially processing the expression, we need to do some preparatory work. First, we must separate the { {...}}wrapped expression from the template string. For example, a template to be compiled is as follows:

  const template = "<div><p>{
   
   { name }}</p>{
   
   { age + 100 }}</div>";

The current requirements are very clear. Separate { {...}}the expressions from the string, and only when all the expressions are obtained can the next stage of compilation work be started.

The expression list can be exp_list = []stored in a variable , and a variable must be defined fragmentsto store the non-expression htmlstring. Because the calculation result of the expression must be htmlconcatenated with the string before the string is returned to the user.

At this moment, you need to write a function compilethat cuts the string to separate the expression, and expect the following output.

  compile(template){
     ...
   
   exp_list = [{index:1,exp:"name"},{index:3,exp:"age + 100"}];
   
   fragments = ["<div><p>","","</p>","","</div>"];
   
  }

If we design the returned data structure to look like the above, the next work will be easy.

exp_listThe list template is stored in a string expression, its indexproperty represents the current expression in fragmentsthe index placeholder, but expis an expression of content.

When the code is here, the idea is very clear. Loop through exp_listeach expression, calculate the corresponding result and fill it in the fragmentscorresponding position, and finally use fragmentsthe content of the array to joinsynthesize the string is the final return result.

compileHow to write the function, the core code is as follows:

compile(exp){
    let i = 0, last = 0, tmp = '', record = false;
    while (i < exp.length) {
        if (exp[i] === '{' && exp[i + 1] === '{') {
            this.fragments.push(exp.slice(last, i), '');
            last = i;
            record = true; // 开始记录
            i += 2;
            continue;
        }
        else if (exp[i] === '}' && exp[i + 1] === '}') {
            this.exp_list.push({
                index: this.fragments.length - 1,
                exp: exp.slice(last + 2, i),
            });
            last = i + 2;
            record = false;
            tmp = '';
        }
        else if (record) {
            tmp += exp[i];
        }
        i++;
    }
   ...
}

expCorresponding template string to be compiled template, by traversing its string matches the sentence ends { { and }}. If you hit the { { left-hand side before it is classified into a static htmlstring in, put fragmentsin for storage. If you encounter }}, It means that the expression currently encountered has been traversed, and the expression string can be obtained and stored exp_list.

Expression analysis

Through the previous stage, the expression list has been successfully obtained exp_list, and now it is only necessary to traverse the list to extract each expression and calculate the result, and you are done.

Assuming that { { 100 + age + 10}}the expression taken from the template string is "100 + age + 10", how should this expression be calculated?

Suppose data = { age:10 }, obviously the above expression cannot be calculated directly. Because agewe in the datastate definition of the object, if the expression can be converted to 100 + data.age +10be able to get the results 120of.

Now the problem is again, how do I know "100 + age + 10"which characters are in datathe state of the extracted expression , and which ones are not?

Careful observation of the above expressions can find some rules. Any expression plays a key role in connection is the operator symbol, which is responsible for connecting all data together. For example, the +number above , the +left or the right is two to be added. Then it can be inferred that the elements on both sides of the plus sign may be constants 100or states age.

After discovering the law of symbol connection, we can cut the string according to the symbol. The purpose of this is to separate the symbol from the element and accurately classify the element as a constant or state.

For example, in the above expression "100 + age + 10", expect to write a parseExpressionfunction, and the result will be resultas follows after execution :

  parseExpression(expression) {
     ...
    result = [
       {
          type: 'state',
          exp:"100",
          isConstant:true
       },
       {
          type: 'symbal',
          exp:"+"
       }
       {
          type: 'state',
          exp:"age",
          isConstant:false
       },
       {
          type: 'symbal',
          exp:"+"
       },
       {
          type: 'state',
          exp:"10",
          isConstant:true
       },
    ] 
  }

type:'symbal'It means that the current data is an arithmetic symbol, and type:'state'that the current data is a state variable. In addition, an attribute is added to isConstantidentify whether the current data is a digital constant 100or a state variable that needs to be converted data[age].

If you get a data structure similar to the above, that the expression "100 + age + 10"value can be easily calculated out. The resultloop through successively extracted symbol and each element, if the element is a numerical constant directly involved in the calculation, if the element is to use a state datagot The data then participates in symbolic operations.

parseExpressionThe function of the function is to process the expression string, convert the data structure into the above-mentioned resultlook and return it back. The parseExpressioncore code is as follows, expressioncorresponding to the expression string.

parseExpression(expression) {
        const data = [];
        let i = 0, tmp = '', last = 0;
        while (i < expression.length) {
            const c = expression[i];
            if (this.symbal.includes(c)) {
                let exp = expression.slice(last, i), isConstant = false;
                if (this.isConstant(exp)) {
                    //是否为数字常量
                    isConstant = true;
                }
                //它是一个字符
                data.push({
                    type: 'state',
                    exp,
                    isConstant,
                });
                data.push({
                    type: 'symbal',
                    exp: c,
                });
                last = i + 1;
            }
            i++;
        }
        ...
        return data;
    }

The above case only uses symbols +, but the actual expression symbols also include -,*,/,%,[,],(,),&&,||,!etc. Currently these symbols are defined in an array symbal, the same +logic can be used to classify symbols and elements, and then divide the elements into constants and State variables.

Symbol resolution

If an expression is all addition, the corresponding result can be calculated by traversing the loop after converting it into the above data structure. But if the expression is 10 + age * 2. According to the above analysis steps, first convert the data structure into the following format:

 result = [
     {
          type: 'state',
          exp:"10",
          isConstant:true
     },
     {
          type: 'symbal',
          exp:"+"
     },
     {
          type: 'state',
          exp:"age",
          isConstant:false
     },
     {
          type: 'symbal',
          exp:"*"
     },
     {
          type: 'state',
          exp:"2",
          isConstant:true
     }
 ]

After expression contains multiplication can not be directly traverse resultthe array to calculate the results, because in accordance with the rules of arithmetic, the expression 10 + age * 2should be considered first and then adding the multiplication operator.

In order to make multiplication take precedence over addition, we can first resultdo a layer of multiplication, age*2convert it into a whole and then do addition to ensure the priority. We expect resultthe data structure after multiplication to be transformed into the following .

 result = [
     {
          type: 'state',
          exp:"10",
          isConstant:true
     },
     {
          type: 'symbal',
          exp:"+"
     },
     {
          type: 'state',
          catagory:"*",
          left:"age",
          right:"2"
          getValue:(scope)=>{
             ...
             //返回 age *2 的值
          }
     }
 ]

This data structure has become a familiar addition again.By traversing the resultarray to get the symbol and element value, the sum of the addition can be obtained.The resultthird element can be obtained by calling the getValuefunction age * 2.

Next, let's take a look at how the multiplication processing of the above layer is implemented.

  function exec(result){
     for (let i = 0; i < result.length; i++) {
      if (result[i].type === 'symbal' && result[i].exp === "*") {
        //删除三个元素
        const tmp = result.splice(i - 1, 3);
        // 组合新元素
        const left = tmp[0];
        const right = tmp[2];
        const new_item = {
          type: 'state',
          catagory: tmp[1].exp, 
          getValue: (data) => { // data对应着状态数据
            const lv = data[left]; // left就是'age'
            const rv = right; // right就是2 
            return lv * rv;
          },
        };
        //插入新元素
        result.splice(i - 1, 0, new_item);
        //修正索引
        i -= 1;
      }
    } 	 
  }

By resulttraversing the array, it is judged whether the sign of the current element is *. If it matches, the sum *of the elements participating in the multiplication operation on both sides must be taken out .leftright

The 左侧元素, *, 右侧元素from the resultremoval of the array, and then combined into a new element new_itemplug it back in the corresponding position new_itemof getValueit corresponds to age * 2the product.

The above getValueis a simplified version, in fact, on the first leftand rightdo some constants determination. If it is numeric constants extracted directly out of the calculation, if the state is going to call datato obtain the status of the value of re-computation.

Similarly, expression includes only +and *just very simple cases, in fact, a lot of symbols further comprises, for example (),[],&&, etc., but no matter what kind of symbol, which has a different order of the priority calculation, as noted above before addition multiplication In the same way, parentheses will take precedence over the calculation of multiplication, and addition will take precedence over the calculation of logical symbols (for example &&).

Although there are many symbols, they do the *same thing, that is, first convert the symbols and elements with high priority into a whole new element new_item, and the element will reserve a getValuefunction to calculate the overall value. Wait until these priorities The high operators are all converted, and there are only simple operators left in the array, and the final desired result can be easily obtained by traversing the values.

If the priorities of all operators are sorted, the final resultarray processing program will look like the following function:

 function optimize(result){
 	
    // 第一步 处理小括号

    result = this.bracketHanlder(result);

    // 第二步 处理 '[' 和 ']' 和 '.'

    this.squreBracketHanlder(result);

    // 第三步 处理 "!"

    result = this.exclamationHandler(result);

    // 第四步 处理 "*","/","%"

    this.superiorClac(result);

    // 第五步 处理 "+" 和 "-"

    this.basicClac(result);

    // 第六步 处理 "&&", "||" 和 "=="

    this.logicClac(result);

    return result;
 
 }


Parentheses are the highest priority of all operators, so they should be placed in the first step.

Assuming the existing expression 10 * (age + 2), the corresponding parsed resultdata structure is as follows.

const result = [
	 {
          type: 'state',
          exp:"10",
          isConstant:true
     },
     {
          type: 'symbal',
          exp:"*"
     },
     {
          type: 'symbal',
          exp:"("
     },
     {
          type: 'state',
          exp:"age",
          isConstant:false
     },
     {
          type: 'symbal',
          exp:"+"
     },
     {
          type: 'state',
          exp:"2",
          isConstant:true
     },
     {
          type: 'symbal',
          exp:")"
     }
]

If by the bracketHanlder(result)handler, a data structure converted into the form easier to handle.

const result = [
	 {
          type: 'state',
          exp:"10",
          isConstant:true
     },
     {
          type: 'symbal',
          exp:"*"
     },
     {
          type: 'expression',
          exp:"age+2",
          getValue:(data)=>{
             ...
             //能够得到age+2的值
          }
     }
]

bracketHanlderThe processing logic of the function is as follows.

   /**
   * 处理小括号
   */
  bracketHanlder(result) {
      
      ... //省略
	  
      const exp = result.slice(start + 1, end).reduce((cur, next) => {
        return cur + next.exp;
      }, '');    // exp = "age+2"	

      result.push({
        type: 'expression',
        exp,
        getValue: (data) => {
          return this.parseExpression(exp)(data);
        },
      });
	
      ... //省略
     
  }

startAnd endrespectively correspond to the index of '('and ')'in the resultarray, which 'exp'is the expression wrapped in parentheses.

The parentheses are a bit different from other operators, because any symbol can be written in the parentheses, and it is an expression in itself.

In fact, if you want to parse the content in the parentheses, you can go through the above process again with the statement wrapped in it.You only need to call the parseExpressionfunction recursively in the code, and treat the content wrapped in the parentheses as an expression.

parseExpressionAfter execution, it will eventually return an analytic function.At this time, as long as the state value is passed to it, the function can return the value of the expression.

Next, let’s look at the processing of square brackets and decimal points. The []front can be a state, for example array[3], or a square bracket, such as a multidimensional array array[0][1], and the part wrapped in the square bracket is an expression array[1+age*2]. As long as it is an expression, the processing method is the same as Same as parentheses.

The processing logic of the decimal point is relatively simple. It only needs to focus on the left element and the right element. If the left element is a number, then similar to this situation 100.11, the whole should be treated as a decimal. If the left is a state ob.score, The whole must be treated as an object.

In addition []and .there is no definite priority order, for example, there is such a template { { list[0].students[0].name }}. The square brackets may be calculated before the decimal point, or it may be calculated after the decimal point.

Next, let's take a look at the core logic of bracket and decimal point processing.

  /**
   *  处理中括号 [] 和 .
   */
  squreBracketHanlder(result){
      //遍历result
           ...
      if(result[i].exp === ']'){ // 当前中括号已经遍历结束了     
          //删除元素
          //start_index 对应着 "[" 的索引
          const extra = result.splice(start_index-1,i-start_index+2); //将中括号包裹的内容和左侧元素一起删掉
          //添加新元素,组合成一个新的整体
          const left = extra[0].getValue?extra[0].getValue:extra[0].exp; // 获取中括号左边的元素
          const right = extra.slice(2,extra.length-1).reduce((cur, next) => { // 获取中括号包裹的表达式
            return cur + next.exp;
          }, '');
          result.splice(start_index-1,0,{
            type:"state",
            category:"array",
            getValue:(data)=>{
              if(typeof left === "function"){ //可能是多维数组
                return left(data)[this.deepParse(right)(data)];
              }
              else{ // 中括号左边是一个状态,例如array[0],left = "array",right = 0
                return data[left][this.deepParse(right)(data)];
              }
            }
          })
          
          // 修正索引
          i = start_index - 2;
          ...
      }else if(result[i].exp === '.'){
         // 小数点的处理逻辑和乘法一样,都是只需要关注左右元素转化成整体即可
      }    
    }
  }

squreBracketHanlderThe workflow can be briefly described as follows. Their core ideas are the same as multiplication, combining elements and symbols into a whole.

For example { { list[0].students[0].name }}, the function squreBracketHanlderwill process the expression first and list[0]convert it into a whole.Because item1the element is deleted, the loop index value must be modified before continuing to traverse.

Then convert the item1sum .studentsinto item2and modify the index to continue traversal.

item2And [0]transformed into again item3, modify the index to continue traversal.

Finally, the item3AND is .nametransformed into the last element item4before participating in other operations.

item4There is a getValuefunction that returns list[0].students[0].namethe value of the entire expression .

The last remaining operators %, /, &&and so on, and *processing logic, as elements only need to focus on both sides of the symbol, is converted into a new element can be integral.

Source code

Complete code

Guess you like

Origin blog.csdn.net/brokenkay/article/details/114198027
Recommended