手書きのjsテンプレートコンパイラ

序文

現在、最新のフロントエンドフレームワークはすべてMVVMアーキテクチャを採用しています。開発者は、ページ上でいくつかの状態データを定義し、関連する変更を実行するだけで、ページをレンダリングする目的を達成できます。この偉業は、フロントエンドエンジニアの手を完全に解放します。以前と同じようにコストが高くなります。dom操作の泥沼に多くの時間が費やされました。

テンプレートのコンパイルMVVMは、実装プロセス重要な役割を果たします。データの状態とhtml文字列を組み合わせてレンダリングのためにブラウザに返すことができるのは、テンプレートのコンパイルによってのみです。この記事では、テンプレートのコンパイルを個別に抽出して、下部を覗くことができるコアコードについて説明します。レイヤー。それを達成するために使用された方法。要件は次のとおりです。

 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>

テンプレート文字列があると仮定して、状態パラメータとしてインスタンスメソッドに渡されるクラスtemplateを記述し、最終的な出力結果がになることを期待してくださいParserdata<div>4</div>

多くの学生は、これは実際には非常に単純であると考えてwithいます。関数ブロック内のステートメントを使用して式にコードを作成し、それを出力するだけです。

市場に出回っている多くのフレームワークがこのメソッドを使用してコンパイルするため、このショートカットメソッドは確かに可能ですwithが、制限があります。withラップされたステートメントを実行すると、計算速度が大幅に低下します。AngularJsソースコードのインスピレーションにより、この記事別の方法を使用して式をコンパイルします(完全なコードは最後に掲載されています)。

原理説明

{ {...}}内部の式は、注意を払うべき最も重要なポイントです。式には、算術演算子など、多くの演算記号をリストできます'+','-','*','/','%'

さらに、を含むサポートする論理演算子があります'&&','||','=='。さらに、括弧と小数点の'()','[]','.'処理があります。

上記の記号に加えて、式内の変数はdata内部の状態に'()','[]','.'どのように関連している、および変数がそれらと組み合わされた場合の処理方法。

上記の説明から、最初からすべての状況をサポートする包括的なソリューションを考えることは非常に困難です。私たちの考え方を単純なものから複雑なものに変えて、最も単純な要件から始めて、シンボルごとにそれを実行しましょう。

ストリングカット

式を正式に処理する前に、いくつかの準備作業を行う必要があります。まず、{ {...}}ラップされた式をテンプレート文字列から分離する必要があります。たとえば、コンパイルされるテンプレートは次のとおりです。

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

現在の要件は非常に明確です。{ {...}}を文字列から分離し、すべての式が取得された場合にのみ、コンパイル作業の次の段階を開始できます。

式リストは変数exp_list = []に格納できfragments、非式html文字列を格納するために変数を定義する必要があります。html文字列がユーザーに返される前に、式の計算結果を文字列連結する必要があるためです。

この時点で、式を区切るために文字列をカットする関数を作成する必要がありcompileます。期待される出力は次のとおりです。

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

返されるデータ構造を上記のように設計すれば、次の作業は簡単になります。

exp_listリストテンプレートは文字列式に格納され、そのindexプロパティfragmentsはインデックスプレースホルダーの現在の式を表しますexpが、コンテンツの式です。

コードがここにあるとき、アイデアは非常に明確です。exp_list各式をループし、対応する結果を計算して対応fragmentsする位置に入力し、最後fragmentsに配列の内容を使用joinて文字列合成すると、最終的な戻り結果になります。

compile関数の書き方コアコードは次のとおりです。

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

expテンプレート文字列を対応するコンパイルされるtemplateその文字列が文の終了に一致する横断することにより、{ { そして}}。あなたがヒットした場合{ { 、それが静的なのに分類される前に、左側をhtml置くの文字列、fragments保管のために。あなたが発生した場合は}}、それが現在表現することを意味し遭遇したものがトラバースされ、式文字列を取得して保存できますexp_list

発現解析

前の段階では、式リストが正常に取得されました。exp_listこれで、リストをトラバースして各式を抽出し、結果を計算するだけで済みます。

テンプレート文字列から{ { 100 + age + 10}}取得た式がであるとすると"100 + age + 10"この式どのように計算する必要がありますか?

仮定data = { age:10 }、明らかに上記の式を直接計算することはできません。そのためage、私たちにあるdataオブジェクトの状態の定義、表現をするために変換することができた場合100 + data.age +10の結果を取得することができる120のを。

ここでも問題は抽出された式の状態に"100 + age + 10"ある文字とそうでない文字をどのように知るdataかです。

上記の式を注意深く観察すると、いくつかのルールが見つかります。接続で重要な役割を果たす式は、すべてのデータを接続するための演算子記号です。たとえば、上の+数字、+左または右の数字は2つ追加されます。 。次に、プラス記号の両側の要素が定数100または状態である可能性があると推測できますage

シンボル接続の法則を発見した後、シンボルに従って文字列をカットすることができます。これの目的は、シンボルを要素から分離し、要素を定数または状態として正確に分類することです。

たとえば、上記の式"100 + age + 10"で、parseExpression関数を作成することを期待すると、実行後の結果はresult次のようになります

  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'これは、現在のデータが算術記号でありtype:'state'、現在のデータが状態変数であることを意味します。さらにisConstant、現在のデータがデジタル定数である100か、変換が必要な状態変数である識別するための属性が追加されdata[age]ます。

上記のようなデータ構造が得られれば、式の"100 + age + 10"値を簡単に計算できます。result連続して抽出された記号と各要素をループします。要素が計算に直接関係する数値定数の場合、要素を使用する場合状態dataが取得されたデータは、シンボリック操作に参加します。

parseExpression関数の機能は、式の文字列を処理し、データ構造を上記のresult外観に変換して返すことです。parseExpressionコアコードはexpression、式の文字列に対応する次のとおりです。

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

上記の場合はシンボルのみを使用します+が、実際の式シンボルには-,*,/,%,[,],(,),&&,||,!なども含まれます。現在、これらのシンボルは配列symbal定義されており、同じ+ロジックを使用してシンボルと要素分類し、要素を定数と状態変数に分割できます。

シンボル解決

式がすべて加算の場合、対応する結果は、ループを上記のデータ構造に変換した後にループをトラバースすることで計算できます。ただし、式がの場合10 + age * 2。上記の分析手順に従って、最初にデータ構造を次の形式に変換します。

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

式に乗算が含まれているとresult、配列を直接トラバースして結果を計算することはできません。算術の規則に従って、式10 + age * 2を最初に検討してから、乗算演算子を追加する必要があるためです。

乗算を加算よりも優先させるために、最初resultに乗算のレイヤーを実行し、age*2それを全体に変換してから加算を実行して優先順位を確保します。乗算後resultのデータ構造は次のように変換されると予想されます。

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

このデータ構造は再びおなじみの加算になりました。result配列をトラバースしてシンボルと要素の値を取得することにより、加算の合計を取得できますresult。3番目の要素getValue関数を呼び出すことで取得できますage * 2

次に、上記のレイヤーの乗算処理がどのように実装されているかを見てみましょう。

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

result配列をトラバースすることにより、現在の要素の符号がであるかどうかが判断され*ます。一致する場合*、両側の乗算演算に参加している要素left合計取り出す必要がありますright

左侧元素*右侧元素からのresult新しい要素に組み合わさ配列の除去、次いでnew_itemそれが対応する位置にバックプラグnew_itemgetValueそれに対応するage * 2製品。

上記getValue第一に、実際には、簡略化されたバージョンであるleftright、一部の定数の決意を行います。状態が呼び出すために起こっている場合には、計算のうち、直接数値定数抽出された場合はdata再計算の値の状態を取得します。

同様に、式のみ含ま+*ちょうど非常に単純な場合、実際には、シンボルの多くをさらに含む、例えば(),[],&&、等が、添加前の上述したように、優先度算出の異なる順序を有する種類のシンボルのもの、どんなに乗算同様に、括弧は乗算の計算よりも優先され、加算は論理記号の計算よりも優先されます(たとえば&&)。

多くのシンボルがありますが、*同じことを行います。つまり、最初に優先度の高いシンボルと要素をまったく新しい要素new_item変換し、要素はgetValue全体の値を計算する関数を予約します。これらの優先度が高くなるまで待機します。はすべて変換され、配列には単純な演算子しか残っていません。値をトラバースすることで、最終的な目的の結果を簡単に取得できます。

すべての演算子の優先順位がソートされている場合、最終的なresult配列処理プログラムは次の関数のようになります。

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


括弧はすべての演算子の中で最も優先度が高いため、最初のステップに配置する必要があります。

既存の式を想定すると、10 * (age + 2)対応する解析result済みデータ構造は次のようになります。

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

よる場合はbracketHanlder(result)、ハンドラ、データ構造が扱いやすい形式に変換します。

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

bracketHanlder関数の処理ロジックは次のとおりです。

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

startそして、endそれぞれ配列のインデックス'('配列')'resultのインデックスに対応'exp'します。これは、括弧で囲まれた式です。

括弧内には任意の記号を書き込むことができ、それ自体が式であるため、括弧は他の演算子とは少し異なります。

実際、括弧内のコンテンツを解析する場合は、ステートメントをラップして上記のプロセスを再度実行できます。コードでparseExpression関数を再帰的に呼び出すだけで、括弧内にラップされたコンテンツを次のように扱うことができます。表現。

parseExpression実行後、最終的に分析関数を返します。この時点で、状態値が渡されている限り、関数は式の値を返すことができます。

次に、角括弧と小数点の処理を見てみましょう。[]前面は、たとえば状態array[3]、または多次元配列などの角括弧にすることができ、角括弧で囲まれたarray[0][1]部分は式array[1+age*2]です。式であり、処理方法は括弧と同じです。

小数点の処理ロジックは比較的単純で、左の要素と右の要素にのみ焦点を当てる必要があります。左の要素が数値の場合、この状況と同様に100.11、全体を小数として扱う必要があります。左は状態ですob.score、全体をオブジェクトとして扱う必要があります。

さらに[].明確な優先順位はありません{ { list[0].students[0].name }}たとえば、そのようなテンプレートがあります。角括弧は小数点の前に計算することも、小数点の後に計算することもできます。

次に、角かっこと小数点処理のコアロジックを見てみましょう。

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

squreBracketHanlderワークフローは次のように簡単に説明できます。それらのコアアイデアは乗算と同じで、要素と記号を全体に結合します。

たとえば{ { list[0].students[0].name }}、関数squreBracketHanlderは最初にを処理list[0]して全体に変換しますitem1。要素が削除されているため、トラバースを続行する前にループインデックス値を変更する必要があります。

次に、item1合計.studentsをに変換しitem2、インデックスを変更してトラバーサルを続行します。

item2そして[0]再びitem3変換され、トラバーサルを続行するようにインデックスを変更します。

最後に、item3ANDは、他の操作に参加する前に.name最後の要素item4変換されます。

item4あるgetValue返す関数list[0].students[0].nameの値式全体が

最後に残った演算子、、%など/&&および*処理ロジックは、要素がシンボルの両側に焦点を合わせるだけでよいため、新しい要素に変換され、積分することができます。

ソースコード

完全なコード

おすすめ

転載: blog.csdn.net/brokenkay/article/details/114198027