手写一个本身的babel

babel 背景介绍

babel 是一个前端的代码转换工具,目的是为了让开发者使用ECMA最新的标准甚至一些在stage阶段的提案功能,而不用过多考虑运行环境的兼容性。javascript

近些年得益于js社区的活跃,ES版本从15年开始每一年都会发布一个新的版本,截止目前最新的版本是ECMAScript 2019,已是第10个版本了,可是目前最新的chrome还没有彻底支持ES6的全部功能,好比模块方面的功能,而babel的出现可让前端开发工程师们用最新的语法去coding,由它来保证在浏览器中代码的转换,换句话说它可让咱们用最新的标准写代码,而不用考虑浏览器的兼容~前端

若是想了解更多关于babel的内容,请移步这里,还有中文版文档java

上回分析的基础配置篇在这里node

babel 原理

babel 能够转化ES6的语法到ES5,以下git

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});
复制代码

全部被babel处理的js内容都会经历3个大的流程:github

  1. 词法分析,解析为tokens数组
  2. 语法分析
    • 转化tokens到对象的带有语法信息的ast[抽象语法树]
    • 根据ast转化为对应的须要的新的ast
  3. 生成,将新的ast生成为可执行的代码,并输出

实现一个本身的的babel

本文实现的babel fork 自 the-super-tiny-compiler,内容基本同样,重在但愿能够对没有时间阅读代码的同窗起个快速理解的做用,后续若是有时间,会写一个和js相关的转换代码chrome

步骤

  1. input -> tokens [tokenizer]
  2. tokens -> ast [parser]
  3. ast -> newAst [transformer]
  4. newAst -> output [codeGenerator]

例子

const code = '(add 2 (add 4 2))'; // 初始的code
    const expectCode = 'add(2, add(4,2))'; // 指望转换结果
复制代码

预期过程

  1. tokenizer result [词法分析结果]
const tokens = [
    { type: 'paren', value: '(' },
    { type: 'name', value: 'add' },
    { type: 'number', value: '2' },
    { type: 'paren', value: '(' },
    { type: 'name', value: 'add' },
    { type: 'number', value: '4' },
    { type: 'number', value: '2' },
    { type: 'paren', value: ')' },
    { type: 'paren', value: ')' }
    ]
复制代码
  1. parser result [语法分析结果,构建ast]
const ast = {
    "type": "Program",
    "body": [
        {
            "type": "CallExpression",
            "name": "add",
            "params": [
                { "type": "NumericLiteral", "value": "2" },
                {
                    "type": "CallExpression",
                    "name": "add",
                    "params": [{ "type": "NumericLiteral", "value": "4" }, { "type": "NumericLiteral", "value": "2" }]
                }
            ]
        }
    ]
}
复制代码
  1. transformer result [转换原始ast到新的目标ast格式,语法转化结果]
const newAst = {
    "type": "Program",
    "body": [
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "CallExpression",
                "callee": { "type": "Identifier", "name": "add" },
                "arguments": [
                    { "type": "NumberLiteral", "value": "2" },
                    {
                        "type": "CallExpression",
                        "callee": { "type": "Identifier", "name": "add" },
                        "arguments": [
                            { "type": "NumberLiteral", "value": "4" },
                            { "type": "NumberLiteral", "value": "2" }
                        ]
                    }
                ]
            }
        }
    ]
}
复制代码
  1. output result [生成预期code]
const output = 'add(2, add(4, 2));'
复制代码

实现

词法分析阶段[tokenizer]

解析源代码,生成tokensexpress

const tokenizer = input => {
    let current = 0; // 记录起始位置
    const tokens = []; // 存放tokens
    while (current < input.length) {
        let char = input[current];
        if (char === '(' || char === ')') {
            // 遇到括号,转为type是paren的token
            tokens.push({
                type: 'paren',
                value: char
            });
            current++; // 递增一位
            continue;
        }
        if (/\s/.test(char)) {
            // 空格无论,直接跳过 【空格在这里对咱们没有实际的意义,可是须要current下标跳过】
            current++;
            continue;
        }
        if (/[0-9]/.test(char)) {
            // 若是是数字,用while来遍历到下一位不是数字的位置
            let value = ''; // 用来放这个数字
            while (/[0-9]/.test(char)) {
                value += char;
                char = input[++current];
            }
            tokens.push({
                type: 'number',
                value
            });
            continue;
        }
        if (char === "'") {
            // 这里只处理 '' 包围的字符串,和处理数字的思路是同样的
            let value = ''; // 用来存放字符串
            char = input[++current];
            while (char !== "'") {
                value += char;
                char = input[++current];
            }
            char = input[++current];
            tokens.push({
                type: 'string',
                value
            });
            continue;
        }

        const LETTERS = /[a-z]/i; // 处理name
        if (LETTERS.test(char)) {
            // 处理连续的字母,变量的名称,也所以把type定义为name
            let value = '';
            while (LETTERS.test(char)) {
                value += char;
                char = input[++current];
            }
            tokens.push({
                type: 'name',
                value
            });
            continue;
        }
        // 若是都匹配不到,咱们就认为没法解析~
        throw new TypeError('I dont know what this character is: ' + char);
    }

    // 返回处理好的tokens
    return tokens;
};
复制代码

语法分析阶段[parser]

构建原始ast

解析第一步生成的tokens为对应的ast抽象语法树小程序

const parser = tokens => {
    let current = 0;
    // walk 函数 用来 parse token到对应的 【语法】 类型
    function walk() {
        let token = tokens[current];
        if (token.type === 'number') {
            current++;
            return {
                type: 'NumericLiteral',
                value: token.value
            };
        }
        if (token.type === 'string') {
            current++;
            return {
                type: 'StringLiteral',
                value: token.value
            };
        }
        // 遇到括号的时候,咱们认为开始进行方法调用的转换了
        if (token.type === 'paren' && token.value === '(') {
            token = tokens[++current]; // 跳过 '(' 自己这个token,它只是个标记,在这里没有实际做用
            let node = {
                // 定义一个node节点,类型就是 CallExpression
                type: 'CallExpression',
                name: token.value, // 咱们认为括号接下来的token 放的就是这个方法的 name
                params: [] // 默认方法的参数是一个空数组
            };
            token = tokens[++current]; // 跳过name这个token

            // 遍历name以后的token,必定要遇到方法调用的结束符号 '(' 为止
            while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')) {
                node.params.push(walk()); // 在'('和')' 中间全部的token,咱们都认为是当前方法调用时候的参数,所以递归解析就能够了~
                token = tokens[current]; // 这里每次从新获取一下当前的token,为了抛出错误的时候用到
            }
            current++; // 和前面同样,处理完当前current 加一位到下一个
            return node; // 返回当前构建好的node
        }

        throw new TypeError(token.type);
    }

    // 定义原始的ast结构
    const ast = {
        type: 'Program', // 入口开始咱们称为 Program
        body: []
    };

    // 遍历tokens
    while (current < tokens.length) {
        ast.body.push(walk()); // 递归tokens,填充ast
    }
    return ast; // 返回填充结束的ast~
};
复制代码
生成新的ast

根据原始ast转化为新的ast数组

// 拿到ast后,就能够进行下一步的“转化”流程了,可是在这以前咱们须要先设计一个traverser方法,这个方法会对生成好的ast进行深度优先的递归遍历,同时还能够提供一个visitor参数用来“访问”每个语法类型的遍历过程,有了visitor就能够在“访问”到当前type类型的node的时候,对当前的node进行一些额外的处理了,这也是babel插件的工做原理,其实仍是在对ast的转化过程当中进行的处理~

const traverser = (ast, visitor) => {
    // traverseArray 会遍历array,对每个item进行traverseNode转化
    function traverseArray(array, parent) {
        array.forEach(el => {
            traverseNode(el, parent);
        });
    }

    // 转化过程,接受2个参数 当前转化的node 和 父节点parent
    function traverseNode(node, parent) {
        // 先看下提供的visitor中有没有当前node.type类型的method
        // 从babel的官网中能够看到method能够直接经过 [type](){}这种function的类型进行定义,也能够经过一个config,包含enter 和 exit的形式来定义~
        // 树的遍历过程当中,咱们对每个节点会有2次访问的机会,第一次是进入,第二次是退出,对应这里的 enter 和 exit
        const method = visitor[node.type];
        if (typeof method === 'function') {
            // 若是method是方法,直接调用,并送入node和parent
            method(node, parent);
        } else if (method && method.enter) {
            // 若是是enter,则调用enter~
            method.enter(node, parent);
        }
        switch (
            node.type // 对每一个type进行转化
        ) {
            case 'Program':
                traverseArray(node.body, node); // 若是是Program,咱们知道他下面的子节点都在body中,所以咱们经过traverseArray把子节点的数组和当前节点送进去
                break;
            case 'CallExpression':
                traverseArray(node.params, node); // 若是是CallExpression,咱们知道CallExpression的子节点声名在params这个数组中,一样的对子节点进行递归转化
                break;
            case 'NumericLiteral': // 当遇到 NumericLiteral 和 StringLiteral 的时候,它们都没有子节点了,所以直接break就能够了
            case 'StringLiteral':
                break;
            default:
                throw new TypeError(node.type); // 一样,找不到就丢出去一个错误
        }
        if (typeof method === 'function') {
            // 每一个节点在遍历结束后,咱们须要调用visitor的exit方法,相似于enter的调用
            method(node, parent);
        } else if (method && method.exit) {
            method.exit(node, parent);
        }
    }

    traverseNode(ast, null); // 递归的启动,咱们的第一个节点就是ast,它是没有父节点的,所以第二个参数给null~
};

// 接下来是对ast进行默认的转化的过程,参数就是ast
const transformer = ast => {
    const newAst = {
        // 转化后的结果会保存到一个新的newAst中去
        type: 'Program',
        body: []
    };
    // 这里是一个hack,真正的实现比这个要复杂,咱们为了实例简单,经过_context来传递新老ast之间的上下文的对应关系
    // 由于虽然咱们遍历的是旧的ast,可是须要转化的结果保存到新的ast中,所以存在一个节点之间的对应关系
    // 咱们给旧的ast加一个_context属性,而且让它指向实际的新的ast的body
    // 这里指向body是由于最外层的type类型是Program 而且他们子节点都是body,所以我只要后面修改parent的_context,就会更新到新的ast的body中
    ast._context = newAst.body;
    traverser(ast, {
        // 利用 traverser 开始遍历
        NumericLiteral: {
            enter(node, parent) {
                // 遇到 NumericLiteral 类型,转化为新的带有语法意义的node,并经过 parent._context push 到新的ast对应的节点中
                // 请注意这里的用词 parent._context 是ast对应的新节点,不会一直是第一层的body节点
                // ----- 分割线 ----- 第一次看到这里请继续往下看代码,忽略掉下面紧挨着的【第一次先忽略】的注释
                // [第一次先忽略]
                // ok,这里咱们继续遍历到 NumericLiteral 的时候,可能已经遍历到了 CallExpression 下的子节点 params 中,可是这个时候,请注意:
                // parent._context 指向的是 expression.arguments 这个节点哦,这就解释了这里是如何经过 parent._context hack了新老ast的对应关系的指向
                parent._context.push({
                    type: 'NumberLiteral',
                    value: node.value
                });
            }
        },
        StringLiteral: {
            // StringLiteral 同上
            enter(node, parent) {
                parent._context.push({
                    type: 'StringLiteral',
                    value: node.value
                });
            }
        },
        CallExpression: {
            // 这里要注意,遇到方法调用表达式的时候,咱们要转为新的表达式类型
            enter(node, parent) {
                let expression = {
                    // 定义新的调用方法的语法node格式
                    type: 'CallExpression',
                    callee: {
                        // 存放方法名称
                        type: 'Identifier',
                        name: node.name
                    },
                    arguments: [] // 存放调用参数
                };
                // 由于 Program 类型下的子节点都在body中,咱们用 _context 来指向新的body,这里的原理是同样的
                // 类型 CallExpression 的全部子节点在 expression.arguments 所以咱们把当前node的 _context 指向 expression.arguments
                // 请回到上面的 【第一次先忽略】 节点所在的注释部分
                node._context = expression.arguments;
                if (parent.type !== 'CallExpression') {
                    // 当父节点的类型不在是 CallExpression 的时候,咱们知道这个表达式部分已经结束了
                    expression = {
                        type: 'ExpressionStatement',
                        expression: expression
                    };
                }
                parent._context.push(expression); // 把结果经过 parent._context 保存到新ast的对应节点中去
            }
        }
    });

    return newAst; // 递归结束后 返回新的ast
};
复制代码

生成最后的output代码

根据新的ast,生成咱们处理事后的代码

const codeGenerator = node => {
    // 区分不一样的类型,进行不一样的 generator
    switch (node.type) {
        case 'Program':
            return node.body.map(codeGenerator).join('\n'); // 对于 Program 只须要加上换行符
        case 'ExpressionStatement':
            return codeGenerator(node.expression) + ';'; // 对于表达式,递归表达式的expression,而后加上分号 保证代码容许的正确性
        case 'CallExpression':
            // 对于方法调用要注意下,咱们的初始的name token被放到callee属性中,同时 params被放到arguments中,所以须要递归这2个属性
            // 同时对于 arguments 要先加括号,再用 , 隔开每一项 以便于生成(x,y,z)这样的调用格式
            return codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')';
        case 'Identifier':
            return node.name; // 对于变量直接返回name就能够
        case 'NumberLiteral':
            return node.value; // 对于数字返回value
        case 'StringLiteral':
            return '"' + node.value + '"'; // 对于字符串须要加上双引号,切记咱们在转码,不是在运行~
        default:
            throw new Error(node.type); // 没法解析的 抛出错误
    }
};
复制代码

到此,咱们的迷你版babel就实现好了,来测试一下:

const code = '(add 2 (add 4 2))'; // 初始的code

const ast = parser(tokenizer(code));
const newAst = transformer(ast);
const output = codeGenerator(newAst);
console.log(output); // 'add(2, add(4,2))'
复制代码

完美,真正的babel源码中会处理不少细节的问题,可是从原理来说,你们都是相通的~,有兴趣的话能够去翻翻babel的源码~

总结

  1. babel的思想仍是要掌握,好比最近很火的各类小程序框架,你们都会基于微信的代码去作转换,若是你不理解babel,可能对转换的过程就不是很懂~
  2. 对js来讲万物皆对象,可是,对babel来讲,万物都是字符,咱们的转化过程当中,input 和 output 都是字符,转换只是将input串修改为指望的output串~

若是对你有点帮助,但愿顺手给个赞,您的赞是我坚持输出的动力,谢谢~

相关文章
相关标签/搜索