你了解过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)
,从而在开发人员无感知的状况下解决计算失精的问题,提高代码的可读性。javascript
首先了解一下为何会出现0.1+0.2
不等于0.3
的状况:java
传送门:如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)
上面的文章讲的很详细了,我用通俗点的语言归纳一下:
咱们平常生活用的数字都是10进制
的,而且10进制
符合大脑思考逻辑,而计算机使用的是2进制
的计数方式。可是在两个不一样基数的计数规则中,其中并非全部的数都能对应另一个计数规则里有限位数的数(比较拗口,可能描述的不太准确,可是意思就是这个样子)。 node
在十进制中的0.1
表示是10^-1
也就是0.1,在二进制中的0.1
表示是2^-1
也就是0.5。webpack
例如在十进制中1/3的表现方式为0.33333(无限循环),而在3进制中的表示为0.1,由于3^-1就是0.3333333……
按照这种运算十进制中的0.1在二进制的表示方式为0.000110011......0011...... (0011无限循环)
git
babel的工做原理实际上就是利用AST语法树来作的静态分析,例如let a = 100
在babel处理以前翻译成的语法树长这样:github
{ "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为遍历获得的对象。web
在线翻译AST传送门
AST节点类型文档传送门
了解babel插件的开发流程 babel-plugin-handlebook
咱们须要解决的问题:express
polyfill主要须要提供四个函数分别用于替换加、减、乘、除的运算,同时还须要判断计算参数数据类型,若是数据类型不是number则采用本来的计算方式: npm
accAddjson
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的插件引入方式有两种:
babel-plugin接受一个函数,函数接收一个babel参数,参数包含bable经常使用构造方法等属性,函数的返回结果必须是如下这样的对象:
{ visitor: { //... } }
visitor是一个AST的一个遍历查找器,babel会尝试以深度优先遍历AST语法树,visitor里面的属性的key为须要操做的AST节点名如VariableDeclaration
、BinaryExpression
等,value值可为一个函数或者对象,完整示例以下:
{ visitor: { VariableDeclaration(path){ //doSomething }, BinaryExpression: { enter(path){ //doSomething } exit(path){ //doSomething } } } }
函数参数path包含了当前节点对象,以及经常使用节点遍历方法等属性。
babel遍历AST语法树是以深度优先,当遍历器遍历至某一个子叶节点(分支的最终端)的时候会进行回溯到祖先节点继续进行遍历操做,所以每一个节点会被遍历到2次。当visitor的属性的值为函数的时候,该函数会在第一次进入该节点的时候执行,当值为对象的时候分别接收两个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遍历完毕最后退出的节点确定是Program
的exit
方法,所以能够在这个方法里面对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