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