应用babel(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.2不即是0.3的状况:

传送门:怎样避开JavaScript浮点数盘算精度题目(如0.1+0.2!==0.3)

上面的文章讲的很详细了,我用浅显点的言语归纳综合一下:
我们一样平常生活用的数字都是10进制的,而且10进制相符大脑思索逻辑,而盘算机运用的是2进制的计数体式格局。然则在两个差别基数的计数划定规矩中,个中并非一切的数都能对应别的一个计数划定规矩里有限位数的数(比较拗口,能够形貌的不太正确,然则意义就是这个模样)。

在十进制中的0.1示意是10^-1也就是0.1,在二进制中的0.1示意是2^-1也就是0.5。

比方在十进制中1/3的表现体式格局为0.33333(无穷轮回),而在3进制中的示意为0.1,由于3^-1就是0.3333333……
依据这类运算十进制中的0.1在二进制的示意体式格局为0.000110011......0011...... (0011无穷轮回)

相识babel

babel的事情道理现实上就是应用AST语法树来做的静态剖析,比方let a = 100在babel处置惩罚之前翻译成的语法树长如许:

{
    "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插件的目标就是经由过程递归遍历全部代码文件的语法树,找到须要修正的位置并替代成响应的值,然后再翻译回代码交由浏览器去实行。比方我们把上面的代码中的let改成var我们只须要实行AST.kind = "var",AST为遍历获得的对象。

在线翻译AST传送门

AST节点范例文档传送门

最先

相识babel插件的开辟流程 babel-plugin-handlebook

我们须要处置惩罚的题目:

  • 盘算polyfill的编写
  • 定位须要变动的代码块
  • 推断当前文件须要引入的polyfill(按需引入)

polyfill的编写

polyfill重要须要供应四个函数离别用于替代加、减、乘、除的运算,同时还须要推断盘算参数数据范例,假如数据范例不是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的插件引入体式格局有两种:

  • 经由过程.babelrc文件引入插件
  • 经由过程babel-loader的options属性引入plugins

babel-plugin吸收一个函数,函数吸收一个babel参数,参数包括bable经常使用组织要领等属性,函数的返回结果必需是以下如许的对象:

{
    visitor: {
        //...
    }
}

visitor是一个AST的一个遍历查找器,babel会尝试以深度优先遍历AST语法树,visitor内里的属性的key为须要操纵的AST节点名如VariableDeclarationBinaryExpression等,value值可为一个函数或许对象,完全示比方下:

{
    visitor: {
        VariableDeclaration(path){
            //doSomething
        },
        BinaryExpression: {
            enter(path){
                //doSomething
            }
            exit(path){
                //doSomething
            }
        }
    }
}

函数参数path包括了当前节点对象,以及经常使用节点遍历要领等属性。
babel遍历AST语法树是以深度优先,当遍历器遍历至某一个子恭弘=叶 恭弘节点(分支的终究端)的时刻会举行回溯到先人节点继续举行遍历操纵,因而每一个节点会被遍历到2次。当visitor的属性的值为函数的时刻,该函数会在第一次进入该节点的时刻实行,当值为对象的时刻离别吸收两个enterexit属性(可选),离别在进入与回溯阶段实行。

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

    原文作者:JachinZ
    原文地址: https://segmentfault.com/a/1190000019094881
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞