前言你了解过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)上面的文章写的很详细,我用通俗的来总结重点语言:我们日常生活中使用的数字都是十进制的,十进制符合大脑的思维逻辑,而计算机采用的是二进制计数方式。但是,在两个不同基数的计数规则中,并不是所有的数都能对应到另一个计数规则中位数有限的数(有点啰嗦,描述的可能不准确,但是意思是这样的)。十进制的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语法树进行静态分析,例如leta=100在babel处理前先翻译语法树是这样的:{“类型”:“VariableDeclaration”,“声明”:[{“类型”:“VariableDeclarator”,“id”:{“类型”:“标识符”,“名称”:“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在当前文件中导入(importingasneeded)polyfillpolyfill主要需要提供四个函数来代替加减乘除运算,还需要判断计算参数的数据类型。如果数据类型不是number,则使用原来的计算方法:accAddfunctionaccAdd(arg1,arg2){if(typeofarg1!=='number'||typeofarg2!=='number'){returnarg1+arg2;}变量r1,r2,m,c;试试{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));如果(c>0){varcm=Math.pow(10,c);如果(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;}accSubfunctionaccSub(arg1,arg2){if(typeofarg1!=='number'||typeofarg2!=='number'){returnarg1-arg2;}变量r1,r2,m,n;试试{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;returnNumber(((arg1*m-arg2*m)/m).toFixed(n));}accMulfunctionaccMul(arg1,arg2){if(typeofarg1!=='number'||typeofarg2!=='数'){返回arg1*arg2;}varm=0,s1=arg1.toString(),s2=arg2.toString();尝试{m+=s1.split(".")[1].length;}catch(e){}try{m+=s2.split(".")[1].length;}catch(e){}returnNumber(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m);}accDivfunctionaccDiv(arg1,arg2){if(typeofarg1!=='number'||typeofarg2!=='number'){返回arg1/arg2;}vart1=0,t2=0,r1,r2;尝试{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-handle本书babel介绍插件的方式有两种:通过.babelrc文件导入插件和通过babel-loader的options属性导入插件包含bable常用构造方法等属性,函数的返回结果必须是如下对象:{visitor:{//...}}visitor是AST的遍历查找器,babel会尝试遍历AST语法treeindepthfirst,visitor中属性的key是需要操作的AST节点的名字,比如VariableDeclaration,BinaryExpression等,value值可以是函数也可以是对象。完整的例子如下:{visitor:{VariableDeclaration(path){//doSomething},BinaryExpression:{enter(path){//doSomething}exit(path){//doSomething}}}}函数参数路径包含当前节点对象,常用节点遍历方法等属性。Babel以深度优先的方式遍历AST语法树。当遍历器遍历到某个子叶节点(分支的末端)时,会回溯到祖先节点继续遍历操作,所以每个节点都会遍历两次。当visitor属性的值为函数时,第一次进入该节点时会执行该函数。当值为对象时,会接收两个enter和exit属性(可选),分别在entry和backtracking阶段执行。当我们向下遍历树的每个分支时,我们最终会遇到死胡同,我们需要向上遍历树才能到达下一个节点。沿着树向下我们进入每个节点,然后返回我们退出每个节点。代码中需要替换的代码块是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可以轻松构建任何你想要的节点。该函数接收一个代码字符串参数,该参数使用大写字符作为代码占位符。此函数返回一个替换函数,该函数接收一个对象作为参数来替换代码占位符。varpreOperationAST=babel.template('FUN_NAME(ARGS)');varAST=preOperationAST({FUN_NAME:babel.types.identifier(replaceOperator),//方法名ARGS:[path.node.left,path.node.right]//Parameters})AST就是最后需要替换的语法树。babel.types是节点创建方法的集合,里面包含了每个节点的创建方法。最后,使用path.replaceWith替换节点BinaryExpression:{exit:function(path){path.replaceWith(preOperationAST({FUN_NAME:t.identifier(replaceOperator),ARGS:[path.node.left,path.node.right]}));}},确定要引入的方法节点遍历完成后,需要知道文件中需要引入多少个方法,所以需要定义一个数组来缓存当前文件使用的方法,并插入当节点遍历点击添加元素时。varneedRequireCache=[];...return{visitor:{BinaryExpression:{exit(path){needRequireCache.push(path.node.operator)//根据path.node.operator向needRequireCache添加元素...}}}}...AST遍历后的最后一个exit节点一定是Program的exit方法,所以可以在这个方法中引用polyfill。您还可以使用babel.template来构建节点插入引用:varrequireAST=template('varPROPERTIES=require(SOURCE)');...返回babel.types.objectProperty(t.identifier(key),t.identifier(key),false,true);});返回t.ObjectPattern(属性);}...程序:{exit:function(path){path.unshiftContainer('body',requireAST({PROPERTIES:preObjectExpressionAST(needRequireCache),SOURCE:t.stringLiteral("babel-plugin-arithmetic/src/calc.js")}));needRequireCache=[];}},...path.unshiftContainer是在当前语法树中插入节点,所以最后的效果是这样的:vara=0.1+0.2;//0.30000000000000004↓↓↓↓↓↓var{accAdd}=require('babel-plugin-arithmetic/src/calc.js');vara=accAdd(0.1,0.2);//0.3vara=0.1+0.2;varb=0.8-0.2;//0.30000000000000004//0.60000000000000001↓↓↓↓↓↓var{accAdd,accSub}=require('babel-plugin-arithmetic/src/calc.js');vara=accAdd(0.1,0.2);vara=accSub(0.8,0.2);//0.3//0.6完整代码示例Github项目地址:npminstallbabel-plugin-arithmetic--save-devaddplugin/.babelrc{"plugins":["arithmetic"]}or/webpack.config.js...{test:/\.js$/,loader:'babel-loader',option:{plugins:[require('babel-plugin-arithmetic')]},},...欢迎大家star?????,有的请随意提出任何建议。我参考了关于如何避免JavaScript浮点计算精度问题的文档(例如0.1+0.2!==0.3)ASTexplorer@babel/typesbabel-plugin-handlebook
