创建 RawJS 之后,我再也没有碰过 React。

2025-06-10

创建 RawJS 之后,我再也没有碰过 React。

补充:我是 RawJS 的作者。各位,请冷静一下。在正文中省略了这一点是一个疏忽,而且我从一开始就在我的简介中声明了这个库的作者身份。

编辑2:我正在考虑锁定这篇文章的评论,重写,并添加链接。有些人对我部分内容的组织方式提出了严重质疑。我认为这会引导讨论,对社区没有益处。我会把这篇文章留在这里,但如果评论区继续出现骚动,我会锁定并重写。

早在 2012 年 10 月,TypeScript 0.8 就发布了。当时我正在开发一个中型 JavaScript 项目。发布当天,我仔细阅读了初始规范,大约试用了 10 分钟后,就坚信 TypeScript 就是未来的发展方向,于是我开始用 TypeScript 重写我的整个应用程序。相比标准的无类型 JavaScript,它的优势实在是太显著了。

我对RawJS也有同样的感觉

RawJS 是一个小型库,它使原生 JavaScript 应用开发更加人性化。它不仅仅是与 React、Vue、Svelte 或其他框架竞争的当今最新 Web 框架。RawJS 与众不同。

RawJS 深刻地证明了为什么框架本身的整个前提可能有点误导。它表明,大多数应用程序最好使用原生 JS 并采用某些编程模式。

我知道,这是一个相当大胆的声明。但我恳请你去探究一下 RawJS 诞生背后的理念和思考。

React 往往会以极高的复杂性毒害项目

我认为说 React 过于复杂并不过分。毕竟,Svelte 就是为此而生的。

React 会导致应用程序臃肿。举个例子——我最近接到一个任务,负责监督一个 React 应用的开发,它的复杂性已经失控。最终,我放弃了整个应用,用 RawJS 重新构建了整个应用,而我的团队从未接触过 RawJS,甚至从未进行过直接的 DOM 操作。几周之内,团队就跟上了进度,现在这个应用比之前的 React 应用小了大约 90%。没错,这可不是笔误。

我们现在到了这样的地步:React 是“没人会因为购买 IBM 产品而被解雇”的选择。问题是——人们应该因为购买 IBM 产品而被解雇。这比喻那些懒得做充分需求分析、陷入从众心理、出于恐惧和懒惰而盲目跟风的人。

一旦你体验过使用 RawJS 加速的原生 JavaScript 开发应用,你就会更加清楚地认识到 React 的过度复杂程度。RawJS 质疑了 props、state、hooks、JSX、强制继承特定基础组件(React.Component)、虚拟 DOM、自动数据绑定以及 React 所做的一切的必要性。

React 的膨胀和它所施加的限制(比如禁止你直接编辑 DOM)都是为了试图维护它的虚拟 DOM 系统的完整性,我认为这是针对特定于 facebook.com 的特定领域问题的解决方案,而精心构建的应用程序根本没有这个问题。

所谓的直接 DOM 操作的性能劣势被夸大了。事实上,如果操作得当,直接 DOM 操作通常可以提升性能。这是因为您可以精确控制 DOM 的更新方式。它允许您根据需要对 DOM 的微小区域进行精细更新。虚拟 DOM 与此相反。它是一种钝性工具,在大量 DOM 子树上运行复杂的 diffing 算法,以自动计算需要更新的内容。

自动数据绑定和响应式的实用性也被夸大了。假设你的代码组织良好,那么构建应用让 DOM 随着数据变化而更新,看起来并不会比仅仅构建应用在必要时显式更新 DOM 的代码量少。但前者的区别在于,它会强加给你一个庞大且难以调试的黑盒,并迫使你遵循其官僚主义的规范。除非你构建了一个构建良好的原生 JS 应用(例如使用 RawJS!),否则很难体会到摆脱这种做法的好处。

为什么没有人谈论匿名控制器类?

匿名控制器类 (ACC) 是一种需要更多关注的模式。它是将原生 JavaScript 应用从杂乱无章的状态转变为清晰美观的关键理念之一。

ACC 的基本前提是创建一个与 DOM 中的单个元素松散连接的对象,其垃圾回收生命周期与所连接元素的生命周期相同。这是对继承自 HTMLElement 的改进,后者是另一种选择(但我并不喜欢,原因我会留到另一篇文章再说)。

考虑以下代码:

class SomeComponent {
    readonly head;

    constructor() {
        this.head = document.createElement("div");
        this.head.addEventListener("click', () => this.click());
        // Probably do some other stuff to this.head
    }

    private handleClick() {
        alert("Clicked!")
    }
}
Enter fullscreen mode Exit fullscreen mode

ACC 是创建单个 .head 元素(可能包含其他嵌套元素)、连接事件监听器、分配样式等的类。它们包含一些方法,通常是事件处理程序或其他辅助方法。然后,实例化组件,并将组件的 .head 元素添加到 DOM 中:

const component = new SomeComponent();
document.body.append(component.head);
Enter fullscreen mode Exit fullscreen mode

该类被认为是“匿名的”,因为您可以在组件实例附加到 DOM 后立即将其丢弃。一旦元素从 DOM 中移除,该类的实例也会被垃圾回收,因为 DOM 是唯一拥有对其引用的对象。例如:

class SomeComponent {
    readonly head;

    constructor() {
        this.head = document.createElement("div");
        this.head.addEventListener("click', () => this.remove());
        // Probably do some other stuff to this.head
    }

    private remove() {
        // Remove the component's .head element from the DOM,
        // which will by extension garbage collect this instance of
        // SomeComponent.
        this.head.remove();
    }
}
Enter fullscreen mode Exit fullscreen mode

ACC 的优点在于它们基本上没有任何限制。它们可以继承任何东西(或者什么都不继承)。它们只是一个想法——你可以随意塑造它们,使它们的行为符合你的喜好。

当然,在很多情况下,你可能需要获取与特定元素关联的 ACC。例如,想象一下遍历 this.head 元素的祖先元素,并获取与其关联的 ACC 以调用某些公共方法。有一个名为HatJS的轻量级库,旨在改善使用 ACC 的人体工程学。

补充:我是 HatJS 的作者。“匿名控制器类”这个术语是我发明的。这是一种在实验过程中逐渐浮现的模式,尽管我怀疑自己是不是第一个发现它的人,因为这个概念非常明显。就像 JSON 在有名字之前一样。你不需要将 HatJS 与 RawJS 一起使用。许多人正在构建原生 JavaScript 应用(或者更严谨一点的说是“原生 TypeScript 应用”),他们只需创建继承自 HTMLElement 的自定义元素,就能有效地将元素和控制器合并到同一个实体中,从而获得不错的效果。我已经用这种方法构建了一些应用,并决定匿名控制器类 (ACC) 更胜一筹,具体原因我会在以后的文章中详细介绍。

改进 document.createElement() 具有令人惊讶的强大影响

尽管本文试图尽可能有力地论证直接使用 DOM API 的可行性,但这些 API 的一个明显不足之处是,在构建包含属性、样式和事件附件的复杂 DOM 层级结构时。DOM API 的这一部分非常冗长,如果没有外部帮助,你的代码将会比实际需要长 10 倍左右。这时,RawJS 就派上用场了。

RawJS 的设计初衷只有一个:它让 document.createElement() 的用户体验提升了 10 倍。它只负责调用函数并获取 HTMLElement 实例的层级结构。除此之外,它什么都不做,也没有任何奇怪的背景魔法。你可能觉得这听起来没什么特别之处,但你这样想就错了。

事实证明,在过去 15 年围绕框架模式的构思中,我们并不需要虚拟 DOM、响应式、数据绑定预编译器或任何其他疯狂的科学项目。我们需要的是匿名控制器类模式和一种更好的创建 HTMLElement 实例的方法。

有了这两种技巧,我可以肯定地说,我再也不会刻意使用 React 或任何其他竞争框架了。这类框架的功能远远不够,不足以支撑它们施加如此巨大的负担和繁琐的官僚作风。

那么 RawJS 代码是什么样的呢?对 RawJS 创建函数的调用遵循以下形式:

const htmlElement = raw.div(...parameters);
Enter fullscreen mode Exit fullscreen mode

强大的人体工程学来自于可接受参数的广度(Raw.Param在 RawJS 中类型为 a)。

参数可以是字符串、数字、布尔值、数组、函数、DOM 节点实例、调用raw.on("event", ...)(创建可移植事件附件)以及几乎任何其他类型。RawJS 始终如你所愿。

我不会重复使用 RawJS 构建层级结构有哪些很棒的功能。快速入门中已经详细介绍了这些功能。

主要思想是,由于几乎任何东西都可以是 Raw.Param,因此您可以创建迷你函数库来生成 Raw.Param 列表并返回它们。通过这种方式,您可以实现前所未有的代码重用级别。同样,在实际使用之前很难体会到它的重要性。我不想将它与 LISP/闭包进行比较,但它们之间确实存在相似之处。

我见过的最好的 CSS-in-JS 解决方案

一个 HTML 元素层次结构构造器如果不同时构造相应的 CSS 又有什么用呢?RawJS 还拥有同类最佳的 CSS-in-JS 解决方案,它能做到我在其他任何解决方案中都未曾见过的事情。例如,RawJS 完全支持作用域限定于特定元素的 CSS。

const anchor = raw.a(
    // This constructs CSS within a global style sheet,
    // and the rules below will be scoped to the containing
    // anchor element.
    raw.css(
        ":focus", {
            outline: 0
        },
        ":visited": {
            color: "red"
        }
    ),
    raw.text("Hyperlink!")
);
Enter fullscreen mode Exit fullscreen mode

使用 RawJS 的 CSS-in-JS 解决方案还可以实现很多其他功能。本文并非 RawJS 教程,但如果您需要这类内容,可以参考快速入门指南和演示应用。

使用 DOM 作为状态管理器实际上是好的。

我们经常会遇到一个问题:应用程序状态应该存储在哪里?答案是使用 DOM 作为状态管理器。

在你对此感到震惊之前,还记得 Tailwind 是如何率先提出删除 HTML 元素上数百万个类名的想法的吗?这实际上重新创建了相当于内联 CSS 的功能。多年来,内联 CSS 一直被认为是一种反模式,但开发人员坚持认为它实际上是好的,现在每个人都在这样做了。同样的想法也适用于使用 DOM 作为状态管理器。

如今,大家构建应用的方式通常是先从某种数据结构入手,将其视为“真实数据来源”,然后需要以某种笨拙的方式将其投射到 UI 中。否则,这种方式会被认为是“幼稚的”,甚至是反模式的。然而,我想指出的是,需要保持两种独立的表示形式同步,这本身就是一种反模式。

尝试使用某些框架以声明方式将数据映射到 DOM 中会大幅增加复杂性。这种技术的问题在于,对同一事物使用两种不同的表示形式,必然会比假设的只有一种表示形式的替代技术更加臃肿。

事实证明,如果让 ACC 接受输入数据并自行渲染,效果会更好。然后,你可以通过某种保存函数来检查 DOM 的状态,从而生成可保存的数据块。这样,你的数据源就不必与 DOM 同步,因为你的数据源就是 DOM。

检查以下代码示例:

class FormComponent {
    readonly head;
    private readonly firstNameInput;
    private readonly lastNameInput;

    constructor(firstName: string, lastName: string) {
        this.head = raw.form(
            this.firstNameInput = raw.input({ type: "text", value: firstName }),
            this.lastNameInput = raw.input({ type: "text", value: lastName }),
            raw.button(
                raw.text("Save"),
                raw.on("submit", () => this.save())
            )
        );
    }

    private save() {
        const firstName = this.firstNameInput.value;
        const lastName = this.lastNameInput.value;
        SomeDatabaseSomewhere.save({ firstName, lastName });
        alert("Saved!");
    }
}
Enter fullscreen mode Exit fullscreen mode

看到了吗?你只需将值直接存储在 DOM 中即可。在本例中,我们使用了文本输入的值,但你也可以将数据存储为 HTML 属性、类名或其他任何有意义的形式。

当然,有些情况下你需要存储无法分解为字符串、数字和布尔值的状态。我也见过一些情况,状态存储在ACC的属性中。你可以根据自己的情况选择合适的方法。

组件之间的通信

在某些情况下,您可能需要更新多个组件以响应一个操作,或者更简单地在 ACC 之间发送消息。HatJS 可以帮助您实现这一点。

请记住,ACC 会创建一种隐藏的控制器层次结构。虽然你拥有典型的 DOM 元素层次结构,但只有部分元素是 ACC 的头元素。因此,ACC 会创建自己的 ACC 层次结构,它是整个 DOM 元素层次结构的严格子集。

HatJS具有遍历 ACC 层次结构的功能,可以快速捕获可能位于或不位于元素后面的 ACC 实例。

class ParentComponent {
    readonly head;
    constructor() {
        this.head = raw.div(
            new ChildComponent().head
        );

        // Call Hat.wear() to define the object as a "hat"
        // and make it discoverable by HatJS
        Hat.wear(this);
    }

    callAlert() {
        alert("Hello!");
    }
}

class ChildComponent {
    readonly head;
    constructor() {
        this.head = raw.div(
            raw.on("click", () => {
                // Hat.over finds the "Hat" (or the ACC) that exists
                // above the specified element in the hierarchy.
                // And passing ParentComponent gives you type-safe
                // tells HatJS what kind of component you're looking
                // for, and also gives you type-safe access to it.
                Hat.over(this, ParentComponent).callAlert()
            })
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

除了 之外Hat.over(),还有Hat.under()Hat.nearest()和其他方法来查找相对于指定元素在 DOM 中可能存在或不存在的特定类型的其他 ACC。

兴奋起来!

那么,我是否已经说服你,用 RawJS 重新构建你的 React 应用,并用原生代码重新构建你的毕生心血了呢?如果你想开始的话,可以访问 RawJS 的网站。RawJS 的代码库在这里,HatJS 的代码库在这里。

鏂囩珷鏉簮锛�https://dev.to/user30948756/after-using-rawjs-im-never-touching-react-again-or-any-framework-vanilla-javascript-is-the-future-3ac1
PREV
2024年最佳无头CMS
NEXT
掌握正则表达式成为算法向导(基础版)