摘要:因此利用以及語法樹在代碼構(gòu)建過程中重寫等符號,開發(fā)時直接以這樣的形式編寫代碼,在構(gòu)建過程中編譯成,從而在開發(fā)人員無感知的情況下解決計算失精的問題,提升代碼的可讀性。
前言
你了解過0.1+0.2到底等于多少嗎?那0.1+0.7,0.8-0.2呢?
類似于這種問題現(xiàn)在已經(jīng)有了很多的解決方案,無論引入外部庫或者是自己定義計算函數(shù)最終的目的都是利用函數(shù)去代替計算。例如一個漲跌幅百分比的一個計算公式:(現(xiàn)價-原價)/原價*100 + "%"實際代碼:Mul(Div(Sub(現(xiàn)價, 原價), 原價), 100) + "%"。原本一個很易懂的四則運(yùn)算的計算公式在代碼里面的可讀性變得不太友好,編寫起來也不太符合思考習(xí)慣。
因此利用babel以及AST語法樹在代碼構(gòu)建過程中重寫+ - * /等符號,開發(fā)時直接以0.1+0.2這樣的形式編寫代碼,在構(gòu)建過程中編譯成Add(0.1, 0.2),從而在開發(fā)人員無感知的情況下解決計算失精的問題,提升代碼的可讀性。
首先了解一下為什么會出現(xiàn)0.1+0.2不等于0.3的情況:
傳送門:如何避開JavaScript浮點(diǎn)數(shù)計算精度問題(如0.1+0.2!==0.3)
上面的文章講的很詳細(xì)了,我用通俗點(diǎn)的語言概括一下:
我們?nèi)粘I钣玫臄?shù)字都是10進(jìn)制的,并且10進(jìn)制符合大腦思考邏輯,而計算機(jī)使用的是2進(jìn)制的計數(shù)方式。但是在兩個不同基數(shù)的計數(shù)規(guī)則中,其中并不是所有的數(shù)都能對應(yīng)另外一個計數(shù)規(guī)則里有限位數(shù)的數(shù)(比較拗口,可能描述的不太準(zhǔn)確,但是意思就是這個樣子)。
在十進(jìn)制中的0.1表示是10^-1也就是0.1,在二進(jìn)制中的0.1表示是2^-1也就是0.5。
例如在十進(jìn)制中1/3的表現(xiàn)方式為0.33333(無限循環(huán)),而在3進(jìn)制中的表示為0.1,因為3^-1就是0.3333333……
按照這種運(yùn)算十進(jìn)制中的0.1在二進(jìn)制的表示方式為0.000110011......0011...... (0011無限循環(huán))
babel的工作原理實際上就是利用AST語法樹來做的靜態(tài)分析,例如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插件的目的就是通過遞歸遍歷整個代碼文件的語法樹,找到需要修改的位置并替換成相應(yīng)的值,然后再翻譯回代碼交由瀏覽器去執(zhí)行。例如我們把上面的代碼中的let改成var我們只需要執(zhí)行AST.kind = "var",AST為遍歷得到的對象。
在線翻譯AST傳送門開始
AST節(jié)點(diǎn)類型文檔傳送門
了解babel插件的開發(fā)流程 babel-plugin-handlebook
我們需要解決的問題:
計算polyfill的編寫
定位需要更改的代碼塊
判斷當(dāng)前文件需要引入的polyfill(按需引入)
polyfill的編寫polyfill主要需要提供四個函數(shù)分別用于替換加、減、乘、除的運(yùn)算,同時還需要判斷計算參數(shù)數(shù)據(jù)類型,如果數(shù)據(jù)類型不是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); }
原理:將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)來進(jìn)行計算。
定位代碼塊了解babel插件的開發(fā)流程 babel-plugin-handlebook
babel的插件引入方式有兩種:
通過.babelrc文件引入插件
通過babel-loader的options屬性引入plugins
babel-plugin接受一個函數(shù),函數(shù)接收一個babel參數(shù),參數(shù)包含bable常用構(gòu)造方法等屬性,函數(shù)的返回結(jié)果必須是以下這樣的對象:
{ visitor: { //... } }
visitor是一個AST的一個遍歷查找器,babel會嘗試以深度優(yōu)先遍歷AST語法樹,visitor里面的屬性的key為需要操作的AST節(jié)點(diǎn)名如VariableDeclaration、BinaryExpression等,value值可為一個函數(shù)或者對象,完整示例如下:
{ visitor: { VariableDeclaration(path){ //doSomething }, BinaryExpression: { enter(path){ //doSomething } exit(path){ //doSomething } } } }
函數(shù)參數(shù)path包含了當(dāng)前節(jié)點(diǎn)對象,以及常用節(jié)點(diǎn)遍歷方法等屬性。
babel遍歷AST語法樹是以深度優(yōu)先,當(dāng)遍歷器遍歷至某一個子葉節(jié)點(diǎn)(分支的最終端)的時候會進(jìn)行回溯到祖先節(jié)點(diǎn)繼續(xù)進(jìn)行遍歷操作,因此每個節(jié)點(diǎn)會被遍歷到2次。當(dāng)visitor的屬性的值為函數(shù)的時候,該函數(shù)會在第一次進(jìn)入該節(jié)點(diǎn)的時候執(zhí)行,當(dāng)值為對象的時候分別接收兩個enter,exit屬性(可選),分別在進(jìn)入與回溯階段執(zhí)行。
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這樣的類型,因此我們得知該類型的節(jié)點(diǎn)為BinaryExpression,而我們需要把這個類型的節(jié)點(diǎn)替換成accAdd(a, b),AST語法樹如下:
{ "type": "ExpressionStatement", }, "expression": { "type": "CallExpression", }, "callee": { "type": "Identifier", "name": "accAdd" }, "arguments": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ] } }
因此只需要將這個語法樹構(gòu)建出來并替換節(jié)點(diǎn)就行了,babel提供了簡便的構(gòu)建方法,利用babel.template可以方便的構(gòu)建出你想要的任何節(jié)點(diǎn)。這個函數(shù)接收一個代碼字符串參數(shù),代碼字符串中采用大寫字符作為代碼占位符,該函數(shù)返回一個替換函數(shù),接收一個對象作為參數(shù)用于替換代碼占位符。
var preOperationAST = babel.template("FUN_NAME(ARGS)"); var AST = preOperationAST({ FUN_NAME: babel.types.identifier(replaceOperator), //方法名 ARGS: [path.node.left, path.node.right] //參數(shù) })
AST就是最終需要替換的語法樹,babel.types是一個節(jié)點(diǎn)創(chuàng)建方法的集合,里面包含了各個節(jié)點(diǎn)的創(chuàng)建方法。
最后利用path.replaceWith替換節(jié)點(diǎn)
BinaryExpression: { exit: function(path){ path.replaceWith( preOperationAST({ FUN_NAME: t.identifier(replaceOperator), ARGS: [path.node.left, path.node.right] }) ); } },判斷需要引入的方法
在節(jié)點(diǎn)遍歷完畢之后,我需要知道該文件一共需要引入幾個方法,因此需要定義一個數(shù)組來緩存當(dāng)前文件使用到的方法,在節(jié)點(diǎn)遍歷命中的時候向里面添加元素。
var needRequireCache = []; ... return { visitor: { BinaryExpression: { exit(path){ needRequireCache.push(path.node.operator) //根據(jù)path.node.operator判斷向needRequireCache添加元素 ... } } } } ...
AST遍歷完畢最后退出的節(jié)點(diǎn)肯定是Program的exit方法,因此可以在這個方法里面對polyfill進(jìn)行引用。
同樣也可以利用babel.template構(gòu)建節(jié)點(diǎn)插入引用:
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的作用就是在當(dāng)前語法樹插入節(jié)點(diǎn),所以最后的效果就是這個樣子:
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浮點(diǎn)數(shù)計算精度問題(如0.1+0.2!==0.3)
AST explorer
@babel/types
babel-plugin-handlebook
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/104168.html
摘要:又如,對于,結(jié)果其實并不是,但是最接近真實結(jié)果的數(shù),比其它任何浮點(diǎn)數(shù)都更接近。許多語言也就直接顯示結(jié)果為了,而不展示一個浮點(diǎn)數(shù)的真實結(jié)果了。小結(jié)本文主要介紹了浮點(diǎn)數(shù)計算問題,簡單回答了為什么以及怎么辦兩個問題為什么不等于。 原文地址:為什么0.1+0.2不等于0.3 先看兩個簡單但詭異的代碼: 0.1 + 0.2 > 0.3 // true 0.1 * 0.1 = 0.01000000...
摘要:方法使用定點(diǎn)表示法來格式化一個數(shù),會對結(jié)果進(jìn)行四舍五入。該數(shù)值在必要時進(jìn)行四舍五入,另外在必要時會用來填充小數(shù)部分,以便小數(shù)部分有指定的位數(shù)。如果數(shù)值大于,該方法會簡單調(diào)用并返回一個指數(shù)記數(shù)法格式的字符串。在環(huán)境中,只能是之間,測試版本為。 showImg(https://segmentfault.com/img/remote/1460000011913134?w=768&h=521)...
摘要:由于浮點(diǎn)數(shù)不是精確的值,所以涉及小數(shù)的比較和運(yùn)算要特別小心。根據(jù)標(biāo)準(zhǔn),位浮點(diǎn)數(shù)的指數(shù)部分的長度是個二進(jìn)制位,意味著指數(shù)部分的最大值是的次方減。也就是說,位浮點(diǎn)數(shù)的指數(shù)部分的值最大為。 一 前言 這篇文章主要解決以下三個問題: 問題1:浮點(diǎn)數(shù)計算精確度的問題 0.1 + 0.2; //0.30000000000000004 0.1 + 0.2 === 0.3; // ...
摘要:標(biāo)準(zhǔn)二進(jìn)制浮點(diǎn)數(shù)算法就是一個對實數(shù)進(jìn)行計算機(jī)編碼的標(biāo)準(zhǔn)。然后把取出的整數(shù)部分按順序排列起來,先取的整數(shù)作為二進(jìn)制小數(shù)的高位有效位,后取的整數(shù)作為低位有效位。 浮點(diǎn)運(yùn)算JavaScript 本文主要討論JavaScript的浮點(diǎn)運(yùn)算,主要包括 JavaScript number基本類型 二進(jìn)制表示十進(jìn)制 浮點(diǎn)數(shù)的精度 number 數(shù)字類型 在JavaScript中,數(shù)字只有numb...
閱讀 2111·2023-04-25 19:15
閱讀 2294·2021-11-23 09:51
閱讀 1297·2021-11-17 09:33
閱讀 2210·2021-08-26 14:15
閱讀 2514·2019-08-30 15:54
閱讀 1608·2019-08-30 15:54
閱讀 2195·2019-08-30 12:50
閱讀 1163·2019-08-29 17:08