许多初学者经常会问 “我需要学习哪个框架 ?” 以及 “学习框架前需要掌握多少 JS 或者 TS ?” 无数带有主观色彩的文章都在宣传作者首选框架或库的优势,而不是向读者展示其背后的概念以做出更明智的决定。所以让我们先解决第二个问题
学习框架前需要掌握多少 JS 或者 TS
尽可能多地去学以让更好的你理解它们所基于的概念。你将需要了解基本数据类型、函数、基本运算符和文档对象模型 ( DOM ),这是 HTML 和 CSS 在 JS 中的表示。除此之外的一切也都 OK,但并不严格要求某个精通框架或库。
如果你是一个完完全全的新手,JS for cats 应该是一个不错的入门资料。持续学习,直到你感到自信为止,然后继续前进,直到你再次感到自信为止。当掌握了足够的 JS / TS 知识后,你就可以开始学习框架。其他的知识你可以并行学习。
哪些重要概念
State (状态)
Effects (副作用)
Memoization (记忆化)
Templating and rendering (模板与渲染)
所有现代框架都从这些概念中派生出它们的功能
state
State 只是为你的应用程序提供动力的数据。它可能在全局级别,适用于应用程序的大部分组件,或适用于单个组件。让我们写一个计数器的简单例子来说明一下。它保留的计数是 state 。我们可以读取 state 或者写入 state 以增加计数
最简单的表示通常是一个变量,其中包含我们的状态所包含的数据:
let count = 0;
const increment = () => { count++; };
const button = document.createElement(‘button’);
button.textContent = count;
button.addEventListener(‘click’, increment); document.body.appendChild(button);
但这个代码有个问题:类似调用 increment 方法一样去修改 count 的值 ,并不会自动修改 button 的文案。我们需要手动去更新所有的内容,但这样的做法在复杂场景下代码的可维护性 & 扩展性都不是很好。
让 count 自动更新依赖它的使用方的能力称之为 reactivity(响应式) 。这是通过订阅并重新运行应用程序的订阅部分来更新的。
几乎所有的现代前端框架和库都拥有让 state 变成 reactivity 的能力。基本上可以分为 3 种解决方案,采用其中至少一种或者多种混用来实现这个能力:
Observables / Signals (可观察的 / 信号)
Reconciliation of immutable updates (协调不可变的更新)
Transpilation (转译)
这些概念还是直接用英文表达比较贴切 🤣
Observables / Signals (可观察的 / 信号)
Observables 基本上是在读取 state 的时候通过一个订阅方法来收集依赖,然后在更新的时候触发依赖的更新
const state = (initialValue) => ({
_value: initialValue,
get: function() {
/* 订阅 /;
return this._value;
},
set: function(value) {
this._value = value;
/ 触发更新 */;
}
});
knockout 是最早使用这个概念的框架之一,它使用带有 / 不带参数的相同函数进行写/读访问
这种模式最近有开始有框架通过 signals 来实现,比如 Solid.js 和 preact signals ;相同的模式也在 Vue 和 Svelte 中使用到。RxJS 为 Angular 的 reactive 层提供底层能力,是这一模式的延伸,超越了简单状态。Solid.js 用 Stores(一些通过 setter 方法来操作的对象)的方式进一步抽象了 signals
Reconciliation of immutable states(协调不可变的更新)
不可变意味着如果对象的某个属性发生改变,那么整个对象的引用就会发生改变。所以协调器做的事情就包括通过简单的引用对比就判断出对象是否发生了改变
const state1 = {
todos: [{ text: ‘understand immutability’, complete: false }],
currentText: ‘’
};
// 更新 currentText 属性
const state2 = {
todos: state1.todos,
currentText: ‘understand reconciliation’
};
// 添加一个 todo
const state3 = {
todos: [
state1.todos[0],
{ text: ‘understand reconciliation’, complete: true }
],
currentText: ‘’
};
// 由于不可变性,这里将会报错
state3.currentText = ‘I am not immutable!’;
如你所见,未变更项目的引用被重新使用。如果协调器检测到不同的对象引用,那么它将重新运行所有的组件,让所有的组件的 state (props, memos, effects, context) 都使用最新的这个对象。由于读取访问是被动的,所以需要手动指定对响应值的依赖。
很显然,你不会用上面这种方式定义 state 。要么你是从一个已经存在的属性构造 state ,要么你会使用 reducer 来构造 state。一个 reducer 函数就是接收一个 state 对象然后返回一个新的 state 对象。
react 和 preact 就使用这种模式。它适合与 vDOM 一起使用,我们将在稍后描述模板时探讨它。
并不是所有的框架都借助 vDOM 将 state 变成完成响应式。例如 Mithril.JS 要不是在 state 修改后触发对应的生命周期事件,要不是手动调用 m.redraw() 方法,才能够触发更新
Transpilation(转译)
Transpilation 是在构建阶段,重写我们的代码让代码可以在旧的浏览器运行或者赋予代码其他的能力;在这种情况下,转译则是被用于把一个简单的变量修改成响应式系统的一部分。
Svelte 就是基于转译器,该转译器还通过看似简单的变量声明和访问为他们的响应式系统提供能力
另外,Solid.js 也是使用 Transpilation ,但 Transpilation 只使用到模版上,没有使用到 state 上
Effects
大部分情况下,我们需要做的更多是操作响应式的 state,而很少需要操作基于 state 的 DOM 渲染。我们需要管理好副作用,这些副作用是由于视图更新之外的状态变化而发生的所有事情(虽然有些框架把视图更新也当作是副作用,例如
https://b23.tv/5CVC7NX
https://b23.tv/BY7NBFf
https://b23.tv/0eHr98g
https://b23.tv/qVGZIUB
记得之前 state 的例子中,我们故意把订阅操作的代码留空。现在让我们把这些留空补齐来处理副作用,让程序能够响应更新
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 signals 或者 Solid.js 响应式 state 的简化版本,它不包含错误处理和复杂状态处理(使用一个函数接收之前的状态值,返回下一个状态值),但这些都是很容易就可以加上的