摘要:因此利用以及語法樹在代碼構建過程中重寫等符號,開發時直接以這樣的形式編寫代碼,在構建過程中編譯成,從而在開發人員無感知的情況下解決計算失精的問題,提升代碼的可讀性。
前言
你了解過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的工作原理實際上就是利用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節點名如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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104168.html
摘要:又如,對于,結果其實并不是,但是最接近真實結果的數,比其它任何浮點數都更接近。許多語言也就直接顯示結果為了,而不展示一個浮點數的真實結果了。小結本文主要介紹了浮點數計算問題,簡單回答了為什么以及怎么辦兩個問題為什么不等于。 原文地址:為什么0.1+0.2不等于0.3 先看兩個簡單但詭異的代碼: 0.1 + 0.2 > 0.3 // true 0.1 * 0.1 = 0.01000000...
摘要:方法使用定點表示法來格式化一個數,會對結果進行四舍五入。該數值在必要時進行四舍五入,另外在必要時會用來填充小數部分,以便小數部分有指定的位數。如果數值大于,該方法會簡單調用并返回一個指數記數法格式的字符串。在環境中,只能是之間,測試版本為。 showImg(https://segmentfault.com/img/remote/1460000011913134?w=768&h=521)...
摘要:由于浮點數不是精確的值,所以涉及小數的比較和運算要特別小心。根據標準,位浮點數的指數部分的長度是個二進制位,意味著指數部分的最大值是的次方減。也就是說,位浮點數的指數部分的值最大為。 一 前言 這篇文章主要解決以下三個問題: 問題1:浮點數計算精確度的問題 0.1 + 0.2; //0.30000000000000004 0.1 + 0.2 === 0.3; // ...
摘要:標準二進制浮點數算法就是一個對實數進行計算機編碼的標準。然后把取出的整數部分按順序排列起來,先取的整數作為二進制小數的高位有效位,后取的整數作為低位有效位。 浮點運算JavaScript 本文主要討論JavaScript的浮點運算,主要包括 JavaScript number基本類型 二進制表示十進制 浮點數的精度 number 數字類型 在JavaScript中,數字只有numb...
閱讀 2077·2023-04-25 19:15
閱讀 2245·2021-11-23 09:51
閱讀 1264·2021-11-17 09:33
閱讀 2165·2021-08-26 14:15
閱讀 2476·2019-08-30 15:54
閱讀 1582·2019-08-30 15:54
閱讀 2167·2019-08-30 12:50
閱讀 1132·2019-08-29 17:08