许多初学者会问“我应该学习哪个框架?”和“在学习框架之前,我需要学多少 JS 或 TS?” - 无数意见文章都在宣传作者喜欢的框架或库的优势,而不是向读者展示背后的概念,以便进行明智的决策。那么,让我们先解决第二个问题:
“在学习框架之前,我需要学多少 JS/TS?”
在学习框架之前,你需要掌握足够多的基础知识,使你能够理解它们所基于的概念。这些知识包括基本数据类型、函数、基本运算符和文档对象模型 (DOM)。虽然除此之外的知识并不会有害,但严格来说不是掌握框架或库所必需的。
如果你是完全的新手,JS for cats 可能是你的第一步的好资源。继续前进,直到你感到自信为止。这就是你知道足够多的 JS/TS 的时候,可以转向框架。其余的东西你可以在过程中学习。
你指的是哪些概念?
- State
- Effects
- Memoization
- Templating and rendering
所有现代框架都从这些概念中获得其功能。
State
状态只是为应用程序提供动力的数据。 它可能在应用程序的较大部分的全局级别上,也可能是单个组件上。 以简单的计数器为例。 它保留的计数即为状态。 我们可以读取状态并写入它以增加计数。
最简单的表示通常是一个变量,其中包含我们的状态所包含的数据:
代码语言:javascript复制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
能够更新其用户的能力称为响应性。 这是通过订阅并重新运行应用程序的订阅部分来更新而实现的。
几乎每种现代的前端框架和库都有一种方法来管理反应性状态。 解决方案有三个部分,至少使用一个或多个部分:
- 可观测值/信号
- 不可变更新的协调
- 转换
可观测值/信号
可观测值基本上是允许通过订阅读者的函数进行读取的结构。 然后在更新时重新运行订阅者:
代码语言:javascript复制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 也使用了相同的模式。
RxJS 是这个原则在简单状态之外的延伸,但可以说它模拟复杂性的能力是针对你的脚的一整套枪。 Solid.js 还提供了这些信号的进一步抽象,即存储(可以通过 setter
操作的对象)和可变对象(可以像正常的 JS 对象一样使用的对象或 Vue 中的状态来处理嵌套状态对象)。
不可变更新的协调
不可变意味着,如果对象的属性发生更改,则必须更改整个对象引用,因此可以轻松检测是否存在更改(这就是协调器所做的),只需简单比较引用。
代码语言:javascript复制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: ''
};
//这破了不变性:
state3.currentText = 'I am not immutable!';
如你所见,未更改项的引用被重用。 如果协调器检测到不同的对象引用,它将使用状态(props,memos,effects,context)再次运行所有组件。 由于读访问是被动的,因此需要手动指定对响应性值的依赖关系。
显然,我们不会以这种方式定义状态。 要么从现有属性构造它,要么使用所谓的 reducer
。 reducer
是一个将一个状态转换为另一个状态的函数。
React 和 preact 使用了这种模式。 它适用于与 vDOM 一起使用,我们将在后面描述模板时进一步探讨。
并非每个框架都使用其 vDOM 使状态完全响应性。 例如,Mithril.JS 在组件中设置的事件之后从状态更改中更新; 否则,必须手动触发 m.redraw()
。
转换
转换是一个构建步骤,它重写我们的代码,使其在旧浏览器上运行或使其具有额外的能力;在这种情况下,技术用于将简单变量变为反应系统的一部分。
Svelte 基于转换器,该转换器也从看似简单的变量声明和访问中为其反应系统提供动力。
顺便提一下,Solid.js 使用转换,但不是用于其状态,只是用于模板。
Effects
在大多数情况下,我们需要做更多的事情来处理响应性状态,而不仅仅是从中导出并将其渲染到 DOM 中。 我们必须管理副作用,这是所有由于状态更改而发生的事情(尽管一些像 Solid.js 的框架将视图更改视为effects )。
记得第一个来自状态的示例吗,其中订阅处理故意省略了? 让我们填充这个以在更新时处理effects :
代码语言:javascript复制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 中响应态的简化,但没有错误处理和状态变化模式(使用接收前一个值并返回下一个值的函数),但是很容易添加。
这可会使上一个示例变成向应性:
代码语言:javascript复制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);
在大多数情况下,框架允许使用不同的时间来让 effects 在渲染 DOM 之前、期间或之后运行。
Memoization
Memoization 指的是缓存从状态中计算出来的值,以便在它来源的状态更改时更新。它基本上是一个 effect,返回一个派生的状态。
在像 React 和 Preact 这样重新运行组件函数的框架中,这允许在其依赖的状态不变时再次选择组件的一部分。
对于其他框架,情况正好相反:它允许你将组件的部分与响应性更新相关联,同时缓存前一个计算。
对于我们简单的响应系统,memo 看起来像这样:
代码语言:javascript复制const memo = (fn) => {
let memoized;
effect(() => {
if (memoized) {
memoized.set(fn());
} else {
memoized = state(fn());
}
});
return memoized.get;
};
Templating and rendering
既然我们有了纯状态、派生状态和缓存状态,我们想要向用户显示它。在我们的示例中,我们直接使用 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 的虚拟表示,称为虚拟文档对象模型(virtual document object model,简称 vDOM)的内部视图状态。
这是基于这样一个前提:创建对象比访问 DOM 快得多,因此如果你可以用当前值替换后者,就可以节省时间。然而,如果在任何情况下都有大量 DOM 更改或者为了没有更改而创建无数个对象,这种解决方案的优势很容易变成劣势,需要通过缓存来规避。
代码语言:javascript复制// 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使用其转码器来更大幅度地改变代码。
代码语言:javascript复制// 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甚至进一步转译了模板和状态。
代码语言:javascript复制// 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。
代码语言:javascript复制// 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破坏了,而静态页面加上一些互动性的东西也能完成工作。
现在你知道了这些框架和库所应用的概念,选择那些最适合你当前任务的框架。不要害怕在你的下一个项目中转换框架。没有必要学习所有的框架。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://dev.to/lexlohr/concep...