preface

Have you ever wondered what 0.1+0.2 is? What about 0.1+0.7, 0.8-0.2? Similar to this problem, there are many solutions, no matter the introduction of external libraries or the definition of their own calculation function is the ultimate purpose of using functions to replace the calculation. For example, a formula for a percentage increase or decrease: (current price – original price)/ original price *100 + ‘%’ Actual code: Mul(Div(Sub(current price, original price), original price), 100) + ‘%’ Originally a very easy to understand calculation formula of four operations in the code becomes not very friendly to read, writing is not very consistent with the habit of thinking. Therefore, Babel and AST syntax tree are used to rewrite + – * / and other symbols in the process of code construction, and the code is directly written in the form of 0.1+0.2 during development, which is compiled into Add(0.1, 0.2) during construction, so as to solve the problem of computational inaccuracy without the awareness of developers and improve the readability of the code.

To prepare

Let’s start by understanding why 0.1+0.2 does not equal 0.3:

Portal: How to get around JavaScript floating-point accuracy issues (e.g. 0.1+0.2! = = 0.3)

The above article is very detailed, I use popular language to summarize: the numbers we use in daily life are base 10, and base 10 conforms to the logic of brain thinking, while the computer uses base 2 counting method. But not all of the numbers in two different cardinal numbers correspond to a finite number of digits in the other one.

0.1 in decimal is 10^-1, which is 0.1, and 0.1 in binary is 2^-1, which is 0.5.

For example, 1/3 is represented as 0.33333(infinite loop) in decimal and 0.1 in base 3, because 3^-1 is 0.3333333… The binary representation of 0.1 in operatively decimal is 0.000110011…… 0011… (0011 Infinite loop)

Understand the Babel

Babel works by doing static analysis using the AST syntax tree. For example, let a = 100 is translated into a syntax tree that looks like this before Babel processes it:

{
    "type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
          "type": "Identifier"."name": "a"
        },
        "init": {
          "type": "NumericLiteral"."extra": {
            "rawValue": 100."raw": "100"
          },
          "value": 100}}]."kind": "let"
  },
Copy the code

Babel translates a text code into a JSON object so that it can traverse and recursively find each of the different properties, so that Babel knows exactly what each line of code is doing. The purpose of the Babel plug-in is to recursively traverse the syntax tree of the code file, find the location that needs to be changed, replace it with the corresponding value, and then translate the code back for the browser to execute. For example, if we change let to var in the code above, we just need to execute ast. kind = “var”,AST is the object iterated.

AST Portal AST node type Document portal

start

Understand the development process of Babel plug-in babel-plugin-Handlebook

Problems we need to solve:

  • Calculate the writing of polyfill
  • Locate the code block that needs to be changed
  • Determine which polyfills need to be imported for the current file (imported on demand)

The writing of the polyfill

Polyfill mainly needs to provide four functions to replace the operations of addition, subtraction, multiplication and division respectively. Meanwhile, it also needs to determine the data type of the calculation parameter. If the data type is not number, the original calculation method is adopted:

accAdd

function accAdd(arg1, arg2) {
    if(typeofarg1 ! = ='number' || typeofarg2 ! = ='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;
}
Copy the code

accSub

function accSub(arg1, arg2) {
    if(typeofarg1 ! = ='number' || typeofarg2 ! = ='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));
}
Copy the code

accMul

function accMul(arg1, arg2) {
    if(typeofarg1 ! = ='number' || typeofarg2 ! = ='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);
}
Copy the code

accDiv

function accDiv(arg1, arg2) {
    if(typeofarg1 ! = ='number' || typeofarg2 ! = ='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);
}
Copy the code

How it works: Converts a floating point number to an integer for calculation.

Location code block

Understand the development process of Babel plug-in babel-plugin-Handlebook

Babel plug-ins are introduced in two ways:

  • Introduce the plug-in through the.babelrc file
  • Import plugins through the options property of babel-loader

The babel-plugin accepts a function that takes a Babel argument containing properties such as the common constructor of bable. The result of the function must be an object like this:

{
    visitor: {
        / /...}}Copy the code

Visitor is a traversal finder for an AST. Babel will attempt a depth-first traversal of the AST syntax tree. The key of the property in the visitor is the AST node name that needs to be operated on, such as VariableDeclaration, BinaryExpression, etc. Value The value can be a function or an object, as shown in the following example:

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

The function parameter path contains the current node object, as well as properties such as common node traversal methods. Babel traverses the AST syntax tree depth-first. When traversing a cotyledon node (the end of the branch), the traverser will go back to the ancestor node to continue the traversal, so each node will be traversed twice. Visitor property value is a function, the function is executed on the first entry to the node. Visitor property value is an object and receives two enter and exit properties (optional), one for the entry and one for the traceback phases.

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.

The code block that needs to be replaced in the code is of type A + B, so we know that the node of this type is BinaryExpression, and we need to replace the node of this type with accAdd(a, b). The AST syntax tree is as follows:

{
        "type": "ExpressionStatement",},"expression": {
          "type": "CallExpression",},"callee": {
            "type": "Identifier"."name": "accAdd"
          },
          "arguments": [{"type": "Identifier"."name": "a"
            },
            {
              "type": "Identifier"."name": "b"}}}]Copy the code

So just build the syntax tree and replace the nodes. Babel provides an easy way to build any node you want using babel.template. This function takes a code string argument with an uppercase character as a code placeholder and returns a replacement function that takes an object as an argument to replace the code placeholder.

var preOperationAST = babel.template('FUN_NAME(ARGS)');
var AST = preOperationAST({
    FUN_NAME: babel.types.identifier(replaceOperator), / / the method name
    ARGS: [path.node.left, path.node.right] / / parameters
})
Copy the code

The AST is the syntax tree that ultimately needs to be replaced, and babel.types is a collection of node creation methods that contain the creation methods of each node.

Finally, replace the node with path.replacewith

BinaryExpression: {
    exit: function(path){
        path.replaceWith(
            preOperationAST({
                FUN_NAME: t.identifier(replaceOperator),
                ARGS: [path.node.left, path.node.right] }) ); }},Copy the code

Determine the method to be introduced

After the node traversal is complete, I need to know how many methods the file needs to import, so I need to define an array to cache the methods used in the current file and add elements to it when the node traversal hits.

varneedRequireCache = []; . return {visitor: {
            BinaryExpression: {
                exit(path){
                    needRequireCache.push(path.node.operator)
                    // Add elements to needRequireCache based on path.node.operator judgment. }}}}...Copy the code

The last node to exit after the AST traverses must be Program’s exit method, so you can reference polyfill in this method. It is also possible to build a node insert reference using 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);
        });
        returnt.ObjectPattern(properties); }... Program: {exit: function(path){
            path.unshiftContainer('body', requireAST({
                PROPERTIES: preObjectExpressionAST(needRequireCache),
                SOURCE: t.stringLiteral("babel-plugin-arithmetic/src/calc.js")})); needRequireCache = []; }},...Copy the code

Path. unshiftContainer is used to insert nodes into the current syntax tree, so the result looks like this:

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
Copy the code
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
Copy the code

Complete code example

Github project address

Usage:

npm install babel-plugin-arithmetic --save-dev
Copy the code

Add the plugin /.babelrc

{
	"plugins": ["arithmetic"]}Copy the code

or

/webpack.config.js

. {test: /\.js$/.loader: 'babel-loader'.option: {
		plugins: [
			require('babel-plugin-arithmetic'},},...Copy the code

Welcome to send me star⭐⭐⭐⭐⭐. If you have any suggestions, please issue me.

Reference documentation

How to avoid JavaScript floating-point calculation accuracy issues (e.g. 0.1+0.2! ==0.3) AST Explorer @babel/types babel-plugin-handlebook