聊聊Babel背后到底執(zhí)行了什么?
babel背后到底執(zhí)行了什么?
babel對于大多數(shù)前端開發(fā)人員來說,不陌生,但是背后的原理是黑盒。
我們需要了解babel背后的原理在我們開發(fā)中廣泛應用。
一、babel簡單應用
- [1,2,3].map(n => n+1);
經(jīng)過babel轉譯之后,代碼變成這樣
- [1,2,3].map(function(n){
- return n + 1;
- })
那我們應該知道了babel定位:babel將ES6新引進的語法轉換為瀏覽器可以運行的ES5語法。
二、babel背后
babel過程:解析----轉換---生成。
babel背后過程
我們看到一個叫AST(抽象語法樹)的東西。
主要三個過程:
- 解析:將代碼(字符串)轉換為AST(抽象語法樹)。
- 轉換:訪問AST的節(jié)點進行變化操作生成新的AST。
- 生成:以新的AST為基礎生成代碼
三、過程1:代碼解析(parse)
代碼解析(parse)將一段代碼解析成一個數(shù)據(jù)結構。其中主要關鍵步驟:
- 詞法分析:代碼(字符串)分割成token流。即語法單元組成的數(shù)組。
- 語法分析:分析token流(生成的數(shù)組)生成AST。
3.1詞法分析
詞法分析,首先明白JS中哪些屬于語法單元?
- 數(shù)字:js中科學計數(shù)法以及普通數(shù)組都是語法單元。
- 括號:(和)只要出現(xiàn),不管意義都算是語法單元。
- 標識符:連續(xù)字符,常見變量,常量,關鍵字等等
- 運算符:+,-,*,/等。
- 注釋和中括號。
我們來看一下簡單的詞法分析器(Tokenizer)
- // 詞法分析器,接收字符串返回token數(shù)組
- export const tokenizer = (code) => {
- // 儲存 token 的數(shù)組
- const tokens = [];
- // 指針
- let current = 0;
- while (current < code.length) {
- // 獲取指針指向的字符
- const char = code[current];
- // 我們先處理單字符的語法單元 類似于`;` `(` `)`等等這種
- if (char === '(' || char === ')') {
- tokens.push({
- type: 'parens',
- value: char,
- });
- current ++;
- continue;
- }
- // 我們接著處理標識符,標識符一般為以字母、_、$開頭的連續(xù)字符
- if (/[a-zA-Z\$\_]/.test(char)) {
- let value = '';
- value += char;
- current ++;
- // 如果是連續(xù)字那么將其拼接在一起,隨后指針后移
- 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 ++;
- }
- // 當 = 后面有 > 時為箭頭函數(shù)而非運算符
- 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;
- };
上述的這個詞法分析器:主要是針對例子的箭頭函數(shù)。
3.2語法分析
語法分析之所以復雜,是因為要分析各種語法的可能性,需要開發(fā)者根據(jù)token流(上一節(jié)我們生成的 token 數(shù)組)提供的信息來分析出代碼之間的邏輯關系,只有經(jīng)過詞法分析 token 流才能成為有結構的抽象語法樹.
做語法分析最好依照標準,大多數(shù) JavaScript Parser 都遵循estree規(guī)范
1、語句(Statements): 語句是 JavaScript 中非常常見的語法,我們常見的循環(huán)、if 判斷、異常處理語句、with 語句等等都屬于語句。
2、表達式(Expressions): 表達式是一組代碼的集合,它返回一個值,表達式是另一個十分常見的語法,函數(shù)表達式就是一種典型的表達式,如果你不理解什么是表達式, MDN上有很詳細的解釋.
3、聲明(Declarations): 聲明分為變量聲明和函數(shù)聲明,表達式(Expressions)中的函數(shù)表達式的例子用聲明的寫法就是下面這樣.
- 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();
- // 如果跟著 '=' 那么后面應該是個表達式或者常量之類的,額外判斷的代碼就忽略了,直接解析函數(shù)表達式
- 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();
- }
- // 如果括號后跟著箭頭,那么判斷是箭頭函數(shù)表達式
- if (token.type === 'parens' && token.value === ')') {
- next();
- if (token.type === 'ArrowFunctionExpression') {
- init = {
- type: 'ArrowFunctionExpression',
- params: [],
- body: {}
- };
- backTem();
- // 解析箭頭函數(shù)的參數(shù)
- init.params = parseParams();
- // 解析箭頭函數(shù)的函數(shù)主體
- 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;
- };
- // 指針后移的函數(shù)
- const next = () => {
- do {
- ++current;
- token = tokens[current]
- ? tokens[current]
- : { type: 'eof', value: '' };
- } while (token.type === 'whitespace');
- };
- // 指針暫存的函數(shù)
- const setTem = () => {
- tem.push(current);
- };
- // 指針回退的函數(shù)
- 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完成小程序語法轉換。
代碼轉換的關鍵是根據(jù)當前的抽象語法樹,以我們定義的規(guī)則生成新的抽象語法樹。轉換的過程就是新的抽象語法樹生成過程。
代碼轉換的具體過程:
- 遍歷抽象語法樹(實現(xiàn)遍歷器traverser)
- 代碼轉換(實現(xiàn)轉換器transformer)
五、過程3:代碼轉換生成代碼(實現(xiàn)生成器generator)
生成代碼這一步實際上是根據(jù)我們轉換后的抽象語法樹來生成新的代碼,我們會實現(xiàn)一個函數(shù), 他接受一個對象( ast),通過遞歸生成最終的代碼
六、核心原理
Babel 的核心代碼是 babel-core 這個 package,Babel 開放了接口,讓我們可以自定義 Visitor,在AST轉換時被調(diào)用。所以 Babel 的倉庫中還包括了很多插件,真正實現(xiàn)語法轉換的其實是這些插件,而不是 babel-core 本身。