现代框架背后的概念
许多初学者会问“我应该学习哪个框架?”以及“在学习一个框架之前我需要学习多少 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);
但这段代码有一个问题:对 的更改count
(例如对 的更改increment
)不会更新按钮的文本内容。我们可以手动更新所有内容,但对于更复杂的用例,这种方法的扩展性较差。
更新用户的功能count
称为反应性。其工作原理是订阅并重新运行应用程序中已订阅的部分以进行更新。
几乎每个现代前端框架和库都提供了响应式状态管理的方法。解决方案包含三个部分,至少会使用其中一个或多个部分的组合:
- 可观察量/信号
- 不可变更新的协调
- 转译
可观察量/信号
可观察对象本质上是一种结构,它允许通过订阅读取器的函数进行读取。订阅器会在更新时重新运行:
const state = (initialValue) => ({
_value: initialValue,
get: function() {
/* subscribe */;
return this._value;
},
set: function(value) {
this._value = value;
/* re-run subscribers */;
}
});
这个概念的最早用途之一是在knockout中,它使用带有和不带有参数的相同函数进行写/读访问。
这种模式目前正以信号的形式复兴,例如在Solid.js和preact signals中,但在Vue和Svelte的内部也使用了同样的模式。支持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!';
如您所见,未更改项目的引用会被重用。如果协调器检测到不同的对象引用,它会重新使用状态(props、memos、effects、context)运行所有组件。由于读取访问是被动的,因此需要手动将依赖项指定为被动值。
显然,你不应该这样定义状态。你要么从现有属性构造它,要么使用所谓的 Reducer。Reducer 是一个函数,它接受一个状态并返回另一个状态。
react和preact都使用了这种模式。它适合与 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();
};
这基本上是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);
☝使用开发人员工具在空白页面中试用上述两个代码块。
在大多数情况下,框架允许不同的时间让效果在渲染 DOM 之前、期间或之后运行。
记忆化
记忆化 (Memoization) 是指缓存从状态计算出的值,当其衍生状态发生变化时进行更新。它本质上是一种返回衍生状态的效果。
在重新运行其组件功能的框架中,例如react和preact,当其所依赖的状态没有改变时,这允许再次选择退出部分组件。
对于其他框架来说,情况正好相反:它允许您选择组件的某些部分进行反应性更新,同时缓存之前的计算。
对于我们简单的反应系统,备忘录如下所示:
const memo = (fn) => {
let memoized;
effect(() => {
if (memoized) {
memoized.set(fn());
} else {
memoized = state(fn());
}
});
return memoized.get;
};
模板和渲染
现在我们有了纯状态、派生状态和缓存状态,我们希望将其显示给用户。在我们的示例中,我们直接使用 DOM 来添加按钮并更新其文本内容。
为了更加方便开发者,几乎所有现代框架都支持某种领域特定语言,以便在代码中编写类似于所需输出的内容。尽管存在不同的风格,例如.jsx
、.vue
或.svelte
文件,但最终都归结为一种类似于 HTML 的代码中的 DOM 表示,因此基本上
<div>Hello, World</div>
// in your JS
// becomes in your HTML:
<div>Hello, World</div>
你可能会问:“我该把我的状态放在哪里?” 好问题。在大多数情况下,{}
它们用于表达动态内容,既在属性中,也在节点周围。
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>
不过,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>
虽然编译后的代码乍一看可能让人望而生畏,但解释一下这里发生的事情其实很简单。首先,创建包含所有静态部分的模板,然后克隆它以创建其内容的新实例,并添加动态部分并将其连接到状态更改时进行更新。
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>
也有例外。例如,在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>
虽然大多数人会觉得开发体验欠缺,但其他人更喜欢完全掌控自己的代码。根据他们想要解决的问题,省略转译步骤甚至可能更有益处。
许多其他框架允许无需转译即可使用,尽管很少推荐这样做。
“我现在应该学习什么框架或库?”
我有一些好消息和一些坏消息要告诉你。
坏消息是:没有灵丹妙药。没有哪个框架能够在各个方面都比其他框架好很多。每个框架都有其优点和缺点。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