现代框架背后的概念

2025-05-25

现代框架背后的概念

许多初学者会问“我应该学习哪个框架?”以及“在学习一个框架之前我需要学习多少 JS 或 TS?”——无数观点鲜明的文章都在宣传作者所偏爱的框架或库的优势,而不是向读者展示其背后的概念,以便他们做出明智的决定。所以,让我们先把第二个问题解决掉:

“在学习一个框架之前需要学习多少 JS/TS?”

尽可能多地了解它们所基于的概念。你需要了解基本数据类型、函数、基本运算符以及文档对象模型 (DOM),它是 JS 中 HTML 和 CSS 的表示。虽然除此之外的其他知识也都不错,但熟练掌握框架或库并非必要条件。

如果你是完全的初学者,那么《猫咪的 JS》或许是入门的好资源。坚持学习,直到你感到自信,然后继续学习,直到你不再自信。那时,你已经掌握了足够的 JS/TS 知识,可以开始学习框架了。剩下的内容,你可以边学边学。

“你指的是哪些概念?”

  • 状态
  • 效果
  • 记忆化
  • 模板和渲染

所有现代框架的功能都源自这些概念。

状态

状态只是驱动应用程序的数据。它可能存在于全局层面,也可能存在于应用程序的更大部分,甚至存在于单个组件中。我们以一个简单的计数器为例。它保存的计数就是状态。我们可以读取状态,也可以写入状态来增加计数。

最简单的表示通常是一个包含我们状态所包含的数据的变量:

let count = 0;
const increment = () => { count++; };
const button = document.createElement('button');
button.textContent = count;
button.addEventListener('click', increment);
document.body.appendChild(button);
Enter fullscreen mode Exit fullscreen mode

但这段代码有一个问题:对 的更改count(例如对 的更改increment)不会更新按钮的文本内容。我们可以手动更新所有内容,但对于更复杂的用例,这种方法的扩展性较差。

更新用户的功能count称为反应性。其工作原理是订阅并重新运行应用程序中已订阅的部分以进行更新。

几乎每个现代前端框架和库都提供了响应式状态管理的方法。解决方案包含三个部分,至少会使用其中一个或多个部分的组合:

  • 可观察量/信号
  • 不可变更新的协调
  • 转译

可观察量/信号

可观察对象本质上是一种结构,它允许通过订阅读取器的函数进行读取。订阅器会在更新时重新运行:

const state = (initialValue) => ({
  _value: initialValue,
  get: function() {
    /* subscribe */;
    return this._value; 
  },
  set: function(value) {
    this._value = value;
    /* re-run subscribers */;
  }
});
Enter fullscreen mode Exit fullscreen mode

这个概念的最早用途之一是在knockout中,它使用带有和不带有参数的相同函数进行写/读访问。

这种模式目前正以信号的形式复兴,例如在Solid.jspreact signals中,但在VueSvelte的内部也使用了同样的模式支持Angular反应层的RxJS是这一原则在简单状态之外扩展,但有人可能会说,它对复杂性进行建模的能力就像一整套瞄准你的武器。Solid.js还以存储(可通过 setter 操作的对象)和可变对象(可像普通 JS 对象或Vue中的状态一样使用以处理嵌套状态对象的对象)的形式对这些信号进行了进一步的抽象。

不可变状态的协调

不变性意味着如果对象的属性发生变化,整个对象引用也必须改变,因此通过简单的引用比较就可以轻松检测到是否有变化,这就是协调器所做的。

const state1 = {
  todos: [{ text: 'understand immutability', complete: false }],
  currentText: ''
};
// updating the current text:
const state2 = {
  todos: state1.todos,
  currentText: 'understand reconciliation'
};
// adding a to-do:
const state3 = {
  todos: [
    state.todos[0],
    { text: 'understand reconciliation', complete: true }
  ],
  currentText: ''
};

// this breaks immutability:
state3.currentText = 'I am not immutable!';
Enter fullscreen mode Exit fullscreen mode

如您所见,未更改项目的引用会被重用。如果协调器检测到不同的对象引用,它会重新使用状态(props、memos、effects、context)运行所有组件。由于读取访问是被动的,因此需要手动将依赖项指定为被动值。

显然,你不应该这样定义状态。你要么从现有属性构造它,要么使用所谓的 Reducer。Reducer 是一个函数,它接受一个状态并返回另一个状态。

reactpreact都使用了这种模式。它适合与 vDOM 一起使用,我们将在稍后介绍模板时进行探讨。

并非所有框架都使用其 vDOM 来实现状态完全响应。例如,Mithril.JSm.redraw()会在组件中设置事件后根据状态变化进行更新;否则,您必须手动触发。

转译

转译是一个构建步骤,它重写我们的代码,使其在旧版浏览器上运行或赋予其额外的功能;在这种情况下,该技术用于将简单变量更改为反应系统的一部分。

Svelte基于转译器,该转译器还通过看似简单的变量声明和访问为其反应系统提供支持。

顺便说一句,Solid.js使用转译,但不是为了其状态,而只是为了模板。

效果

在大多数情况下,我们需要对响应式状态进行更多处理,而不仅仅是从中派生并将其渲染到 DOM 中。我们必须管理副作用,这些副作用是指除了视图更新之外,由于状态变化而发生的所有事件(尽管有些框架,例如Solid.js,也将视图变化视为副作用)。

还记得第一个例子中故意省略了订阅处理吗?让我们来补充一下,处理更新后的效果:

const context = [];

const state = (initialValue) => ({
  _subscribers: new Set(),
  _value: initialValue,
  get: function() {
    const current = context.at(-1);
    if (current) { this._subscribers.add(current); }
    return this._value;
  },
  set: function(value) {
    if (this._value === value) { return; }
    this._value = value;
    this._subscribers.forEach(sub => sub());
  }
});

const effect = (fn) => {
  const execute = () => {
    context.push(execute);
    try { fn(); } finally { context.pop(); }
  };
  execute();
};
Enter fullscreen mode Exit fullscreen mode

这基本上是preact 信号Solid.js中的反应状态的简化,没有错误处理和状态变异模式(使用接收前一个值并返回下一个值的函数),但这很容易添加。

它允许我们使前面的示例具有反应性:

const count = state(0);
const increment = () => count.set(count.get() + 1);
const button = document.createElement('button');
effect(() => {
  button.textContent = count.get();
});
button.addEventListener('click', increment);
document.body.appendChild(button);
Enter fullscreen mode Exit fullscreen mode

☝使用开发人员工具在空白页面中试用上述两个代码块。

在大多数情况下,框架允许不同的时间让效果在渲染 DOM 之前、期间或之后运行。

记忆化

记忆化 (Memoization) 是指缓存从状态计算出的值,当其衍生状态发生变化时进行更新。它本质上是一种返回衍生状态的效果。

在重新运行其组件功能的框架中,例如reactpreact,当其所依赖的状态没有改变时,这允许再次选择退出部分组件。

对于其他框架来说,情况正好相反:它允许您选择组件的某些部分进行反应性更新,同时缓存之前的计算。

对于我们简单的反应系统,备忘录如下所示:

const memo = (fn) => {
  let memoized;
  effect(() => {
    if (memoized) {
      memoized.set(fn());
    } else {
      memoized = state(fn());
    }
  });
  return memoized.get;
};
Enter fullscreen mode Exit fullscreen mode

模板和渲染

现在我们有了纯状态、派生状态和缓存状态,我们希望将其显示给用户。在我们的示例中,我们直接使用 DOM 来添加按钮并更新其文本内容。

为了更加方便开发者,几乎所有现代框架都支持某种领域特定语言,以便在代码中编写类似于所需输出的内容。尽管存在不同的风格,例如.jsx.vue.svelte文件,但最终都归结为一种类似于 HTML 的代码中的 DOM 表示,因此基本上

<div>Hello, World</div>

// in your JS
// becomes in your HTML:

<div>Hello, World</div>
Enter fullscreen mode Exit fullscreen mode

你可能会问:“我该把我的状态放在哪里?” 好问题。在大多数情况下,{}它们用于表达动态内容,既在属性中,也在节点周围。

JS 最常用的模板语言扩展无疑是 JSX。对于React来说,它被编译为纯 JavaScript,从而允许其创建 DOM 的虚拟表示,即一种称为虚拟文档对象模型(简称 vDOM)的内部视图状态。

这是基于这样的前提:创建对象比访问 DOM 快得多,所以如果可以用当前 DOM 替换后者,就能节省时间。然而,如果你本来就有大量 DOM 变更,或者创建了无数个对象却没有任何变更,那么这种解决方案的优势很容易变成劣势,需要通过记忆化来规避。

// original code
<div>Hello, {name}</div>

// transpiled to js
createElement("div", null, "Hello, ", name);

// executed js
{
  "$$typeof": Symbol(react.element),
  "type": "div",
  "key": null,
  "ref": null,
  "props": {
    "children": "Hello, World"
  },
  "_owner": null
}

// rendered vdom
/* HTMLDivElement */<div>Hello, World</div>
Enter fullscreen mode Exit fullscreen mode

不过,JSX 并不局限于 React。例如,Solid 使用它的转译器可以更彻底地改变代码:

// 1. original code
<div>Hello, {name()}</div>

// 2. transpiled to js
const _tmpl$ = /*#__PURE__*/_$template(`<div>Hello, </div>`, 2);
(() => {
  const _el$ = _tmpl$.cloneNode(true),
    _el$2 = _el$.firstChild;
  _$insert(_el$, name, null);
  return _el$;
})();

// 3. executed js code
/* HTMLDivElement */<div>Hello, World</div>
Enter fullscreen mode Exit fullscreen mode

虽然编译后的代码乍一看可能让人望而生畏,但解释一下这里发生的事情其实很简单。首先,创建包含所有静态部分的模板,然后克隆它以创建其内容的新实例,并添加动态部分并将其连接到状态更改时进行更新。

Svelte 更进一步,不仅可以转换模板,还可以转换状态。

// 1. original code
<script>
let name = 'World';
setTimeout(() => { name = 'you'; }, 1000);
</script>

<div>Hello, {name}</div>

// 2. transpiled to js
/* generated by Svelte v3.55.0 */
import {
        SvelteComponent,
        append,
        detach,
        element,
        init,
        insert,
        noop,
        safe_not_equal,
        set_data,
        text
} from "svelte/internal";

function create_fragment(ctx) {
        let div;
        let t0;
        let t1;

        return {
                c() {
                        div = element("div");
                        t0 = text("Hello, ");
                        t1 = text(/*name*/ ctx[0]);
                },
                m(target, anchor) {
                        insert(target, div, anchor);
                        append(div, t0);
                        append(div, t1);
                },
                p(ctx, [dirty]) {
                        if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
                },
                i: noop,
                o: noop,
                d(detaching) {
                        if (detaching) detach(div);
                }
        };
}

function instance($$self, $$props, $$invalidate) {
        let name = 'World';

        setTimeout(
                () => {
                        $$invalidate(0, name = 'you');
                },
                1000
        );

        return [name];
}

class Component extends SvelteComponent {
        constructor(options) {
                super();
                init(this, options, instance, create_fragment, safe_not_equal, {});
        }
}

export default Component;

// 3. executed JS code
/* HTMLDivElement */<div>Hello, World</div>
Enter fullscreen mode Exit fullscreen mode

也有例外。例如,在Mithril.js中,虽然可以使用 JSX,但我们鼓励你这样写 JS:

// 1. original JS code
const Hello = {
  name: 'World',
  oninit: () => setTimeout(() => {
    Hello.name = 'you';
    m.redraw();
  }, 1000),
  view: () => m('div', 'Hello, ' + Hello.name + '!')
};

// 2. executed JS code
/* HTMLDivElement */<div>Hello, World</div>
Enter fullscreen mode Exit fullscreen mode

虽然大多数人会觉得开发体验欠缺,但其他人更喜欢完全掌控自己的代码。根据他们想要解决的问题,省略转译步骤甚至可能更有益处。

许多其他框架允许无需转译即可使用,尽管很少推荐这样做。

“我现在应该学习什么框架或库?”

我有一些好消息和一些坏消息要告诉你。

坏消息是:没有灵丹妙药。没有哪个框架能够在各个方面都比其他框架好很多。每个框架都有其优点和缺点。React有其钩子规则Angular缺乏简单的信号,Vue缺乏向后兼容性,Svelte 的扩展性不太好,Solid.js禁止解构,Mithril.js并不是真正的响应式框架,仅举几例。

好消息是:没有错误的选择——至少,除非项目需求非常有限,无论是在包大小还是性能方面。每个框架都能胜任其工作。有些框架可能需要根据其设计决策进行调整,这可能会稍微降低你的速度,但无论如何,你都应该能够获得有效的结果。

话虽如此,不使用框架也可能是一个可行的选择。许多项目因为过度使用 JavaScript 而变得糟糕,其实带有少量交互功能的静态页面也能完成工作。

现在您已经了解了这些框架和库所应用的概念,请选择最适合您当前任务的框架。不必担心在下一个项目中切换框架。没有必要学习所有框架。

如果你尝试一个新的框架,我发现最有帮助的事情之一就是联系它的社区,无论是在社交媒体、Discord、GitHub 还是其他地方。他们可以告诉你哪些方法是他们框架的惯用方法,这将帮助你更快地获得更好的解决方案。

“来吧,你肯定有个人喜好!”

如果你的主要目标是就业,我建议你学习React。如果你想要轻松获得性能和控制的出色体验,可以尝试Solid.js;或许你可以在 Solid 的Discord上找到我。

但请记住,所有其他选择都同样有效。你不应该因为我说了就选择一个框架,而应该选择一个最适合你的框架。

如果您读完了全文,感谢您的耐心等待。希望对您有所帮助。欢迎留言评论,祝您拥有美好的一天!

文章来源:https://dev.to/lexlohr/concepts-behind-modern-frameworks-4m1g
PREV
如何使用 SpeaCode 上传代码中的视频/文档?👨‍💻📷🚀
NEXT
What is AWS? A Guide for Beginners.