バベル(AST)で優雅に0.1+0.2を解決!=0.3の質問


前言
0.1+0.2がどれくらいに等しいか知っていますか.0.1+0.7、0.8-0.2は?このような問題と同様に、外部ライブラリの導入や計算関数の定義にかかわらず、計算の代わりに関数を利用することが最終的な目的である多くの解決策が存在しています.例えば、下落率の上昇率の計算式:( - )/ *100 + '%'実際のコード:Mul(Div(Sub( , ), ), 100) + '%'です.もともと分かりやすい四則演算の計算式は、コードの可読性があまり友好的ではなく、書くのも思考習慣に合わない.したがってbabelおよびAST構文ツリーを用いて,コード構築中に+ - * /などの記号を書き換え,開発時に直接0.1+0.2という形でコードを記述し,構築中にAdd(0.1, 0.2)にコンパイルすることで,開発者が感知せずに計算ミスを解決し,コードの可読性を向上させる.
の準備を
まず、なぜ0.1+0.20.3に等しくないのかを理解します.
転送ゲート:JavaScript浮動小数点数計算精度の問題(例えば0.1+0.2!=0.3)を避ける方法
上の文章はとても详しくて、私は通俗的な言叶で概括します:私达の日常生活用の数字はすべて10 で、しかも10 は脳の思考のロジックに合って、コンピュータは2 のカウント方式を使っています.しかし、2つの異なる基数のカウントルールでは、すべての数が別のカウントルールの有限桁数の数に対応できるわけではありません(比較的言いにくいので、正確に記述されていないかもしれませんが、このような意味です).
十進法では0.110^-1つまり0.1を表し、バイナリでは0.12^-1つまり0.5を表す.
例えば10進法では1/3の表現が0.33333(無限ループ)であり、3進法では0.1を表す.3^-1が0.3333333であるからである.このような演算10進法では0.1が2進法では0.000110011......0011...... (0011 )である
babelについて
babelの動作原理は実際にはAST構文ツリーを用いた静的解析であり、例えばlet a = 100babel処理前に翻訳された構文ツリー長のように:
{
    "type": "VariableDeclaration",
    "declarations": [
      {
        "type": "VariableDeclarator",
        "id": {
          "type": "Identifier",
          "name": "a"
        },
        "init": {
          "type": "NumericLiteral",
          "extra": {
            "rawValue": 100,
            "raw": "100"
          },
          "value": 100
        }
      }
    ],
    "kind": "let"
  },

babelは、テキストフォーマットのコードをこのようなjsonオブジェクトに翻訳することで、各異なる属性を遍歴し、再帰的に検索することができ、babelは、各行のコードが何をしているのかを知ることができます.babelプラグインの目的は、コードファイル全体の構文ツリーを再帰的に巡回し、変更する場所を見つけて対応する値に置き換え、コードを翻訳してブラウザに渡して実行することです.例えば、上記のコードのletvarに変更すると、AST.kind = "var"を実行するだけで、ASTは遍歴したオブジェクトになります.
オンライン翻訳AST転送ゲート
ASTノードタイプドキュメント転送ゲート
スタート
babelプラグインの開発プロセスbabel-plugin-handlebookについて
解決すべき問題:
  • 計算polyfillの作成
  • 変更が必要なコードブロックを特定
  • 現在のファイルを導入する必要があると判断したpolyfill(オンデマンド導入)
  • polyfillの作成
    polyfillは主に4つの関数を提供して、それぞれプラス、マイナス、乗算、除算の演算を置き換える必要があります.同時に、計算パラメータのデータ型を判断する必要があります.データ型がnumberでない場合は、元の計算方式を採用します.
    accAdd
    function accAdd(arg1, arg2) {
        if(typeof arg1 !== 'number' || typeof arg2 !== 'number'){
            return arg1 + arg2;
        }
        var r1, r2, m, c;
        try {
            r1 = arg1.toString().split(".")[1].length;
        }
        catch (e) {
            r1 = 0;
        }
        try {
            r2 = arg2.toString().split(".")[1].length;
        }
        catch (e) {
            r2 = 0;
        }
        c = Math.abs(r1 - r2);
        m = Math.pow(10, Math.max(r1, r2));
        if (c > 0) {
            var cm = Math.pow(10, c);
            if (r1 > r2) {
                arg1 = Number(arg1.toString().replace(".", ""));
                arg2 = Number(arg2.toString().replace(".", "")) * cm;
            } else {
                arg1 = Number(arg1.toString().replace(".", "")) * cm;
                arg2 = Number(arg2.toString().replace(".", ""));
            }
        } else {
            arg1 = Number(arg1.toString().replace(".", ""));
            arg2 = Number(arg2.toString().replace(".", ""));
        }
        return (arg1 + arg2) / m;
    }

    accSub
    function accSub(arg1, arg2) {
        if(typeof arg1 !== 'number' || typeof arg2 !== 'number'){
            return arg1 - arg2;
        }
        var r1, r2, m, n;
        try {
            r1 = arg1.toString().split(".")[1].length;
        }
        catch (e) {
            r1 = 0;
        }
        try {
            r2 = arg2.toString().split(".")[1].length;
        }
        catch (e) {
            r2 = 0;
        }
        m = Math.pow(10, Math.max(r1, r2)); 
        n = (r1 >= r2) ? r1 : r2;
        return Number(((arg1 * m - arg2 * m) / m).toFixed(n));
    }

    accMul
    function accMul(arg1, arg2) {
        if(typeof arg1 !== 'number' || typeof arg2 !== 'number'){
            return arg1 * arg2;
        }
        var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
        try {
            m += s1.split(".")[1].length;
        }
        catch (e) {
        }
        try {
            m += s2.split(".")[1].length;
        }
        catch (e) {
        }
        return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
    }

    accDiv
    function accDiv(arg1, arg2) {
        if(typeof arg1 !== 'number' || typeof arg2 !== 'number'){
            return arg1 / arg2;
        }
        var t1 = 0, t2 = 0, r1, r2;
        try {
            t1 = arg1.toString().split(".")[1].length;
        }
        catch (e) {
        }
        try {
            t2 = arg2.toString().split(".")[1].length;
        }
        catch (e) {
        }
        r1 = Number(arg1.toString().replace(".", ""));
        r2 = Number(arg2.toString().replace(".", ""));
        return (r1 / r2) * Math.pow(10, t2 - t1);
    }

    原理:浮動小数点数を整数に変換して計算します.
    位置決めコードブロック
    babelプラグインの開発プロセスbabel-plugin-handlebookについて
    babelのプラグインの導入方法は2つあります.
  • 通過.babelrcファイル導入プラグイン
  • babel-loaderのoptions属性によるplugins
  • の導入
    babel-pluginは関数を受け入れ、関数はbabelパラメータを受信し、パラメータはbableの一般的な構造方法などの属性を含み、関数の戻り結果は以下のようなオブジェクトでなければならない.
    {
        visitor: {
            //...
        }
    }

    visitorはASTの1つの遍歴ルックアップ器で、babelは深さでAST構文ツリーを優先的に遍歴しようとします.visitorの中の属性のkeyは操作が必要なASTノード名です.例えば、VariableDeclarationBinaryExpressionなどです.value値は関数またはオブジェクトとすることができます.完全な例は以下の通りです.
    {
        visitor: {
            VariableDeclaration(path){
                //doSomething
            },
            BinaryExpression: {
                enter(path){
                    //doSomething
                }
                exit(path){
                    //doSomething
                }
            }
        }
    }

    関数パラメータpathには、現在のノードオブジェクトや、一般的なノード遍歴方法などのプロパティが含まれています.babel遍歴AST構文ツリーは深さ優先であり,あるサブリーフノード(ブランチの最終端)に遍歴器が遍歴すると祖先ノードに遡って遍歴操作を継続するため,各ノードは2回遍歴される.visitorの属性の値が関数である場合、この関数は最初にノードに入ったときに実行され、値がオブジェクトである場合、それぞれ2つのenter,exit属性(オプション)が受信され、それぞれ遡及フェーズに入って実行される.
    As we traverse down each branch of the tree we eventually hit dead ends where we need to traverse back up the tree to get to the next node. Going down the tree we enter each node, then going back up we exit each node.
    コードで置換する必要があるコードブロックはa + bのようなタイプであるため、このタイプのノードはBinaryExpressionであることがわかり、このタイプのノードをaccAdd(a, b)に置換する必要がある.AST構文ツリーは以下の通りである.
    {
            "type": "ExpressionStatement",
            },
            "expression": {
              "type": "CallExpression",
              },
              "callee": {
                "type": "Identifier",
                "name": "accAdd"
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "a"
                },
                {
                  "type": "Identifier",
                  "name": "b"
                }
              ]
            }
          }

    したがって、この構文ツリーを構築してノードを置き換えるだけでいいので、babelは簡単な構築方法を提供し、babel.templateを利用して、あなたが望むノードを簡単に構築することができます.この関数は、コードプレースホルダとして大文字を使用したコード文字列パラメータを受信します.この関数は、コードプレースホルダを置換するためのパラメータとしてオブジェクトを受信する置換関数を返します.
    var preOperationAST = babel.template('FUN_NAME(ARGS)');
    var AST = preOperationAST({
        FUN_NAME: babel.types.identifier(replaceOperator), //   
        ARGS: [path.node.left, path.node.right] //  
    })

    ASTは最終的に置換する必要がある文法ツリーですbabel.typesはノード作成方法の集合であり,各ノードの作成方法が含まれている.
    最後にpath.replaceWithを使用してノードを置換
    BinaryExpression: {
        exit: function(path){
            path.replaceWith(
                preOperationAST({
                    FUN_NAME: t.identifier(replaceOperator),
                    ARGS: [path.node.left, path.node.right]
                })
            );
        }
    },

    導入の必要性を判断する方法
    ノードループが完了した後、ファイルにいくつかのメソッドを導入する必要があることを知る必要があります.そのため、現在のファイルで使用されているメソッドをキャッシュする配列を定義し、ノードループがヒットしたときに要素を追加する必要があります.
    var needRequireCache = [];
    ...
        return {
            visitor: {
                BinaryExpression: {
                    exit(path){
                        needRequireCache.push(path.node.operator)
                        //  path.node.operator   needRequireCache    
                        ...
                    }
                }
            }
        }
    ...

    AST遍歴が完了して最後に終了するノードはProgramexitメソッドに違いないので、このメソッドではpolyfillを参照することができます.babel.template構築ノードを使用して参照を挿入することもできます.
    var requireAST = template('var PROPERTIES = require(SOURCE)');
    ...
        function preObjectExpressionAST(keys){
            var properties = keys.map(function(key){
                return babel.types.objectProperty(t.identifier(key),t.identifier(key), false, true);
            });
            return t.ObjectPattern(properties);
        }
    ...
        Program: {
            exit: function(path){
                path.unshiftContainer('body', requireAST({
                    PROPERTIES: preObjectExpressionAST(needRequireCache),
                    SOURCE: t.stringLiteral("babel-plugin-arithmetic/src/calc.js")
                }));
                needRequireCache = [];
            }
        },
    ...
    path.unshiftContainerの役割は、現在の構文ツリーにノードを挿入することです.したがって、最後の効果はこのようになります.
    var a = 0.1 + 0.2;
    //0.30000000000000004
        ↓ ↓ ↓ ↓ ↓ ↓
    var { accAdd } = require('babel-plugin-arithmetic/src/calc.js');
    var a = accAdd(0.1, 0.2);
    //0.3
    var a = 0.1 + 0.2;
    var b = 0.8 - 0.2;
    //0.30000000000000004
    //0.6000000000000001
        ↓ ↓ ↓ ↓ ↓ ↓
    var { accAdd, accSub } = require('babel-plugin-arithmetic/src/calc.js');
    var a = accAdd(0.1, 0.2);
    var a = accSub(0.8, 0.2);
    //0.3
    //0.6

    完全なコードの例
    Githubプロジェクトアドレス
    使用方法:
    npm install babel-plugin-arithmetic --save-dev

    プラグインを追加/.babelrc
    {
        "plugins": ["arithmetic"]
    }

    または
    /webpack.config.js
    ...
    {
        test: /\.js$/,
        loader: 'babel-loader',
        option: {
            plugins: [
                require('babel-plugin-arithmetic')
            ]
        },
    },
    ...

    皆さんstarへようこそ⭐⭐⭐⭐⭐,何かアドバイスがあればissueを歓迎します.
    リファレンスドキュメント
    JavaScript浮動小数点数計算精度の問題を回避する方法(0.1+0.2!=0.3など)
    AST explorer
    @babel/types
    babel-plugin-handlebook