Compilador de plantillas js manuscritas

Prefacio

En la actualidad, todos los frameworks front-end modernos adoptan la MVVMarquitectura. Los desarrolladores solo necesitan definir algunos datos de estado en la página y realizar modificaciones relacionadas para lograr el propósito de renderizar la página. Esta hazaña libera completamente las manos de los ingenieros front-end y no Costos más largos que antes. Mucho tiempo estuvo atascado en domel atolladero de las operaciones.

La compilación de la plantilla MVVMjuega un papel importante en el proceso de implementación . Solo a través de la compilación de la plantilla, el estado de los datos y la htmlcadena se pueden combinar y devolver al navegador para su renderizado. Este artículo extrae la compilación de la plantilla por separado para explicar su código principal, que se puede ver en la parte inferior capa. Qué método se utilizó para lograrlo. Los requisitos son los siguientes:

 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>

Suponiendo que hay una cadena de plantilla template, escriba una Parserclase que se pasará dataal método de instancia como parámetro de estado y espere que el resultado final sea <div>4</div>.

Muchos estudiantes piensan que esto es realmente muy simple, simplemente withconstruya el código en la expresión con una declaración en un bloque de funciones y dé salida.

Este método de acceso directo es ciertamente posible, porque muchos frameworks en el mercado usan este método para compilar. Pero withtiene sus limitaciones. La withejecución de la declaración envuelta hará que la velocidad de cálculo se reduzca en gran medida. Debido a AngularJsla inspiración del código fuente, este artículo Se utilizará otro método para compilar la expresión (el código completo se publica al final).

Explicación del principio

{ {...}}La expresión en el interior es el punto más importante al que prestar atención. La expresión puede enumerar muchos símbolos de operación, como operadores aritméticos '+','-','*','/','%'.

Además, hay operadores lógicos para admitir, incluidos '&&','||','=='Además, hay '()','[]','.'procesamiento de paréntesis y puntos decimales .

Además de los símbolos anteriores, ¿cómo se relacionan las variables de la expresión datacon el estado interno y '()','[]','.'cómo tratar las variables si se combinan con ellas?

A partir de la descripción anterior, es muy difícil pensar en una solución integral que soporte todas las situaciones desde el principio. Cambiemos nuestro pensamiento, de simple a complejo, y hagámoslo símbolo por símbolo, comenzando con los requisitos más simples.

Corte de hilo

Antes de procesar oficialmente la expresión, necesitamos hacer un trabajo preparatorio. Primero, debemos separar la { {...}}expresión envuelta de la cadena de plantilla. Por ejemplo, una plantilla a compilar es la siguiente:

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

Los requisitos actuales son muy claros, separe { {...}}las expresiones de la cadena, y solo cuando se obtengan todas las expresiones se podrá iniciar la siguiente etapa del trabajo de compilación.

La lista de expresiones se puede exp_list = []almacenar en una variable y se debe definir una variable fragmentspara almacenar la htmlcadena sin expresión, ya que el resultado del cálculo de la expresión debe htmlconcatenarse con la cadena antes de que la cadena se devuelva al usuario.

En este momento, debe escribir una función compileque corte la cadena para separar la expresión, y el resultado esperado es el siguiente.

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

Si diseñamos la estructura de datos devuelta para que se parezca a la anterior, el próximo trabajo será fácil.

exp_listLa plantilla de lista se almacena en una expresión de cadena, su indexpropiedad representa la expresión actual en fragmentsel marcador de posición del índice, pero expes una expresión de contenido.

Cuando el código está aquí, la idea es muy clara: recorra exp_listcada expresión, calcule el resultado correspondiente y llénelo en la fragmentsposición correspondiente, y finalmente use fragmentsel contenido del arreglo para joinsintetizar la cadena que es el resultado final devuelto.

compileCómo escribir la función, el código central es el siguiente:

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++;
    }
   ...
}

expLa cadena de plantilla correspondiente que se compilará template, al atravesar su cadena, coincide con los extremos de la oración { { y }}. Si presiona el { { lado izquierdo antes de que se clasifique en una htmlcadena estática en, colóquelo fragmentspara almacenarlo. Si encuentra }}, significa que la expresión actualmente encontrado, y la cadena de expresión se puede obtener y almacenar exp_list.

Análisis de expresión

A través de la etapa anterior, la lista de expresiones se obtuvo con éxito exp_list, y ahora solo es necesario recorrer la lista para extraer cada expresión y calcular el resultado, y listo.

Suponiendo que { { 100 + age + 10}}la expresión tomada de la cadena de la plantilla es "100 + age + 10", ¿cómo debería calcularse esta expresión?

Supongamos que data = { age:10 }, obviamente, la expresión anterior no se puede calcular directamente. Porque ageestamos en la datadefinición de estado del objeto, si la expresión se puede convertir para 100 + data.age +10poder obtener los resultados 120de.

Ahora el problema es nuevamente, ¿cómo sé "100 + age + 10"qué caracteres están en datael estado de la expresión extraída y cuáles no?

La observación cuidadosa de las expresiones anteriores puede encontrar algunas reglas. Cualquier expresión juega un papel clave en la conexión es el símbolo del operador, que es responsable de conectar todos los datos juntos. Por ejemplo, el +número de arriba , la +izquierda o la derecha son dos para agregar Entonces se puede inferir que los elementos a ambos lados del signo más pueden ser constantes 100o estados age.

Después de descubrir la ley de la conexión de símbolos, podemos cortar la cuerda de acuerdo con el símbolo, con el propósito de separar el símbolo del elemento y clasificar con precisión el elemento como una constante o un estado.

Por ejemplo, en la expresión anterior "100 + age + 10", espere escribir una parseExpressionfunción y el resultado será el resultsiguiente después de la ejecución :

  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'Significa que los datos actuales son un símbolo aritmético y type:'state'que los datos actuales son una variable de estado. Además, se agrega un atributo para isConstantidentificar si los datos actuales son una constante digital 100o una variable de estado que necesita ser convertida data[age].

Si obtiene una estructura de datos similar a la anterior, el "100 + age + 10"valor de la expresión se puede calcular fácilmente. El resultbucle a través del símbolo extraído sucesivamente y cada elemento, si el elemento es una constante numérica directamente involucrada en el cálculo, si el elemento se va a utilizar. un estado dataobtuvo Los datos luego participa en operaciones simbólicas.

parseExpressionLa función de la función es procesar la cadena de expresión, convertir la estructura de datos en el resultaspecto mencionado anteriormente y devolverla. El parseExpressioncódigo central es el siguiente, expressioncorrespondiente a la cadena de expresión.

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

El caso anterior solo usa símbolos +, pero los símbolos de expresión reales también incluyen -,*,/,%,[,],(,),&&,||,!etc. Actualmente, estos símbolos se definen en una matriz symbal, la misma +lógica se puede usar para clasificar símbolos y elementos, y luego dividir los elementos en constantes y variables de estado.

Resolución de símbolo

Si una expresión es toda suma, el resultado correspondiente se puede calcular atravesando el bucle después de convertirlo en la estructura de datos anterior. Pero si la expresión lo es 10 + age * 2. De acuerdo con los pasos de análisis anteriores, primero convierta la estructura de datos en el siguiente formato:

 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
     }
 ]

Después de que la expresión contenga la multiplicación, no se puede atravesar directamente resultla matriz para calcular los resultados, porque de acuerdo con las reglas de la aritmética, la expresión 10 + age * 2debe considerarse primero y luego agregar el operador de multiplicación.

Para que la multiplicación tenga prioridad sobre la suma, primero podemos resulthacer una capa de multiplicación, age*2convertirla en un todo y luego hacer una suma para asegurar la prioridad. Esperamos que resultla estructura de datos después de la multiplicación se transforme en lo siguiente.

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

Esta estructura de datos se ha convertido en una adición familiar nuevamente. Al atravesar la resultmatriz para obtener el símbolo y el valor del elemento, se puede obtener la suma de la adición. El resulttercer elemento se puede obtener llamando a la getValuefunción age * 2.

A continuación, echemos un vistazo a cómo se implementa el procesamiento de multiplicación de la capa anterior.

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

Al resultatravesar la matriz, se juzga si el signo del elemento actual es *. Si coincide, se debe sacar la suma *de los elementos que participan en la operación de multiplicación en ambos lados .leftright

El 左侧元素, *, 右侧元素de la resulteliminación de la matriz, y luego se combinan en un nuevo elemento de new_itemenchufe de nuevo en la posición correspondiente new_itemde getValueque se corresponde con age * 2el producto.

Lo anterior getValuees una versión simplificada, de hecho, en la primera lefty righthacer una determinación de constantes, si son constantes numéricas extraídas directamente del cálculo, si el estado va a llamar datapara obtener el estado del valor de recálculo.

Del mismo modo, la expresión incluye sólo +y *sólo casos muy simples, de hecho, una gran cantidad de símbolos comprende además, por ejemplo (),[],&&, etc, pero no importa qué tipo de símbolo, que tiene un orden diferente del cálculo de prioridad, como se señaló anteriormente antes de la adición multiplicación De la misma manera, los paréntesis prevalecerán sobre el cálculo de la multiplicación, y la suma prevalecerá sobre el cálculo de símbolos lógicos (por ejemplo &&).

Aunque hay muchos símbolos, hacen lo *mismo, es decir, primero convierten los símbolos y elementos con alta prioridad en un elemento completamente nuevo new_item, y el elemento reservará una getValuefunción para calcular el valor total. Espere hasta estas prioridades Los operadores altos se convierten todos, y solo quedan operadores simples en la matriz, y el resultado final deseado se puede obtener fácilmente atravesando los valores.

Si se ordenan las prioridades de todos los operadores, el resultprograma de procesamiento de la matriz final se verá como la siguiente función:

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


Los paréntesis son la prioridad más alta de todos los operadores, por lo que deben colocarse en el primer paso.

Suponiendo la expresión existente 10 * (age + 2), la resultestructura de datos analizados correspondiente es la siguiente.

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:")"
     }
]

Si es por el bracketHanlder(result)controlador, una estructura de datos convertida en el formulario más fácil de manejar.

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

bracketHanlderLa lógica de procesamiento de la función es la siguiente.

   /**
   * 处理小括号
   */
  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);
        },
      });
	
      ... //省略
     
  }

startY endcorresponden respectivamente al índice de '('y ')'en la resultmatriz, que 'exp'es la expresión entre paréntesis.

Los paréntesis son un poco diferentes de otros operadores, porque cualquier símbolo puede escribirse entre paréntesis y es una expresión en sí misma.

De hecho, si desea analizar el contenido entre paréntesis, puede volver a realizar el proceso anterior con la declaración envuelta en él.Solo necesita llamar a la parseExpressionfunción de forma recursiva en el código y tratar el contenido entre paréntesis como una expresión.

parseExpressionDespués de la ejecución, eventualmente devolverá una función analítica. En este momento, siempre que se le pase el valor de estado, la función puede devolver el valor de la expresión.

A continuación, veamos el procesamiento de corchetes y puntos decimales. El []frente puede ser un estado, por ejemplo array[3], o un corchete, como una matriz multidimensional array[0][1], y la parte envuelta en corchetes es una expresión array[1+age*2]. Siempre que es una expresión, el método de procesamiento es el mismo que Igual que los paréntesis.

La lógica de procesamiento del punto decimal es relativamente simple. Solo necesita enfocarse en el elemento izquierdo y el elemento derecho. Si el elemento izquierdo es un número, entonces similar a esta situación 100.11, el entero debe tratarse como un decimal. Si el elemento la izquierda es un estado ob.score, el todo debe ser tratado como un objeto.

Además, []y .no hay un orden de prioridad definido, por ejemplo, existe una plantilla de este tipo { { list[0].students[0].name }}. Los corchetes pueden calcularse antes del punto decimal o pueden calcularse después del punto decimal.

A continuación, echemos un vistazo a la lógica central del procesamiento de corchetes y puntos decimales.

  /**
   *  处理中括号 [] 和 .
   */
  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 === '.'){
         // 小数点的处理逻辑和乘法一样,都是只需要关注左右元素转化成整体即可
      }    
    }
  }

squreBracketHanlderEl flujo de trabajo se puede describir brevemente de la siguiente manera: sus ideas centrales son las mismas que la multiplicación, combinando elementos y símbolos en un todo.

Por ejemplo { { list[0].students[0].name }}, la función squreBracketHanlderprocesará la expresión primero y la list[0]convertirá en un todo. Debido a que item1el elemento se elimina, el valor del índice de bucle debe modificarse antes de continuar recorriendo.

Luego, convierta la item1suma .studentsen item2y modifique el índice para continuar el recorrido.

item2Y [0]transformado de nuevo item3, modifica el índice para continuar el recorrido.

Finalmente, el item3AND se .nametransforma en el último elemento item4antes de participar en otras operaciones.

item4Hay una getValuefunción que devuelve list[0].students[0].nameel valor de toda la expresión .

Los últimos operadores restantes %, /, &&y así sucesivamente, y *el procesamiento de la lógica, como elementos solamente necesidad de centrarse en ambos lados de la símbolo, se convierte en un nuevo elemento puede ser integral.

Código fuente

Código completo

Supongo que te gusta

Origin blog.csdn.net/brokenkay/article/details/114198027
Recomendado
Clasificación