状态更改检测,也就是检测应用程序对状态值的改变,这样才会相应地更新 UI。
(#MVC模式中模型Model的改变会更新View界面UI,这点类似后端的ORM,对象状态更改通过ORM框架自动变更相应数据表值)
变更检测是前端框架的基本特征
一个框架对这个问题的解决方式还决定了其他一切:开发人员体验、用户体验、API 表面积、社区满意度和参与度等。
事实证明,从这个角度检查各种框架将为您提供所需的所有信息,以便为您和您的用户确定最佳选择。因此,让我们深入了解每个框架如何处理变更检测。
主要框架比较
我们将研究每个主要参与者以及他们如何处理变更检测,但同样的批判眼光也适用于您可能遇到的任何前端 JavaScript 框架。
ReactJS
React 中的更改检测是通过JS:
开发人员只需通过 API 直接调用 React 的运行时就可更新状态;由于 React 被通知了要进行状态更改,因此它也要知道它需要重新渲染组件。
React长时间发展依赖,编写组件的默认样式已经发生了各种演变变化,但核心原则保持不变。
下面是一个实现按钮计数器的示例组件,以 hooks 风格编写:
export default function App() {
const [count, setCount\] = useState(0);
return (
<div>
<button onClick={() => setCount(count - 1)}>decrement</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>increment</button>
<button onClick={() => setTimeout(() => setCount(count + 1), 1000)}>increment later</button>
</div>
);
}
这里的关键部分是setCount函数,这个函数返回给我们的是React的钩子函数useState。
当调用setCount函数时,React 可以使用其内部虚拟 DOM 比较算法来确定要重新渲染页面的哪些部分。
(请注意,这意味着 React 运行时必须包含在用户下载的应用程序包中。)
React 的变更检测范例很简单:应用程序状态在框架内维护(通过向开发人员公开用于更新它的 API),以便 React 知道何时重新渲染。
Angular
当你搭建一个新的 Angular 应用程序时,变更检测似乎会自动发生:
@Component({
selector: 'counter',
template: \`
<div>
<button (click)="count = count - 1"\>decrement</button>
<span>{{ count }}</span>
<button (click)="count = count + 1"\>increment</button>
<button (click)="incrementLater()"\>increment later</button>
</div>
\`
})
export **class** Counter {
count = 0;
incrementLater() {
setTimeout(() => {
**this**.count++;
}, 1000);
}
}
这里Angular 使用 NgZone 来观察用户操作,并在每个事件中检查整个组件树。
对于任何合理大小的应用程序,这都会导致性能问题,因为快速检查整个树的成本太高。
因此,Angular 允许开发人员选择不同的变更检测策略,从而为这种行为提供了一个逃生门:OnPush
OnPush 意味着开发者有责任在状态发生变化时通知 Angular,以便 Angular 重新渲染组件。除了默认的天真策略,OnPush 是 Angular 提供的唯一一种变化检测策略。启用 OnPush 后,如果新状态被异步更新,我们必须手动告诉 Angular 的变化检测器检查新状态:
@Component({
selector: 'counter',
template: \`
<div>
<button (click)="count = count - 1"\>decrement</button>
<span>{{ count }}</span>
<button (click)="count = count + 1"\>increment</button>
<button (click)="incrementLater()"\>increment later</button>
</div>
\`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export **class** Counter {
constructor(**private** readonly cdr: ChangeDetectorRef) {}
count = 0;
incrementLater() {
setTimeout(() => {
**this**.count++;
**this**.cdr.markForCheck();
}, 1000);
}
}
对于任何复杂程度的应用来说,这种方法很快就会变得站不住脚。
为了解决这个问题,我们引入了其他解决方案。Angular 文档建议的主要解决方案是将 RxJS 观察对象与 AsyncPipe 结合使用:
enum Action {
INCREMENT,
DECREMENT,
INCREMENT\_LATER
}
@Component({
selector: 'counter',
template: \`
<div>
<button (click)="update.next(Action.DECREMENT)"\>decrement</button>
<span>{{ count | async }}</span>
<button (click)="update.next(Action.INCREMENT)"\>increment</button>
<button (click)="update.next(Action.INCREMENT\_LATER)"\>increment later</button>
</div>
\`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export **class** Counter {
readonly update = **new** Subject<Action>();
readonly count = **this**.update.pipe(
switchScan((prev, action) => {
**switch** (action) {
**case** Action.INCREMENT:
**return** of(prev + 1);
**case** Action.DECREMENT:
**return** of(prev - 1);
**case** Action.INCREMENT\_LATER:
**return** of(prev + 1).pipe(delay(1000));
}
}, 0),
startWith(0)
);
readonly Action = Action;
}
在其内部,AsyncPipe 负责订阅观察对象,在观察对象发出新值时通知变化检测器,并在组件销毁时取消订阅。
可观察对象是对随时间发生的状态变化进行建模的一种强大方法,但也有一些严重的缺点:
- 它们很难调试。
- 学习曲线非常陡峭。
- 它们非常适合对数值流(如鼠标移动)进行建模,但对于更常见的用例(简单的状态变化,如复选框的开/关状态)来说,它们就显得多余了。
为了克服默认变更检测模式的缺点,Angular 团队正在开发一种名为信号(Signals)的新方法。
从概念上讲,信号类似于 Svelte 存储(我们稍后会介绍),从根本上讲,它们解决变更检测问题的方式与 React 相同;框架正在控制应用程序的状态,以便轻松监控变更,并尽可能高效地重新呈现。
这是一个巨大的范式转变,使 Angular 应用程序与其他框架更加相似。
Angular 的变更检测是一场灾难。开发人员有两个次优选择:
- 缓慢而幼稚的默认实现,或者
- 手动管理更改检测的复杂性。
信号将使情况变得更好,尽管已经晚了近十年。
VueJS
Vue 的变更检测方法与 React 和 Angular 略有不同。
您不需要像React那样调用框架函数来更改状态
或像Angular那样更改状态然后通知框架它已更改
而是使用框架专门检测的状态对象来拦截和检测更改。
令人困惑的是,Vue 有两种不同的 API,它们以不同的方式封装了相同的底层变化检测引擎。
在 "Options API "下,你可以定义一个包含状态的对象,Vue 会将该对象的代理版本作为this对象的成员分配给组件的函数使用:
<template>
<div>
<button @click="decrement"\>decrement</button>
<span>{{ count }}</span>
<button @click="increment"\>increment</button>
<button @click="incrementLater"\>increment later</button>
</div>
</template>
<script>
export **default** {
data() {
**return** {
count: 0
};
},
methods: {
decrement() {
**this**.count--;
},
increment() {
**this**.count++;
},
incrementLater() {
setTimeout(() => {
**this**.count++;
}, 1000);
}
}
};
</script>
另外,"Composition API "与 React 的钩子有些类似:调用一个框架函数来检索 Vue 可以监控变化的状态对象:
<script setup>
**import** { ref } from 'vue';
**const** count = ref(0);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function incrementLater() {
setTimeout(() => {
count.value++;
}, 1000);
}
</script>
<template>
<div>
<button @click="decrement"\>decrement</button>
<span>{{ count }}</span>
<button @click="increment"\>increment</button>
<button @click="incrementLater"\>increment later</button>
</div>
</template>
从概念上讲,从 ref() 返回的对象有一个 getter 和一个 setter 值,这样 Vue 就能跟踪对象的变化。
Vue 利用 JavaScript 语言的特性,允许开发人员使用有状态变量,而无需考虑变化检测。
Svelte
从表面上看,Svelte 版本的计数器组件与其他框架非常相似:
<script>
let count = 0;
function decrement() {
count--;
}
function increment() {
count++;
}
function incrementLater() {
setTimeout(() => {
count++;
}, 1000);
}
</script>
<div>
<button on:click="{decrement}"\>decrement</button>
<span>{count}</span>
<button on:click="{increment}"\>increment</button>
<button on:click="{incrementLater}"\>increment later</button>
</div>
但相比之下,Svelte 的变化检测方法就显得非常新颖了。
在编译时,Svelte 会分析组件代码的 AST(抽象语法树),并在编译输出中注入一些代码,以便在必要时对 DOM 进行手术式更新。
例如,编译后的 decrement() 函数就是这样的:
function decrement() {
$$invalidate(0, count--, count);
}
其中 $$invalidate 是对 Svelte 内部的调用,用于指示编译后的组件更新 DOM。
这种编译时方法意味着 Svelte 应用程序不需要将大型运行时与应用程序本身捆绑在一起。
Svelte 取得了罕见的双赢平衡:开发人员根本不必考虑变更检测,并且可以直观地与状态变量交互;然而,最终用户的体验通过更好的性能得到了改善,因为一个最低限度的应用程序(内置更改检测)被发送到浏览器。
总结
这些只是从开发人员的角度举例说明。每种方法都会对最终用户的应用程序性能产生影响。
React、Vue 和 Angular 都会向用户的浏览器发送运行时,需要对运行时进行解析和执行。
Svelte 选择采用编译时框架,因此在大多数情况下都不需要运行时,这样用户就能获得更快的加载体验。
每种框架都有一些微妙之处,使其更容易受到最终用户会遇到的特定错误类别(通常与状态管理或变更检测有关)的影响。
原文:https://www.jdon.com/67402.html