When I write requirements, I find that my mates use the Optional chaining feature a lot in my project. Whenever I have an object property, I use it.

const x = null
consty = x? .aCopy the code

For those of you who haven’t heard of this concept, let me just say that js optional chain is basically what it means. . The symbols that make up the optional chain. Normally, if you remove the optional chain symbol, then the code above will report an error but if you use the optional chain, the code will not report an error, and y will be undefined. There’s a lot more information about the optional chain on the Internet, so I won’t go into it, A well-written intensive reading of Optional Chaining is recommended.

The javascript optional chain feature is already in Stage 4, but it is not supported by default even in the latest Version of Google Chrome, so it must be translated by Babel. In general, the translated code is larger than the original code. So, as a veteran of mobile development experience, I subconsciously thought that with so many alternative chains to translate, how much will the volume of the final project increase?

So, I immediately went to Babel and compiled it online. Here’s what it looks like:

"use strict";

var x = null;
var y = x === null || x === void 0 ? void 0 : x.a;
Copy the code

A roughly three-fold increase in the number of characters compared to the source code does have an impact on the size of the final project, but given that the necessary null-finding logic is needed to keep the project code working properly, it would be nice to avoid abuse

It occurred to me that after Babel had done this translation, what would I do if I was left to do it myself?

After comparing the code before and after Babel translation, it is found that there is a pattern. .sign, and turn it into a ternary expression

If the object being valued is all null or all void 0, then the ternary expression returns void 0(undefined), otherwise it returns the value taken from the object being valued

Although it looks like? The notation only determines if the object being valued is null or void 0, but in practice, because of implicit conversions, this is sufficient for any type of value. ( ̄▽ ̄)”

Now that the pattern is known, the question that follows is clear: how do you accomplish this transformation?

How to identify the source code? .symbol and convert it to the correct ternary expression?

It is theoretically possible to simply replace the source character, but there is clearly a better way to do this: first convert the source character to the AST, then manipulate the AST, and then convert the processed AST to the source character, which is the base operation

So, how do YOU convert a source character to an AST?

In theory, of course, you could write your own conversion plugin, but you don’t have to, because Babel already provides it for us

@babel/ Parser is used to convert source code characters to AST, and @babel/ Generator is used to convert AST to source code characters

The first step is to convert the source character to an AST

@ Babel/parser!

const { parse } = require('@babel/parser')

const code = ` const x = null const y = x? .a `
const ast = parse(source)
Copy the code

The resulting AST is the converted AST object represented by code. You can see that the AST is a well-structured object by debugging the breakpoint:

By traversing the AST object, we get the desired nodes on the AST structure. You can write code to traverse it based on the AST structure, but bable already provides a plugin for traversing this: @babel/traverse

Since we only care about the conversion of js optional chains, and the node type of JS optional chains in Babel is OptionalMemberExpression, we have the following code to iterate over the AST:

const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse').default

const code = ` const x = null const y = x? .a `
const ast = parse(source)
traverse(ast, {
  OptionalMemberExpression(path) {
    console.log(path)
  }
})
Copy the code

Path stores information about the ast structure of the traversed node, i.e. X? . A converts some structural information of the AST

So the next thing you need to do is change this structure to the structure of a ternary expression

But what is the structure of a ternary expression?

You can write the converted ternary expression code by hand, then use @babel/ Parser to convert it to an AST, observe the difference between it and the alternative chain AST, and then modify the alternative chain AST to the ternary expression

There is already a website for compiling and viewing ast online, ASTExplorer, and it is highly recommended that new ast students take advantage of this site

The site allows you to compile the AST in real time and visualize the structure of the AST, which is quite convenient, although the ASTExplorer ast is missing something compared to the @Babel/Parser AST object. But the body of content we need is still there, and it doesn’t affect usage

As you can see from this website, for the following ternary expression

x === null || x === void 0 ? void 0 : x.a
Copy the code

Its AST structure is:

? The type of. Is OptionalMemberExpression, and the conversion to a terplet Expression corresponds to three expressions: LogicalExpression, UnaryExpression, and MemberExpression respectively correspond to the three expressions of ternary expressions

LogicalExpression and its child Expression corresponding three combined Expression of the first Expression: x = = = null | | x = = = void 0; UnaryExpression and its child expressions add up to the second Expression of the ternary Expression: void 0; MemberExpression Corresponds to the third expression of the three-element expression: x.a

So there is the problem of how to construct Expression, and Babel provides @babel/types to solve this problem

We know that the type of the terplet expression is ConditionalExpression, so the top ast is a ConditionalExpression node:

const transCondition = node= > {
  return t.conditionalExpression(
  )
}
Copy the code

Conditionalexpression based on the method documentation provided by @babel/types, we know the three parameters received by this method

All three arguments are of type Expression, which corresponds to the three expressions of the ternary Expression above, so the code can be written as:

const transCondition = node= > {
  return t.conditionalExpression(
    t.logicalExpression(),
    t.unaryExpression(),
    t.memberExpression()
  )
}
Copy the code

We continue to query the documents of t.logicalExpression(), t.naryexpression (), and t.emberexpression () to get the full method:

const transCondition = node= > {
  return t.conditionalExpression(
    t.logicalExpression(
      '| |',
      t.binaryExpression('= = =', node.object, t.nullLiteral()),
      t.binaryExpression('= = =', node.object, t.unaryExpression('void', t.numericLiteral(0)))
    ),
    t.unaryExpression('void', t.numericLiteral(0)),
    t.memberExpression(node.object, node.property, node.computed, node.optional)
  )
}
Copy the code

The AST structure processed by the transCondition method is the structure of a ternary expression, and the ast of the alternative chain needs to be replaced with the AST of this ternary expression

Babel has also provided a number of methods for manipulating ast, including the replacement method replaceWith

After the AST is replaced, the @babel/ Generator is used to convert the AST structure into the source character, which is the final translation result we need

The complete code is as follows:

const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')

const code = ` const x = null const y = x? .a `

const transCondition = node= > {
  return t.conditionalExpression(
    t.logicalExpression(
      '| |',
      t.binaryExpression('= = =', node.object, t.nullLiteral()),
      t.binaryExpression('= = =', node.object, t.unaryExpression('void', t.numericLiteral(0)))
    ),
    t.unaryExpression('void', t.numericLiteral(0)),
    t.memberExpression(node.object, node.property, node.computed, node.optional)
  )
}

const ast = parse(source)
traverse(ast, {
  OptionalMemberExpression(path) {
    path.replaceWith(transCondition(path.node, path))
  }
})
console.log(generator(ast).code)
Copy the code

The input generator(ast). Code is as follows:

const x = null;
const y = x === null || x === void 0 ? void 0 : x.a;
Copy the code

In addition to? .Other than that, irrelevant things don’t matter, so there is no transformationconst

Finished?

That’s what I thought at first, and then when I put const y = x? Const y = x .a? .b? Const y = x, const y = x .a? .b? The code after the.c conversion is:

const x = null;
const y = ((x === null || x === void 0 ? void 0 : x.a) === null || (x === null || x === void 0 ? void 0 : x.a) === void 0 ? void 0 : (x === null || x === void 0 ? void 0 : x.a).b) === null || ((x === null || x === void 0 ? void 0 : x.a) === null || (x === null || x === void 0 ? void 0 : x.a) === void 0 ? void 0 : (x === null || x === void 0 ? void 0 : x.a).b) === void 0 ? void 0 : ((x === null || x === void 0 ? void 0 : x.a) === null || (x === null || x === void 0 ? void 0 : x.a) === void 0 ? void 0 : (x === null || x === void 0 ? void 0 : x.a).b).c;
Copy the code

Why is it so long?

If I go down to the properties, isn’t it going to be even longer? Is it more than tripling the code?

I quickly went to Babel to compile it online and found that it was as simple as I thought:

"use strict";

var _x$a, _x$a$b;

var x = null;
var y = x === null || x === void 0 ? void 0 : (_x$a = x.a) === null || _x$a === void 0 ? void 0 : (_x$a$b = _x$a.b) === null || _x$a$b === void 0 ? void 0 : _x$a$b.c;
Copy the code

When the value is more than one level deep, Babel adds an extra variable to store the result of the value expression at the upper level, and then continues to evaluate the value at that variable instead of the expression at the upper level, significantly shortening the amount of code and avoiding a lot of double counting

We can continue to modify the code along these lines

So what you need to think about is you need to think about a continuous value operation as a whole, as opposed to the idea of just cutting it off, which is, for x, right? .a? .b? .c, the alternative chain value expression, should not be broken up into x? A, (x? .a)? B, (x? .a? .b)? .c, because this would lose context and make it impossible to define the additional variables accurately

Then we need to recurse through all the optional chain structures of the children of an optional chain structure when we enter the top-level structure of an optional chain

const transCondition = (node, path) = > {
  if(node.type ! = ='OptionalMemberExpression') {
    return node
  }
  const expression1 = transCondition(node.object, path)
  const alternate = t.memberExpression(expression1, node.property, node.computed, node.optional)
  const res = t.conditionalExpression(
    t.logicalExpression(
      '| |',
      t.binaryExpression('= = =', expression1, t.nullLiteral()),
      t.binaryExpression('= = =', expression1, t.unaryExpression('void', t.numericLiteral(0)))
    ),
    t.unaryExpression('void', t.numericLiteral(0)),
    alternate
  )
  return res
}
Copy the code

Since we are recursively traversing the child optional chains under the ast structure of the top-level optional chain, we need to implement the logic of this recursive traversing process in the transCondition method. As long as the type of the child is no longer OptionalMemberExpression, the recursion will be skipped

Just modify the transCondition method, leaving the rest of the logic unchanged, run the code, and find that the compiled result is the same as before, and is also a long list of redundant nested ternary expressions

Let’s set up the additional auxiliary variables

for

var _x$a, _x$a$b;
Copy the code

How does Babel describe this assignment code in terms of an AST?

A look at the documentation shows that @babel/types provides variableDeclarations for defining variables

For example, for the above code, the code defined is:

t.variableDeclaration('var', [t.variableDeclarator(t.identifier('_x$a')), t.variableDeclarator(t.identifier('_x$a$b')))Copy the code

Once the code is defined, you need to insert the defined code into the source code

@babel/traverse provides an insertBefore method that inserts an additional Expression before the current AST path

According to the practice of Babel, additional variables with the path variable name is currently a js statements related to the variable name is made up of the current chain of optional attribute values of object properties to get together, the nothing special meaning, just for the sake of convenient code readable and avoid conflict variables of a rule, we can according to the rules here

Now that the variable is defined, how do you assign a value to the variable?

In this case, the value of the variable should actually be the value of the object that was taken before the current optional chain property, for example, for x? .a? _x$a = _x$a

Note that Babel is parsed in the following order:

x? .a? .b? .cCopy the code

Babel’s parsing structures are nested from back to front, such as x? .(a? .b? C) that (x? .a? .b)? C. What about x? .a? . B as a whole, take the value of c for this whole, and then change x? Take a as a whole and take the value of B

In this article, only OptionalMemberExpression processing is considered. For OptionalCallExpression, which is similar to a? B () or a? (), and some other special scenarios are not considered, because the principle is similar, interested can try to implement

The final code looks like this:

// optional-chaining-loader.js
const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')

// In practice, this line of code should be the actual code read from the file. This is just a demonstration
const code = ` a? .b? .[c]? .[++d]? .[e++].r `
const optinal2member = (node, extraList = []) = > {
  if(! node.object) {return node
  }
  let list = []
  while(node && ! node.optional) { list.push(node) node = node.object }if (node) {
    list.push(node)
    node = node.object
  }
  return joinMemberExpression(extraList.concat(list, node ? joinVariable(node) : []))
}
const separator = '$'
const singlePrefix = '_'
const updateExpressionType = 'UpdateExpression'
const joinVariable = node= > {
  let variabelStr = ' '
  let localNode = node
  while (localNode.object) {
    const name = localNode.property.type === updateExpressionType ? localNode.property.argument.name : localNode.property.name
    variabelStr = variabelStr ? (name + separator + variabelStr) : name
    localNode = localNode.object
  }
  variabelStr = singlePrefix + localNode.name + (variabelStr || (separator + variabelStr))
  return variabelStr
}
const joinMemberExpression = list= > {
  const top = list.pop()
  let parentObject = typeof top === 'string' ? t.identifier(top) : top
  while (list.length) {
    const object = list.pop()
    parentObject = t.memberExpression(parentObject, object.property, object.computed)
  }
  return parentObject
}
const transCondition = ({ node, path, expression = null, variabelList = [], memberExpression = [] }) = > {
  if(! node) {return expression
  }
  if(! node.optional) {return transCondition({
      node: node.object,
      path,
      expression,
      variabelList,
      memberExpression: memberExpression.concat(node)
    })
  }
  const extraVariable = t.identifier(joinVariable(node.object))
  variabelList.unshift(t.variableDeclarator(extraVariable))
  const res = t.conditionalExpression(
    t.logicalExpression(
      '| |',
      t.binaryExpression('= = =', t.assignmentExpression('=', extraVariable, optinal2member(node.object)), t.nullLiteral()),
      t.binaryExpression('= = =', extraVariable, t.unaryExpression('void', t.numericLiteral(0)))
    ),
    t.unaryExpression('void', t.numericLiteral(0)),
    expression || optinal2member(node, memberExpression)
  )
  
  if (node.object.object) {
    return transCondition({ node: node.object, path, expression: res, variabelList })
  }
  path.insertBefore(t.variableDeclaration('var', variabelList))
  return res
}

function transOptinal(source) {
  const ast = parse(source, {
    plugins: [
      'optionalChaining',
    ]
  })
  traverse(ast, {
    OptionalMemberExpression(path) {
      path.replaceWith(transCondition({ node: path.node, path }))
    }
  })
  return generator(ast).code
}

const parseCode = transOptinal(code)
console.log(parseCode)
Copy the code

For a? .b? .[c]? .[++d]? For the.[e++].r line, running the above code yields the following output:

var _a$, _ab, _ab$c, _ab$c$d;

(_a$ = a) === null || _a$ === void 0 ? void 0 : (_ab = _a$.b) === null || _ab === void 0 ? void 0 : (_ab$c = _ab[c]) === null || _ab$c === void 0 ? void 0 : (_ab$c$d = _ab$c[++d]) === null || _ab$c$d === void 0 ? void 0 : _ab$c$d[e++].r;
Copy the code

The plug-in code is written, how to use it?

The simplest thing is to think of this code as a WebPack loader and load it before all the other JS processes the loader

// optional-chaining-loader.js
function transOptinal(source) {
  const ast = parse(source, {
    plugins: [
      'optionalChaining',
    ]
  })
  traverse(ast, {
    OptionalMemberExpression(path) {
      path.replaceWith(transCondition({ node: path.node, path }))
    }
  })
  return generator(ast).code
}
module.exports = transOptinal
Copy the code
// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [{test: /\.js$/.use: [{loader: path.resolve(__dirname, 'optional-chaining-loader.js')}]}]}Copy the code

Once you know how to use Babel to translate code, you can do whatever you want with your code. For example, clean up all console. logs in your code when packaging, or even create your own syntax and write your own plugins to translate it (although, in general, nobody does this. ( ̄▽ ̄) Ming)

It is not recommended to write your own translation plugin. For the Optional chaining described in this article, The Babel website already provides translation plugins @babel/plugin-proposal-optional-chaining, so you don’t need to write things again

If you have a wheel, you must make it yourself. – Lu Xun