“知道如何解决每一个已经解决的问题。” ——理查德·费曼

在过去的 2 个月里,我一直在开发我自己的非常简化的 React 版本,称为 Syntact。我不会称它为成熟,但它已经有几个可用的功能,例如:

  • 变量声明

  • 函数声明

  • 个组件

  • 虚拟DOM

  • 动态渲染

除此之外,我还构建了一个自定义编译器来替代Babel。

我为一门名为 Advanced Programming 的课程制作了这个项目,这是我的学士学位 应用计算机科学 的一部分。当我开始这个项目时,我不知道我在做什么。但是多亏了我的教练(Lars Willemsens)和万能的互联网,我设法创造了一些很酷的东西。

这不是一个关于如何制作自己的 React 的教程,但如果你想自己做这种项目,它肯定是一个很好的起点。所以让我们开始吧。

1\。编译器(我们自己的 Babel)

乐星

[Lexing](https://res.cloudinary.com/practicaldev/image/fetch/s--XMdVKOmy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/kto7d02gch00gqpw657g.jpg)

第一步是编写“lexer”或“tokenizer”。 “Lex”代表词法分析,这基本上意味着将您的文本拆分为标记。它被用于创建编程语言,也用于文本处理和其他各种事情。

代币

令牌是代码的一个小单元。它的结构是由一个令牌名称和一个值组成的一对。示例:关键字“let”或“const”是标记。

Lexing with Chevrotain

编写词法分析器是整个过程的第一步,也是最简单的一步。我选择使用工具包Chevrotain来构建我的词法分析器。

要使用 Chevrotain 词法分析器,我们首先必须定义标记:

/// Keywords
const Import: chevrotain.ITokenConfig = createToken({ name: "Import", pattern: /import/ });
const From: chevrotain.ITokenConfig = createToken({ name: "From", pattern: /from/ });
const Return: chevrotain.ITokenConfig = createToken({ name: "Return", pattern: /return/ });
const Const: chevrotain.ITokenConfig = createToken({ name: "Const", pattern: /const/, longer_alt: Identifier });
const Let: chevrotain.ITokenConfig = createToken({ name: "Let", pattern: /let/, longer_alt: Identifier });
...

// We then add all the tokens to an array of tokens
let allTokens = [...]

进入全屏模式 退出全屏模式

好的,所以我们定义了我们的令牌并将它们捆绑在一个数组中。接下来,我们通过将标记传递给构造函数来实例化词法分析器,瞧。就这样 Syntact 词法分析器诞生了。

const syntactLexer: Lexer = new chevrotain.Lexer(allTokens);

进入全屏模式 退出全屏模式

现在我们可以使用这个词法分析器来标记我们的输入。

查看雪佛兰的文档了解更多信息:https://chevrotain.io/docs/tutorial/step1_lexing.html。

解析

[解析](https://res.cloudinary.com/practicaldev/image/fetch/s--YNDxVare--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/fkki5ysc5tkvqyyg9pkr.jpg)

该过程的第二步是解析。解析器将标记列表转换为具体语法树 (CST),这是表示源代码的树数据结构的花哨术语。

为了防止歧义,解析器必须考虑括号和操作顺序。解析本身并不是很困难,但是随着更多功能的添加,解析会变得非常复杂。

用雪佛兰解析

同样,我使用 Chevrotain 为 Syntact 构建了一个解析器。 Chevrotain 解析器分析符合某些语法的标记。

语法

语法是对一组可接受的句子的描述。我们的解析器将使用这个语法来构建它的树。我用 ANTLR 语法语法编写了我的语法。

以下是我的语法文件中的一些示例:

importStatement 
    :  import SEMICOLON
    ;

binaryExpression
    :   atomicExpression operator atomicExpression 
    ;

进入全屏模式 退出全屏模式

在上面的示例中,我们定义了标识符的外观、转义序列是什么以及如何识别导入语句。

但老实说,在使用 Chevrotain 时,实际上并没有必要以这样的方式编写语法以使解析器正常工作。另一方面,它将帮助您更好地了解如何构建解析器。

编写解析器

一旦你的语法规划好了,就该开始构建你的解析器了。正如我们之前所说,解析器必须将词法分析器的输出转换为 CST。

首先,我们首先创建一个 Parser 类,我们将使用我们用来定义 Lexer 的标记数组来调用它。

class SyntactParser extends CstParser {
    constructor() {
        super(allTokens)
        this.performSelfAnalysis()
    }

  // Later on, all grammer rules will come here...

}

进入全屏模式 退出全屏模式

接下来我们在 Parser 类中编写语法规则。两个(缩短的)示例:

public importStatement = this.RULE("importStatement", () => {
        this.SUBRULE(this.import)
        this.CONSUME(Semicolon)
    });
});

public function = this.RULE("function", () => {
        this.CONSUME(Function)
        this.CONSUME(Identifier)
        this.CONSUME(OpenRoundBracket)
        this.SUBRULE(this.parameterDeclaration)
        this.CONSUME(CloseRoundBracket)
        this.CONSUME(OpenCurlyBracket)
        this.MANY(() => {
            this.OR([
                { ALT: () => { this.SUBRULE1(this.declareVariableStatement) } },
                { ALT: () => { this.SUBRULE(this.functionStatement) } },
                { ALT: () => { this.SUBRULE(this.functionCall) } }
            ])
        })
        this.OPTION(() => this.SUBRULE(this.returnStatement))
        this.CONSUME(CloseCurlyBracket)
    });

进入全屏模式 退出全屏模式

我们将根据我们之前使用 ANTLR 语法语法绘制的语法来编写语法规则。

一旦完成 - 相信我,这需要一段时间 - 我们可以开始解析令牌。输出将是雪佛兰为我们构建的 CST。

AST

一旦我们有了 CST,我们将把它转换成抽象语法树 (AST)。 AST 类似于 CST,但它包含特定于我们程序的信息,这意味着它不包含不必要的信息,如_分号_或_大括号_。为了获得 AST,我们必须使用 CST 访问者或我喜欢称之为解释器的方式“访问”CST。

口译员

解释器将遍历我们的 CST 并为我们的 AST 创建节点。感谢雪佛兰,这是一个相对可行的步骤。

下面是对 Syntact 解释器的简要介绍:

class SyntactInterpreter extends SyntactBaseCstVisitor {

    constructor() {
        super();
        this.validateVisitor();
    }

    ...

    declareComponent(ctx: any) {
        const componentName = ctx.Identifier[0].image;
        const parameters = this.visit(ctx.parameterDeclaration);
        const returnStatement = this.visit(ctx.returnStatement);
        const variableStatements = [];

        if (ctx.declareVariableStatement) {
            ctx.declareVariableStatement.forEach((e: any) => {
                variableStatements.push(this.visit(e))
            })
        }

        return {
            type: types.COMPONENT_DECLARATION,
            id: {
                type: types.IDENTIFIER,
                name: componentName
            },
            parameters,
            body: { variableStatements },
            returnStatement
        };
    }

    ...
}

进入全屏模式 退出全屏模式

发电机

明白 AST 的意义了吗?凉爽的!现在我们可以继续并从生成器开始。生成器实际上将基于 AST 生成 JS 代码。

我发现这是整个解析过程中最难的部分之一。您必须遍历 AST 中的所有节点并从中生成有效的 JS 代码。

这可能是这样的:

class SyntactGenerator implements Generator {

    ...

    private convertFunBody(body: any) {
        let returnCode: any[] = [];

        if (body.variableStatements) {
            body.variableStatements.forEach((vS: any) => {
                let datatype = vS.dataType;
                let varName = vS.variableName;
                let value = vS.value;
                returnCode.push(`${datatype.toLowerCase()} ${varName} = ${value};\n`)
            });
        }

        if (body.functionCalls) {
            body.functionCalls.forEach((fC: any) => {
                let params: string[] = [];
                if (fC.params) {
                    fC.params.forEach((p: string) => { params.push(p) })
                }
                returnCode.push(`${fC.function}(${params.join(",")});`)
            });
        }

        return returnCode.join("");
    }

    ...
}

进入全屏模式 退出全屏模式

Err,请再来一次。

[https://dev-to-uploads.s3.amazonaws.com/uploads/articles/khgr3csbrfcrnsfyn4f5.jpg](https://res.cloudinary.com/practicaldev/image/fetch/s--HfjIp5JX --/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/khgr3csbrfcrnsfyn4f5.jpg)

读完这一切后筋疲力尽和有点困惑?我明白了。这是一个回顾:

  1. Lexer u003d> 负责将原始文本转换为 tokens 流。

  2. Parser u003d> 将标记流转换为 Concrete Syntax Tree (CST)。

  3. CST 访问者/解释器 u003d> 递归访问 CST 中的每个节点,从而生成一个抽象语法树 (AST)。

  4. Generator u003d > 实际上makes JS code 基于提供的AST。

一旦我们完成了上述工作,我们就可以开始制作我称之为“SyntactEngine”的东西了。

语法引擎

接下来,我创建了一个 SyntactEngine 类。这将使我们更容易协调将 JSX 转换为 JS 的不同阶段。它拥有一个名为“transpileJsxToJs”的入口点方法,我们稍后可以在我们的Webpack 加载器中使用它。

class SyntactEngine implements Engine {
    private lexer: Lexer;
    private parser: SyntactParser;
    private interpreter: SyntactInterpreter;
    private generator: Generator;

    constructor() {
        ...
    }

    transpileJsxToJs(input: string): string {
        ...
    }

    tokenizeInput(input: string): ILexingResult {
        ...
    }

    parseInput(lexingResult: ILexingResult): ParseResultType {
        ...
    }

    toAst(parsedInput: ParseResultType) {
        ...
    }

    generateJsFromAst(ast: string): string {
        ... 
    }

}

进入全屏模式 退出全屏模式

2\。语法 API

我们有一个可以从 JSX 生成 JS 代码的工作编译器。现在我们需要构建一个 Syntact API,它实际上可以做像 React 这样的框架可以做的事情。创建一个虚拟 DOM,保存状态等等。

我现在只是坚持一个简单的虚拟 DOM。为此,我做了一个小的递归算法,它根据初始给定元素(例如 div)及其所有成员创建一个 DOM。

这是该方法的简化版本:

createDom(type: string, props: any, members: any, value: string | null) {
        const element: any = document.createElement(type, null);

        props.forEach((prop: any) => {

            if (prop.type.substring(0, 2) === 'on') {
                /* Check if prop type is a function handler
                 * Note: eval might be a security risk here. */
                element[prop.type.toLowerCase()] = () => {
                    eval(prop.value)
                }
            } else if (prop.type == 'class') {
                element.classList.add(prop.value)
            }
        });

        return element;

    }

进入全屏模式 退出全屏模式

3\。 Web客户端+Webpack

一旦我们有了编译器和 Syntact API,我们就可以开始使用 webpack 加载器将两者集成到我们的客户端应用程序中。

webpack 加载器将使用编译器对 Syntact JSX 进行预处理,并将其转换为 JS 代码。然后,JS 代码将使用 Syntact API 来实际使用 Syntact 的功能。

[流](https://res.cloudinary.com/practicaldev/image/fetch/s--udsLQ0bm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/0dji7txtei33383yrf2m.jpg)

结束

如果你能做到这一点,感谢阅读!我希望这篇文章能帮助你理解 React 和 Babel 是如何在幕后工作的。

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐