Babel コンパイル原理から Vue ソースコードまでの抽象構文ツリー

画像.png

バベルとは何ですか

  • Babel は JavaScript コンパイラーであり、ECMAScript の誕生以来、コードと動作環境のトランスレーターとして機能し、js の新しい構文を使用して自由にコードを記述できるようにしてきました。
  • babel はツールチェーンです
  • babel は開発者に対して何ができるのでしょうか?
    1. 構文変換。
    2. ソースコードの変換。
    3. ポリフィル (サードパーティのポリフィル モジュールを導入することによる変換とポリフィル) を通じて、ターゲット環境に不足している機能を追加します。
    4. Babel を使用してソース コードを操作すると、静的分析に使用できます。実戦でよく使われるのは、自動国際化、自動ドキュメント生成、自動埋め込み、jsインタプリタなどです。

babelのコンパイルプロセス

解析する

  • 埋め込む
  • パーサーを呼び出してすべてのプラグインを走査する
  • parserOverride コンパイラを見つける
  • JS コードを抽象構文ツリー (略して AST) に変換するパーサー

変身

  • pre (ASTに入るときに操作オブジェクトを初期化)
  • ビジター (操作オブジェクトの初期化後にビジターでメソッドを実行し、traverse を使用して AST ノードを走査し、走査プロセス中にプラグインを実行します)
  • post (AST 終了時に操作オブジェクトを削除)

生成する

  • AST 変換が完了すると、generate によってコードが再生成されます。
  • ターゲットコードが生成されると、sourceMap関連のコードも同時に生成されます。

実際の Vue ソース コードの抽象構文ツリー

通常の vue では、テンプレート構文を作成しますが、それを通常の html 構文にコンパイルする場合、直接コンパイルするのは非常に難しいため、抽象構文ツリーを使用して方向転換する必要があります。 _template syntax_ をASTに変更します。完了後、AST を通常のHTML 構文に変換します。抽象構文ツリーは中間遷移として機能し、コンパイル作業が容易になります。
これはテンプレートの構文です

ここに画像の説明を挿入


これは通常の HTML 構文です

ここに画像の説明を挿入


これは、解析する必要がある抽象構文ツリーです。

ここに画像の説明を挿入


Vue の最下層は、文字列の観点からすべてのテンプレートを検査します。このように見ると、解析された AST は HTML オブジェクトの js バージョンに似ています。抽象構文ツリーは、テンプレートのコンパイルに広く使用されています。テンプレートをコンパイルする場所があるため、抽象構文ツリーが必ず使用されます。
抽象構文ツリーと仮想ノードの関係は何ですか?
テンプレート構文 => 抽象構文ツリー => レンダリング関数 (h 関数) => 仮想ノード => インターフェース

必要なアルゴリズムの予約

ASTポインタのアイデア

【例1】试寻找字符串中,连续重复次数最多的字符‘aaaaaaaaaabbbbbbbbcccddd’
解:
var str = 'aaaaaaaaaaabbbccccccddd';
//指针
var i = 0;
var j = 1;
// 当前重复次数最多的次数
var maxRepeatCount = 0;
// 重复次数最多的字符串
var maxRepeatChar = '';
//当i还在范围内的时候,应该继续寻找
while (i <= str.length - 1) {
    
    
  //看i指向的字符和j指向的字符是不是不相同
  if (str[i] != str[j]) {
    
    
    console.log(i + '和' + j + '之间文字连续相同' + '字母' + str[i] + '它重复了' + (j - i) + '次')
    //和当前重复次数最多的进行比较
    if (j - i > maxRepeatCount) {
    
    
      // 如果当前文字重复次数(j - i)超过了此时的最大值
      // 就让它成为最大值
      maxRepeatCount = j - i;
      // 将i指针只想的字符串存为maxRepeatChar
      maxRepeatChar = str[i];
    }
    // 让指针i追上指针j
    i = j;
  }
  //不管相不相同,j永远要后移
  j++;
}

問題解決のアイデア:
最初に思いつくのは、部分文字列をすべて取り出して長さが同じかどうかを比較することですが、この場合サイクル数が多くなり効率が無駄になります。繰り返されなくなった場合も計算されますので、比較のためにポインタメソッドを紹介します。
C言語でのポインタはポインタではなく添え字であり、C言語でのポインタはメモリ操作が可能で、jsでのポインタは下表の位置になります。
i: 0
j: 1
i と j が指す単語が同じであれば、i は移動せず、j は後ろに移動します
i と j が指す単語が異なる場合は、その間の単語が連続して同じ場合、i を上方向 j に追従させると、j は後方に移動します。
操作結果:

0 と 11 の間で文字 a が 11 回繰り返されます。
11 と 14 の間で b が 3 回繰り返されます。 14 と 20 の間で 6 回繰り返されます。 20 と 23 の間で
6 回繰り返されます。同じ文字 d が連続して含まれるテキストです。 3 回繰り返されます。

再帰的思考

【例1】斐波那契数列,求前N项的和
解:
function fib(n) {
    
    
  return n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2)
}
这其实很容易,但是衍生出来一个思考,代码是否有大量重复计算?应该怎样解决重复计算的问题。
缓存思想用hasOwnProperty方法判断
这样的话总的递归次数减少了,只要命中了缓存,就直接读缓存,不会再引发下一次递归了
解:
var cache = {
    
    }
function fib(n) {
    
    
  if (cache.hasOwnProperty(n)) {
    
    
    return cache[n]
  }
  var v = n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2)
  cache[n] = v;
  return v;
}
【例2】将高维数组 [1, 2, [3, [4, 5], 6], 7, [8], 9] 转换为以下这个对象
{
    
    
  children: [
    {
    
     value: 1 },
    {
    
     value: 2 },
    {
    
    
      children: [
        {
    
     value: 3 },
        {
    
    
          children: [
            {
    
     value: 4 },
            {
    
     value: 5 }
          ]
        },
        {
    
     value: 6 }
      ]
    },
    {
    
     value: 7 },
    {
    
    
      children: [
        {
    
     value: 8 }
      ]
    },
    {
    
     value: 9 }
  ]
}

解:
var arr = [1, 2, 3, [4, 5, 6]]
function convert(arr) {
    
    
  var result = [];
  for (let i = 0; i < arr.length; i++) {
    
    
    if (typeof arr[i] === 'number') {
    
    
      result.push({
    
    
        value: arr[i]
      })
    } else if (Array.isArray(arr[i])) {
    
    
      result.push({
    
    
        children: convert(arr[i])
      })
    }
  }
  return result;
}

// 还有一种更秒的写法不用循环数组,大大减少了递归次数
function convert(item) {
    
    
  var result = [];
  if (typeof item === 'number') {
    
    
    return {
    
    
      value: item
    }
  } else if (Array.isArray(item)) {
    
    
    return {
    
    
      children: item.map(_item => convert(_item))
    }
  }
  return result;
}

スタック構造

配列は線形構造であり、任意の位置にデータを挿入および削除できることは誰もが知っています。しかし、特定の機能を達成するには、この恣意性を制限しなければならない場合があります。スタックとキューは、一般的な制限された線形構造です。
スタックは、後入れ先出しの制限された線形リストです。制限は、挿入および削除操作がテーブルの一方の端 (スタックの最上部と呼ばれ、もう一方の端はスタックの最下部と呼ばれます) でのみ許可されることです。LIFO (後入れ先出し) は、後で入力される要素であり、スタック領域が最初にポップされることを意味します。スタックに新しい要素を挿入することは、プッシュ、プッシュ、またはプッシュとも呼ばれ、新しい要素をスタックの最上位要素の上に配置して、スタックの新しい最上位要素にすることです。スタックからの要素の削除は、スタックの作成またはスタック解除とも呼ばれ、スタックの最上位の要素を削除し、その隣接する要素をスタックの新しい最上位の要素にします。

【例1】smartRepeat智能重复字符串问题
将 '3[abc]' 变为 'abcabcabc''3[2[a]2[b]]' 变成 'aabbaabbaabb''2[1[a]3[b]2[3[c]4[d]]]' 变成 'abbbcccddddcccddddabbbcccddddcccdddd'
function smartRepeat(templateStr) {
    
    
  // 指针下标
  let index = 0
  // 栈一,存放数字
  let stack1 = []
  // 栈二,存放需要重复的字符串
  let stack2 = []
  let tailStr = templateStr
  // 为啥遍历的次数为 length - 1 ? 因为要估计忽略最后的一个 ] 字符串
  while (index < templateStr.length - 1) {
    
    
    // 剩余没处理的字符串
    tailStr = templateStr.substring(index)
    if (/^\d+\[/.test(tailStr)) {
    
    
      // 匹配 "[" 前的数字
      let word = tailStr.match(/^(\d+)\[/)[1]
      // 转为数字类型
      let num = Number(word)
      // 入栈
      stack1.push(num)
      stack2.push('')
      index++
    } else if (/^\w+\]/.test(tailStr)) {
    
    
      // 匹配 "]" 前的需要重复的字符串
      let word = tailStr.match(/^(\w+)\]/)[1]
      // 修改栈二栈顶的字符串
      stack2[stack2.length - 1] = word
      // 让指针后移,word的长度,避免重复计算字符串
      index += word.length
    } else if (tailStr[0] === ']') {
    
    
      // 遇到 [ 字符串就需要出栈了,栈一和栈二同时出栈,栈二出栈的字符串重复栈一出栈的 数字的次数,并赋值到栈二的新栈顶上
      let times = stack1.pop()
      let word = stack2.pop()
      stack2[stack2.length - 1] += word.repeat(times)
      index++
    } else {
    
    
      index++
    }
    // console.log('tailStr', tailStr)

    // console.log('index', index)

    // console.log('stack1', stack1)

    // console.log('stack2', stack2)

  }
  // while结束之后, stack1 和 stack2 中肯定还剩余1项,若不是,则用户输入的格式错误
  if (stack1.length !== 1 || stack2.length !== 1) {
    
    
    throw new Error('输入的字符串有误,请检查')
  } else {
    
    
    return stack2[0].repeat(stack1[0])
  }
}

問題解決のアイデア:

各文字を反復処理する

  • 2 つのスタックを作成する
  • 文字が数字の場合は、その数字をスタック 1 にプッシュし、空の文字列をスタック 2 にプッシュします。
  • この文字が文字の場合、この時点でスタック 2 の一番上の項目をこの文字に変更します
  • 文字が ] の場合、番号はスタック 1 からポップされ、スタック 2 の先頭要素は、スタック 1 から数字がポップされ、スタック 2 がポップされ、スタック 2 の新しい先頭に接合される回数だけ繰り返されます。スタック2


下準備は終わりました。「現実を照らす夢」を使って**テンプレート文字列**をASTツリー構造に変換してみましょう

AST抽象構文ツリーの手書き実装

ここに画像の説明を挿入


テンプレート文字列を解析して AST の
問題解決アイデアを作成する必要があります。

  1. HTML タグ内の Attributes 属性を解析するスタックの考え方は、テンプレート文字列を解析するときに非常に役立ち、ネストされた HTML をすばやく解析できます。
  2. テンプレート文字列をASTツリー構造に変換するアルゴリズムはスタックであり、アルゴリズムリザーブのスタックの考え方を利用します。

HTMLタグ内のattributes属性を解析します。

export default function (attrsString) {
    
    
  let result = []
  if (!attrsString) {
    
    
    return result
  } else {
    
    
    // console.log('attrsString', attrsString)
    // 案例 'class="box" title="标题" data-type="3"'
    let isMatchQuot = false // 是否遇到引号
    // 改变了一下写法,采用双指针来记录 "" 之间走过的字符串
    let i = 0
    let j = 0

    while (j < attrsString.length) {
    
    
      // 当前指针指向的这一项
      const char = attrsString.charAt(j)
      if (char === '"') {
    
    
        // 匹配 " 字符
        isMatchQuot = !isMatchQuot
      } else if (!isMatchQuot && char === ' ') {
    
    
        // 没匹配到 " 字符,而且当前项是空格

        // 尝试拿到 i 和 j 指针之间的目标字符串
        const target = attrsString.substring(i, j).trim()
        // console.console.log(target);
        if (target) {
    
    
          result.push(target)
        }

        // 让指针i 追上 指针j
        i = j
      }
      j++
    }
    // 循环结束,还剩下最后一项属性
    result.push(attrsString.substring(i).trim())

    // filter过滤空字符 
    return result.filter(item => item).map(item => {
    
    
      const res = item.match(/^(.+)="(.+)"$/)
      return {
    
    
        name: res[1],
        value: res[2]
      }
    })
  }
}

アイデア:
attrs 属性については、両側のスペースを削除し、ポインタを使用して各属性の内容を順番に判断してインターセプトし、オブジェクトの形式でキー値を返します。

テンプレート文字列をASTツリー構造に変換します

import parseAttribute from './parseAttribute'

export default function parse(templateString) {
    
    
  let index = 0
  // 未处理的字符串
  let tailStr = templateString
  // 匹配开始的html标签
  const startTagRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/
  // 匹配结束的html标签
  const endTagRegExp = /^\<\/([a-z]+[1-6]?)\>/
  // 抓取结束标签前的文字
  const wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/

  // 准备两个栈
  let stack1 = [] // 存储匹配到的开始html标签
  let stack2 = []
  let result = null

  while (index < templateString.length - 1) {
    
    
    tailStr = templateString.substring(index)

    if (startTagRegExp.test(tailStr)) {
    
    
      // 匹配开始标签
      const res = tailStr.match(startTagRegExp)
      const startTag = res[1]
      const attrsString = res[2]
      // 开始将标记放入到栈1中
      stack1.push(startTag)
      // 将对象推入数组
      stack2.push({
    
     tag: startTag, children: [], attrs: parseAttribute(attrsString)})
      // 得到attrsString的长度
      const attrsStringLength = attrsString ? attrsString.length : 0
      // 指针移动标签的长度 + 2 + attrsString.length
      // 为什么 +2,因为 <> 也占两个长度
      index += startTag.length + 2 + attrsStringLength
    } else if (endTagRegExp.test(tailStr)) {
    
    
      // 匹配结束标签
      const endTag = tailStr.match(endTagRegExp)[1]
      // 栈1和栈2都需要弹栈
      const pop_tag = stack1.pop()
      const pop_obj = stack2.pop()
      // 此时栈1的栈顶的元素肯定和endTag相同
      if (endTag === pop_tag) {
    
    
        if (stack2.length > 0) {
    
    
          stack2[stack2.length - 1].children.push(pop_obj)
        } else if (stack2.length === 0) {
    
    
          // 匹配到结束标签,且stack2出栈完毕,证明已经遍历结束,那么结果就是stack2最后出栈的那一项
          result = pop_obj
        }
      } else {
    
    
        throw new Error(`${
      
      pop_tag}便签没有封闭!!`)
      }
      // 指针移动标签的长度 + 3,为什么 +3,因为 </> 也占三个长度
      index += endTag.length + 3
    } else if (wordRegExp.test(tailStr)) {
    
    
      // 识别遍历到的这个字符,是不是文字,并且不能全是空字符
      const word = tailStr.match(wordRegExp)[1]
      if (!/^\s+$/.test(word)) {
    
    
        stack2[stack2.length - 1].children.push({
    
    
          text: word,
          type: '3'
        })
      }
      index += word.length
    } else {
    
    
      index++
    }
  }

  return result
}

アイデア:
移動ポインタを設定して、残りの文字列が開始ラベルか終了ラベルかテキストかを決定し、ラベル名とコンテナを 2 つのスタックに格納します。開始ラベルが見つかると、ラベルはスタック 1 にプッシュされます。生成されたコンテナはスタック 2 にプッシュされ、テキストが埋められます。スタック 2 の先頭にあるコンテンツは、閉じられると、スタックの先頭にある前のコンテナに移動されます。

import parse from './parse'

const templateString = `
<div>
    <h3 class="box" title="标题" data-id="1">模拟字符串</h3>
    <ul>
        <li>A</li>
        <li>B</li>
        <li>C</li>
    </ul>
</div>
`
console.log('输入的模板字符串', templateString);
const ast = parse(templateString)
console.log('生成的AST\n', ast)

要約する

上記の記事は、Vue の AST 抽象構文ツリーの主要な部分です。最も興味深い部分は、スタックとダブル ポインタの問題解決のアイデアです。この記事を読んで、次のアルゴリズムについて新たな理解を得ることができれば幸いです。スタックとポインタ。

おすすめ

転載: blog.csdn.net/gaojinbo0531/article/details/129355082