1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        前端應(yīng)該掌握的編譯基礎(chǔ)(基于 babel)

        共 22648字,需瀏覽 46分鐘

         ·

        2021-07-10 14:12

        • 作者:陳大魚頭
        • github: KRISACHAN

        開發(fā)息息相關(guān)

        雖然 Babel 團(tuán)隊在各種哭窮,但是 Babel 始終是我們前端在開發(fā)中不可或缺的重要工具。 雖然我們只是 API 調(diào)用工,但是多了解一些總是會有好處的嘛 ??????

        什么是編譯器?

        編譯器(compiler)是一種計算機(jī)程序,它會將某種編程語言寫成的源代碼(原始語言)轉(zhuǎn)換成另一種編程語言(目標(biāo)語言)。

        源代碼(source code)→ 預(yù)處理器(preprocessor)→ 編譯器(compiler)→ 匯編程序(assembler)→ 目標(biāo)代碼(object code)→ 鏈接器(linker)→ 可執(zhí)行文件(executables),最后打包好的文件就可以給電腦去判讀運(yùn)行了。

        什么是解釋器?

        解釋器(英語:interpreter),是一種計算機(jī)程序,能夠把解釋型語言解釋執(zhí)行。解釋器就像一位“中間人”。解釋器邊解釋邊執(zhí)行,因此依賴于解釋器的程序運(yùn)行速度比較緩慢。解釋器的好處是它不需要重新編譯整個程序,從而減輕了每次程序更新后編譯的負(fù)擔(dān)。相對的編譯器一次性將所有源代碼編譯成二進(jìn)制文件,執(zhí)行時無需依賴編譯器或其他額外的程序。

        跟編譯器的區(qū)別就是一個是邊編譯邊執(zhí)行,一個是編譯完才執(zhí)行。

        高級語言編譯器步驟

        1. 輸入源程序字符流
        2. 詞法分析
        3. 語法分析
        4. 語義分析
        5. 中間代碼生成
        6. 機(jī)器無關(guān)代碼優(yōu)化
        7. 代碼生成
        8. 機(jī)器相關(guān)代碼優(yōu)化
        9. 目標(biāo)代碼生成

        V8 編譯 JS 代碼的過程

        1. 生成抽象語法樹(AST)和執(zhí)行上下文
        2. 第一階段是分詞(tokenize),又稱為詞法分析
        3. 第二階段是解析(parse),又稱為語法分析
        4. 生成字節(jié)碼
        5. 字節(jié)碼就是介于 AST 和機(jī)器碼之間的一種代碼。但是與特定類型的機(jī)器碼無關(guān),字節(jié)碼需要通過解釋器將其轉(zhuǎn)換為機(jī)器碼后才能執(zhí)行。
        6. 執(zhí)行代碼

        JS 執(zhí)行代碼的過程

        • 執(zhí)行全局代碼時,創(chuàng)建全局上下文
        • 調(diào)用函數(shù)時,創(chuàng)建函數(shù)上下文
        • 使用 eval 函數(shù)時,創(chuàng)建 eval 上下文
        • 執(zhí)行局部代碼時,創(chuàng)建局部上下文

        關(guān)于 Babel

        Babel ,又名 Babel.js。 是一個用于 web 開發(fā),且自由開源的 JavaScript 編譯器、轉(zhuǎn)譯器。

        Babel 的編譯流程:

        圖片來源:透過製作 Babel-plugin 初訪 AST

        Parse

        Babel 的第一步就是將源碼轉(zhuǎn)換為抽象語法樹(AST)

        const babel = require('@babel/core');
        const { parseAsync } = babel;
        const parseCode = async (code = '', options = {}) => {
          const res = await parseAsync(code, options);
        };
        parseCode(`
          const a = 1;
        `
        )

        可通過 https://astexplorer.net/ 在線查看具體結(jié)果

        這一步會將收集到的的代碼,通過 詞法分析(Lexical analysis) 跟 語法分析(Parsing) 兩個階段將代碼轉(zhuǎn)換成 AST

        詞法分析(Lexical analysis)

        詞法分析會將代碼轉(zhuǎn)為 token ,可以理解為是對每個不可分割單詞元的描述,例如 const 就會轉(zhuǎn)換成下面這樣:

        Token {
            type: 
                TokenType {
                label: 'const',
                keyword: 'const',
                beforeExpr: false,
                startsExpr: false,
                rightAssociative: false,
                isLoop: false,
                isAssign: false,
                prefix: false,
                postfix: false,
                binop: null,
                updateContext: null
            },
            value: 'const',
            start: 5,
            end: 10,
            loc: 
            SourceLocation {
                start: Position { line: 2, column: 4 },
                end: Position { line: 2, column: 9 },
                filename: undefined,
                identifierName: undefined
            }
        }

        type 就是 對 token 的描述,如果想要查看 bebal 生成的 token,我們可以在 options 里寫入:

        parserOpts: {
          tokenstrue
        }

        關(guān)于 @babel/parser  更多配置,可查看:https://babeljs.io/docs/en/babel-parser#options

        語法分析(Parsing)

        語法分析則是將上述的 token 轉(zhuǎn)換成對應(yīng)的 ast 結(jié)構(gòu)

        所以我們就可以看到這樣的一段樹狀結(jié)構(gòu)(過濾部分信息)

        {
            "type""VariableDeclaration",
            "start"0,
            "end"14,
            "loc": {
                "start": {
                    "line"1,
                    "column"0
                },
                "end": {
                    "line"1,
                    "column"14
                }
            },
            "declarations": [
                {
                    "type""VariableDeclarator",
                    "start"6,
                    "end"13,
                    "loc": {
                        "start": {
                            "line"1,
                            "column"6
                        },
                        "end": {
                            "line"1,
                            "column"13
                        }
                    },
                    "id": {
                        "type""Identifier",
                        "start"6,
                        "end"9,
                        "loc": {
                            "start": {
                                "line"1,
                                "column"6
                            },
                            "end": {
                                "line"1,
                                "column"9
                            },
                            "identifierName""abc"
                        },
                        "name""abc"
                    },
                    "init": {
                        "type""NumericLiteral",
                        "start"12,
                        "end"13,
                        "loc": {
                            "start": {
                                "line"1,
                                "column"12
                            },
                            "end": {
                                "line"1,
                                "column"13
                            }
                        },
                        "extra": {
                            "rawValue"1,
                            "raw""1"
                        },
                        "value"1
                    }
                }
            ],
            "kind""const"
        }

        這樣與 type 同級的結(jié)構(gòu)就叫  節(jié)點(diǎn)(Node) , loc ,startend 則是位置信息

        Transform

        Babel 的第二步就是遍歷 AST,并調(diào)用 transform 以訪問者模式進(jìn)行修改

        export default function (babel{
          const { types: t } = babel;
          
          return {
            name"ast-transform"// not required
            visitor: {
              Identifier(path) {
                path.node.name = path.node.name.split('').reverse().join('');
              }
            }
          };
        }

        通過執(zhí)行上述的 transform ,我們可以有:

        上述功能也可通過 https://astexplorer.net/ 在線查看

        Generate

        Babel 的第三步就是把轉(zhuǎn)換后的 AST 打印成目標(biāo)代碼,并生成 sourcemap

        開發(fā)一個 babel 插件

        前置知識 - 訪問者模式

        訪問者模式: 在訪問者模式(Visitor Pattern)中,我們使用了一個訪問者類,它改變了元素類的執(zhí)行算法。通過這種方式,元素的執(zhí)行算法可以隨著訪問者改變而改變。這種類型的設(shè)計模式屬于行為型模式。根據(jù)模式,元素對象已接受訪問者對象,這樣訪問者對象就可以處理元素對象上的操作。

        知道你們不想看文字描述,所以直接上代碼!

        class 漢堡包 {
            accept(fatBoyVisitor) {
                fatBoyVisitor.visit(this);
            }
        };

        class 薯條 {
            accept(fatBoyVisitor) {
                fatBoyVisitor.visit(this);
            }
        };

        class 炸雞 {
            accept(fatBoyVisitor) {
                fatBoyVisitor.visit(this);
            }
        };

        class FatBoy {
            constructor(foods) {
                this.foods = foods;
            }

            accept(fatBoyFoodVisitor) {
                this.foods.forEach(food => {
                    food.accept(fatBoyFoodVisitor);
                });
            }
        };

        class FatBoyFoodVisitor {
            visit(food) {
                console.log(`肥宅吃了${food.constructor.name}`);
            }
        };

        const fatBoy = new FatBoy([new 漢堡包(), new 薯條(), new 炸雞()]);
        fatBoy.accept(new FatBoyFoodVisitor());

        最終輸出結(jié)果是:

        肥宅吃了漢堡包
        肥宅吃了薯條
        肥宅吃了炸雞

        babel-plugin-transform-object-assign 源碼

        import { declare } from "@babel/helper-plugin-utils";

        export default declare(api => {
          api.assertVersion(7);

          return {
            name"transform-object-assign",

            visitor: {
              CallExpressionfunction(path, file{
                if (path.get("callee").matchesPattern("Object.assign")) {
                  path.node.callee = file.addHelper("extends");
                }
              },
            },
          };
        });

        上面的就是 babel-plugin-transform-object-assign 的源碼。

        • declare:是一個用于簡化創(chuàng)建 transformer 的工具函數(shù)
        • assertVersion:檢查當(dāng)前 babel 的大版本
        • name:當(dāng)前插件的名字
        • visitor:對外提供修改內(nèi)容的訪問者
        • CallExpression:函數(shù)調(diào)用的 type,每一句代碼都會生成對應(yīng)的 type,例如最上面的函數(shù)名 abc 則對應(yīng)的是一個 Identifier 類型,如果需要修改某一個 type 的代碼,則在里面創(chuàng)建對應(yīng)的 type 訪問者進(jìn)行修改即可。

        具體生成的代碼如下:

        // input
        const a = Object.assign({ a1 }, { b2 });

        // output
        "use strict";

        function _extends({ _extends = Object.assign || function (targetfor (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(thisarguments); }

        const a = _extends({
          a1
        }, {
          b2
        });

        Babel 插件實(shí)戰(zhàn) - 清除 console 源碼

        先上代碼:

        const babel = require('@babel/core');
        const get = require('lodash/get');
        const eq = require('lodash/eq');

        const { transformAsync } = babel;

        const removeConsole = rootPath => ({
            visitor: {
                ExpressionStatementpath => {
                    const name = get(path, 'node.expression.callee.object.name');
                    const CONSOLE_PREFIX = 'console';
                    if (!eq(name, CONSOLE_PREFIX)) {
                        return;
                    };
                    path.remove();
                },
            }
        });

        const transformCode = async (code = '') => {
            const res = await transformAsync(code, {
                plugins: [
                    removeConsole,
                ],
            });

            console.log(res.code);
        };

        transformCode(`
            const a = 10;
            console.group('嚶嚶嚶');
            console.log(a);
            console.groupEnd();
        `
        );

        輸出結(jié)果:

        const a = 10;

        上面的功能就是我們在聲明語句類型 ExpressionStatement 中實(shí)現(xiàn)的。

        node.expression 對應(yīng)的是當(dāng)前類型里的子表達(dá)式,在這個場景里,它的 type === 'CallExpression'。

        callee 對應(yīng)的就是一個調(diào)用函數(shù)類型,在這個場景里,它的 type === 'MemberExpression'

        object 對應(yīng)的就是當(dāng)前調(diào)用函數(shù)的前置對象,它的 type === 'Identifier'name 則是 console。

        所以我們的實(shí)現(xiàn)就很簡單了,只要 name === 'console' ,我們就可以通過內(nèi)部暴露的 remove 方法直接刪除當(dāng)前代碼。

        Babel 插件實(shí)戰(zhàn) - 新的語法

        總所周知,JS 不能這么寫

        # python
        arr = [123]
        print(arr[-1]) # 3
        print(arr[len(arr) - 1]) # 3

        但是我們可以用魔法打敗魔法

        作為一個兇起來連自己都可以編譯的語言,這有多難呢~

        具體實(shí)現(xiàn)如下:

        const babel = require('@babel/core');
        const get = require('lodash/get');
        const tailIndex = rootPath => ({
            visitor: {
                MemberExpressionpath => {
                    const {
                        object: obj,
                        property: prop,
                    } = get(path, 'node', {});

                    const isNotMatch = codeNotMatch(obj, prop);
                    if (isNotMatch) {
                        return;
                    };

                    const {
                        index,
                        operator,
                        name,
                    } = createMatchedKeys(obj, prop);

                    if (!index || !name) {
                        return;
                    };

                    const res = genHeadIndex(index, name, operator);

                    path.replaceWithSourceString(res);
                },
            },
        });

        MemberExpression 就是當(dāng)前要處理的語句類型。

        codeNotMatch 是我們自己實(shí)現(xiàn)的函數(shù),用于判斷 node.objectnode.property 是否合法,具體實(shí)現(xiàn)如下:

        const t = require('@babel/types');

        const codeNotMatch = (obj, prop) => {
            const objIsIdentifier = t.isIdentifier(obj);
            const propIsUnaryExpression = t.isUnaryExpression(prop);

            const objNotMatch = !obj || !objIsIdentifier;
            const propNotMatch = !prop || !propIsUnaryExpression;

            return objNotMatch || propNotMatch;
        };

        這里的 require('@babel/types') 是 babel 的一個工具包,這里面我們運(yùn)用了它的語句判斷能力。這種 isXXX 的大體實(shí)現(xiàn)如下:

        function isIdentifier(node, opts{
          if (!node) return false;
          const nodeType = node.type;
          if (nodeType === 具體類型) {
            if (typeof opts === "undefined") {
              return true;
            } else {
              return shallowEqual(node, opts);
            }
          }
          return false;
        }

        上面的 shallowEqual 實(shí)現(xiàn)如下:

        function shallowEqual(actual, expected{
          const keys = Object.keys(expected);

          for (const key of keys) {
            if (actual[key] !== expected[key]) {
              return false;
            }
          }

          return true;
        }

        createMatchedKeys 用于創(chuàng)建最終匹配的字符,即需要將 -1 改為 .length - 1 的形式,所以具體實(shí)現(xiàn)如下:

        const createMatchedKeys = (obj, prop) => {
            const {
                prefix,
                operator,
                argument: arg
            } = prop;

            let index;
            let name;

            const propIsArrayExpression = !!prefix && !!operator && !!arg;
            const argIsNumericLiteral = t.isNumericLiteral(arg);

            if (propIsArrayExpression && argIsNumericLiteral) {
                index = get(arg, 'value');
                name = get(obj, 'name');
            };

            return {
                index,
                operator,
                name,
            };
        };

        這里面一路判斷,匹配即可。

        所以當(dāng)我們拿到下標(biāo) ,操作符 跟 數(shù)組名 之后,直接組合成最終要生成的代碼即可,即有:

        const genHeadIndex = (index, name, operator) => `${name}[${name}.length ${operator} ${index}]`;

        最后我們直接替換源碼即可,怎么替換呢,babel 有通過訪問者模式返回 replaceWithSourceString 方法進(jìn)行硬編碼替換。。。

        替換的邏輯就是先通過  babel.parse  將要替換的代碼生成 ast,然后從 loc 到具體的 node 進(jìn)行替換。

        一個新語法,就這么完成啦~

        參考資料

        1. 透過製作 Babel-plugin 初訪 AST
        2. 詞法分析(Lexical analysis)
        3. 語法分析(Parsing)
        4. https://babeljs.io/docs/en/babel-parser#options
        5. https://astexplorer.net/
        6. https://github.com/babel/babel
        7. https://github.com/babel/minify
        8. 『1W7字中高級前端面試必知必會』終極版
        9. Babel 插件手冊
        瀏覽 52
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            91人人草 | 97精品国产露脸对白 | 99久久精品99国产亚洲卡不ay | 三级片视频看看 | 一本久道无码 | 国内外成人免费视频 | 黑大巨大一区二区三区 | 快乐激情五月天 | 91在线超碰 | 免费观看裸体 |