一起來看看Babel到底執行了什麼?

大雄45發表於2021-09-17
導讀 babel對於大多數前端開發人員來說,不陌生,但是背後的原理是黑盒。我們需要了解babel背後的原理在我們開發中廣泛應用。

一起來看看Babel到底執行了什麼?一起來看看Babel到底執行了什麼?
babel對於大多數前端開發人員來說,不陌生,但是背後的原理是黑盒。
我們需要了解babel背後的原理在我們開發中廣泛應用。

一、babel簡單應用
[1,2,3].map(n => n+1);

經過babel轉譯之後,程式碼變成這樣

[1,2,3].map(function(n){ 
  return n + 1; 
})

那我們應該知道了babel定位:babel將ES6新引進的語法轉換為瀏覽器可以執行的ES5語法。

二、babel背後

babel過程:解析----轉換---生成。
一起來看看Babel到底執行了什麼?一起來看看Babel到底執行了什麼?
babel背後過程
我們看到一個叫AST(抽象語法樹)的東西。
主要三個過程:

  1. 解析:將程式碼(字串)轉換為AST(抽象語法樹)。
  2. 轉換:訪問AST的節點進行變化操作生成新的AST。
  3. 生成:以新的AST為基礎生成程式碼
三、過程1:程式碼解析(parse)

程式碼解析(parse)將一段程式碼解析成一個資料結構。其中主要關鍵步驟:

  1. 詞法分析:程式碼(字串)分割成token流。即語法單元組成的陣列。
  2. 語法分析:分析token流(生成的陣列)生成AST。
3.1詞法分析

詞法分析,首先明白JS中哪些屬於語法單元?

  1. 數字:js中科學計數法以及普通陣列都是語法單元。
  2. 括號:(和)只要出現,不管意義都算是語法單元。
  3. 識別符號:連續字元,常見變數,常量,關鍵字等等
  4. 運算子:+,-,*,/等。
  5. 註釋和中括號。

我們來看一下簡單的詞法分析器(Tokenizer)

// 詞法分析器,接收字串返回token陣列 
export const tokenizer = (code) => { 
    // 儲存 token 的陣列 
    const tokens  = []; 
    // 指標 
    let current = 0; 
    while (current < code.length) { 
        // 獲取指標指向的字元 
        const char = code[current]; 
        // 我們先處理單字元的語法單元 類似於`;` `(` `)`等等這種 
        if (char === '(' || char === ')') { 
            tokens.push({ 
                type: 'parens', 
                value: char, 
            }); 
            current ++; 
            continue; 
        } 
        // 我們接著處理識別符號,識別符號一般為以字母、_、$開頭的連續字元 
        if (/[a-zA-Z\$\_]/.test(char)) { 
            let value = ''; 
            value += char; 
            current ++; 
            // 如果是連續字那麼將其拼接在一起,隨後指標後移 
            while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) { 
                value += code[current]; 
                current ++; 
            } 
            tokens.push({ 
                type: 'identifier', 
                value, 
            }); 
            continue; 
        } 
        // 處理空白字元 
        if (/\s/.test(char)) { 
            let value = ''; 
            value += char; 
            current ++; 
            //道理同上 
            while (/\s]/.test(code[current]) && current < code.length) { 
                value += code[current]; 
                current ++; 
            } 
            tokens.push({ 
                type: 'whitespace', 
                value, 
            }); 
            continue; 
        } 
        // 處理逗號分隔符 
        if (/,/.test(char)) { 
            tokens.push({ 
                type: ',', 
                value: ',', 
            }); 
            current ++; 
            continue; 
        } 
        // 處理運算子 
        if (/=|\+|>/.test(char)) { 
            let value = ''; 
            value += char; 
            current ++; 
            while (/=|\+|>/.test(code[current])) { 
                value += code[current]; 
                current ++; 
            } 
            // 當 = 後面有 > 時為箭頭函式而非運算子 
            if (value === '=>') { 
                tokens.push({ 
                    type: 'ArrowFunctionExpression', 
                    value, 
                }); 
                continue; 
            } 
            tokens.push({ 
                type: 'operator', 
                value, 
            }); 
            continue; 
        } 
        // 如果碰到我們詞法分析器以外的字元,則報錯 
        throw new TypeError('I dont know what this character is: ' + char); 
    } 
    return tokens; 
};

上述的這個詞法分析器:主要是針對例子的箭頭函式。

3.2語法分析

語法分析之所以複雜,是因為要分析各種語法的可能性,需要開發者根據token流(上一節我們生成的 token 陣列)提供的資訊來分析出程式碼之間的邏輯關係,只有經過詞法分析 token 流才能成為有結構的抽象語法樹.

做語法分析最好依照標準,大多數 JavaScript Parser 都遵循estree規範

1、語句(Statements): 語句是 JavaScript 中非常常見的語法,我們常見的迴圈、if 判斷、異常處理語句、with 語句等等都屬於語句。

2、表示式(Expressions): 表示式是一組程式碼的集合,它返回一個值,表示式是另一個十分常見的語法,函式表示式就是一種典型的表示式,如果你不理解什麼是表示式, MDN上有很詳細的解釋.

3、宣告(Declarations): 宣告分為變數宣告和函式宣告,表示式(Expressions)中的函式表示式的例子用宣告的寫法就是下面這樣.

const parser = tokens => { 
    // 宣告一個全時指標,它會一直存在 
    let current = -1; 
    // 宣告一個暫存棧,用於存放臨時指標 
    const tem = []; 
    // 指標指向的當前token 
    let token = tokens[current]; 
    const parseDeclarations = () => { 
        // 暫存當前指標 
        setTem(); 
        // 指標後移 
        next(); 
        // 如果字元為'const'可見是一個宣告 
        if (token.type === 'identifier' && token.value === 'const') { 
            const declarations = { 
                type: 'VariableDeclaration', 
                kind: token.value 
            }; 
            next(); 
            // const 後面要跟變數的,如果不是則報錯 
            if (token.type !== 'identifier') { 
                throw new Error('Expected Variable after const'); 
            } 
            // 我們獲取到了變數名稱 
            declarations.identifierName = token.value; 
            next(); 
            // 如果跟著 '=' 那麼後面應該是個表示式或者常量之類的,額外判斷的程式碼就忽略了,直接解析函式表示式 
            if (token.type === 'operator' && token.value === '=') { 
                declarations.init = parseFunctionExpression(); 
            } 
            return declarations; 
        } 
    }; 
    const parseFunctionExpression = () => { 
        next(); 
        let init; 
        // 如果 '=' 後面跟著括號或者字元那基本判斷是一個表示式 
        if ( 
            (token.type === 'parens' && token.value === '(') || 
            token.type === 'identifier' 
        ) { 
            setTem(); 
            next(); 
            while (token.type === 'identifier' || token.type === ',') { 
                next(); 
            } 
            // 如果括號後跟著箭頭,那麼判斷是箭頭函式表示式 
            if (token.type === 'parens' && token.value === ')') { 
                next(); 
                if (token.type === 'ArrowFunctionExpression') { 
                    init = { 
                        type: 'ArrowFunctionExpression', 
                        params: [], 
                        body: {} 
                    }; 
                    backTem(); 
                    // 解析箭頭函式的引數 
                    init.params = parseParams(); 
                    // 解析箭頭函式的函式主體 
                    init.body = parseExpression(); 
                } else { 
                    backTem(); 
                } 
            } 
        } 
        return init; 
    }; 
    const parseParams = () => { 
        const params = []; 
        if (token.type === 'parens' && token.value === '(') { 
            next(); 
            while (token.type !== 'parens' && token.value !== ')') { 
                if (token.type === 'identifier') { 
                    params.push({ 
                        type: token.type, 
                        identifierName: token.value 
                    }); 
                } 
                next(); 
            } 
        } 
        return params; 
    }; 
    const parseExpression = () => { 
        next(); 
        let body; 
        while (token.type === 'ArrowFunctionExpression') { 
            next(); 
        } 
        // 如果以(開頭或者變數開頭說明不是 BlockStatement,我們以二元表示式來解析 
        if (token.type === 'identifier') { 
            body = { 
                type: 'BinaryExpression', 
                left: { 
                    type: 'identifier', 
                    identifierName: token.value 
                }, 
                operator: '', 
                right: { 
                    type: '', 
                    identifierName: '' 
                } 
            }; 
            next(); 
            if (token.type === 'operator') { 
                body.operator = token.value; 
            } 
            next(); 
            if (token.type === 'identifier') { 
                body.right = { 
                    type: 'identifier', 
                    identifierName: token.value 
                }; 
            } 
        } 
        return body; 
    }; 
    // 指標後移的函式 
    const next = () => { 
        do { 
            ++current; 
            token = tokens[current] 
                ? tokens[current] 
                : { type: 'eof', value: '' }; 
        } while (token.type === 'whitespace'); 
    }; 
    // 指標暫存的函式 
    const setTem = () => { 
        tem.push(current); 
    }; 
    // 指標回退的函式 
    const backTem = () => { 
        current = tem.pop(); 
        token = tokens[current]; 
    }; 
    const ast = { 
        type: 'Program', 
        body: [] 
    }; 
    while (current < tokens.length) { 
        const statement = parseDeclarations(); 
        if (!statement) { 
            break; 
        } 
        ast.body.push(statement); 
    } 
    return ast; 
};
四、過程2:程式碼轉換

程式碼解析和程式碼生成是babel。
程式碼轉換是babel外掛
比如taro就是用babel完成小程式語法轉換。

程式碼轉換的關鍵是根據當前的抽象語法樹,以我們定義的規則生成新的抽象語法樹。轉換的過程就是新的抽象語法樹生成過程。

程式碼轉換的具體過程:

遍歷抽象語法樹(實現遍歷器traverser)
程式碼轉換(實現轉換器transformer)

五、過程3:程式碼轉換生成程式碼(實現生成器generator)

生成程式碼這一步實際上是根據我們轉換後的抽象語法樹來生成新的程式碼,我們會實現一個函式, 他接受一個物件( ast),透過遞迴生成最終的程式碼

六、核心原理

Babel 的核心程式碼是 babel-core 這個 package,Babel 開放了介面,讓我們可以自定義 Visitor,在AST轉換時被呼叫。所以 Babel 的倉庫中還包括了很多外掛,真正實現語法轉換的其實是這些外掛,而不是 babel-core 本身。

原文來自: https://www.linuxprobe.com/babel-execution.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2792687/,如需轉載,請註明出處,否則將追究法律責任。

相關文章