探索主流前端框架的响应式原理!

简介: 探索主流前端框架的响应式原理!

本文将探索在各个前端框架中响应式是如何工作的并比较这些框架的不同之处以更好地理解它们。


前置准备

响应式

响应式可以被广义地定义为应用状态变化时自动更新UI。作为开发者,我们只需要关注应用的状态,并让框架将该状态反映到UI 上。但是,框架如何反映该状态可能会对代码的性能和懒加载产生影响,下面会进行深入探讨。

粗粒度 vs 细粒度

在响应式框架之间进行比较的一个维度是粗粒度与细粒度的响应性

  • 粗粒度:框架必须执行大量应用或框架代码来确定哪些 DOM 节点需要更新。
  • 细粒度:框架不需要执行任何代码,就知道哪些 DOM 节点需要更新。

这是一个框架可以处于连续的维度,不过仅仅是众多可以比较的维度之一。本文中也将涉及渲染。在这里,渲染意味着框架确定要更新哪个 DOM 的方式,而不是浏览器因 DOM 更新而执行的实际浏览器渲染。

下面是跟颗粒度得出的一个结论(从左到右表示粗粒度到细粒度):

345.webp.jpg

注意:这并不是一个权威的结果,所以接下俩将深入讨论如何得出这些结论的,也许你会得出不同的结论。

测试应用

在深入讨论如何得出这些结论之前,需要定义一些标准,然后根据这些标准来比较框架之间的响应式行为。从最简单的应用开始:计数器。 计数器需要状态、事件处理和到 DOM 的绑定。

543.webp (1).jpg

这个例子比较简单。在实际的应用中,状态、事件和绑定并不总是在同一个组件中。因此,我们将示例分解成更细粒度的组件,以展示状态存储(Counter)、状态修改(Incrementor)和状态绑定(Incrementor)是如何跨多个组件进行分布的。

456.webp.jpg

接下来引入一个 Wrapper 组件,它是惰性的,仅用于将状态从父组件 Counter 传递到子组件 Display。 在现实的应用中,惰性组件很常见,我们想看看框架如何处理它们。678.webp.jpg

注意: 大多数前端框架提供了机制来优化它们的响应式能力。但是,本文主要探讨框架“开箱即用”的行为,因此这些优化超出了讨论的范围。

React 和 Angular

React 和 Angular 都是粗粒度的,因为状态的改变需要重新执行组件树。 所谓重新执行,是指需要重新运行关联组件的应用代码,以便框架检测更改,以便它可以更新 DOM。

在 React 中,需要重新运行组件以重新创建 vDOM,然后将其与之前的 vDOM 进行比较以确定需要更新哪些 DOM 元素。 在 Angular 中,组件需要重新读取表达式以确定是否需要更新 DOM。

实际上,框架并不知道哪个状态绑定到了哪个 DOM 元素上,框架需要比较当前和以前的 vDOM(或值)以检测变化。

React 示例代码如下:

import * as React from 'react';
import { useState } from 'react';
export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <section>
    <h1>&lt;Counter&gt;: {appCounter()}</h1>
    <Wrapper value={count} />
    <Incrementor setCount={setCount} />
    </section>
  );
}
function Wrapper({ value }: { value: number }) {
  return (
    <section>
    <h1>&lt;Wrapper&gt;: {wrapperCounter()}</h1>
    <Display value={value} />
    </section>
  );
}
function Display({ value }: { value: number }) {
  return (
    <section>
    <h1>&lt;Display&gt;: {displayCounter()}</h1>
    <main>{value}</main>
    </section>
  );
}
function Incrementor({
  setCount,
}: {
  setCount: (fn: (v: number) => number) => void;
}) {
  return (
    <section>
    <h1>&lt;Incrementor&gt;: {incrementorCounter()}</h1>
    <button onClick={() => setCount((v) => v + 1)}>+1</button>
    </section>
  );
}
const appCounter = makeCounter();
const wrapperCounter = makeCounter();
const displayCounter = makeCounter();
const incrementorCounter = makeCounter();
function makeCounter() {
  let count = 0;
  return () => `(re-render count: ${++count})`;
}


在线体验:stackblitz.com/edit/react-…

Svelte

Svelte 使用编译器将 .svelte 文件编译成自定义代码。编译器在生成输出代码方面非常智能和高效。

在初始渲染中,Svelte 必须执行所有组件作为水合的一部分,这就是初始渲染计数为 1 的原因。但在后续交互中,计数不再更新。你可能会认为这是因为 Svelte 是细粒度响应式的,但实际上情况很微妙,让我们深入挖掘一下。

在线示例:stackblitz.com/edit/vitejs…

打开上面的示例,执行以下操作:

  1. 打开一个新的浏览器选项卡,以便调试;
  2. 接着打开浏览器的开发者工具;
  3. 禁用 source maps 以便可以获取生成的代码;
  4. 搜索 p: function以查找 Svelte 执行变更检测的所有位置。
  5. 根据文件名向每个添加 log。例如:Render: Display

当与 Svelte 应用进行交互时,单击会导致CounterWrapperDisplay重新执行,但不会重新执行Incermentor

javascript

复制代码

RenderCounterRenderWrapperRenderDisplay

因此,尽管交互仍会导致组件重新执行,但 Svelte 比React(或Angular)更细粒度。但有一个重要的区别,就是 Svelte 示例没有导致Incrementor重新执行。这是一项重要的优化,因为它允许 Svelte 在不涉及开发人员的情况下修剪树的分支。 在React(或Angular)中也可以实现此操作,但需要开发人员做更多的工作(并非“开箱即用”)。

Svelte编译器在后台会非常高效地进行脏检查。 它要求 Svelte 从更改所在的组件开始,然后访问所有传播更改的子组件,在上面示例中就是 CounterWrapperDisplay

这意味着代码需要不断重新执行,编译器使其非常高效,在实践中永远不会成为性能瓶颈。 但是,代码不断重新执行意味着所有位于渲染树中的组件都不能被懒加载。

Svelte stores

现在 Svelte 编译器只适用于.svelte 文件。 这意味着如果想在 .svelte 文件之外实现响应式,就不能依赖编译器。相反,Svelte 提供了一个单独的机制,称为 stores

上面的 Svelte 示例中,下半部分就是使用 Store 编写的相同应用。当与该应用交互并使用 log 进行测试时,可以看到 Svelte 只重新渲染Display组件,这表明 Svelte 在使用 Store 时比使用编译器更高效(需要执行的代码更少)。

执行更少的代码意味着不需要下载不执行的代码到客户端。但是,水合会导致所有组件在启动时至少运行一次,这意味着无法对渲染树中的组件进行代码懒加载。

Vue

Vue 与 Svelte 相似,它会重新运行组件。单击+1按钮会导致CounterWrapperDisplay重新渲染。与 Svelte 不同的是,Vue 不是基于编译器的响应式,而是基于运行时的。Vue 将响应式原语称为 Refs,它与后面将要讨论的 Signals 类似。

在线示例:stackblitz.com/edit/vitejs…

Vue 可以跨组件边界传递响应式原语。 它可以通过 WrapperRefCounter 传递到 Display。这样做的结果就是,即使 Wrapper 只是一个传递,Wrapper 也必须参与 Display 的重新渲染。

Vue 也要求在启动时进行水合,因此渲染树中的所有组件都必须在启动时执行一次。 这意味着必须下载它们,这使得懒加载很困难。

Vue 依赖注入

Vue 可以通过使用 provideinject API 将 RefCounter 传递到 Display,绕过 Wrapper。如第二个示例所示。 在这种情况下,与应用交互只会导致 Display 重新执行,这是一种更高效的行为。

这种差异是在.vue文件中在组件之间传递值会导致Refs在组件边界上取消包装和重新包装,但是provide/inject允许绕过此包装行为,并直接传递Ref而不需要取消包装,从而实现更细粒度的更新(类似于Svelte stores)。

Vue 比 Svelte 更具有响应式,因为它只有单一的响应式模型Ref,而不像 Svelte 那样具有两个。从.vue文件中移动代码或移回.vue文件不需要进行任何响应式重构。

Qwik

到目前为止,当检测到更改时,都必须至少重新执行一个组件,一个组件是最少的工作量。但是我们可以做得更好:DOM 级别的响应式

Qwik 展示了 DOM 级别上的响应式。 当与应用进行交互时,Signal 连接的不是关联的组件,而是直接连接到 DOM 元素。 更新 Signal 会直接更新 DOM 而无需执行关联的组件。 因为组件没有执行,所以不必被下载。 因此,节省的不是不执行组件而是不下载组件所带来的。

另外值得注意的是,Qwik 不需要在启动时执行水合。 因为没有水合,所以不需要执行任何代码,因此也不需要下载任何代码。

在线示例:stackblitz.com/edit/qwik-s…

在这个例子中,没有在客户端(点击处理程序之外)下载或执行任何应用代码。这是因为 Qwik 可以充分描述 Signal 和 DOM 之间的关系。这种关系是从在服务器上运行应用中提取的(因此应用不需要在浏览器中运行)。

Qwik 不能描述 Signals 中的结构变化。 因此,对于结构更改(添加/删除 DOM 节点),Qwik 就会下载并重新执行组件。

Solid

Solid 与 Qwik 一样,将 Signal 直接与 DOM 更新联系起来。 但是,Solid 不仅可以对常规值进行此操作,还可以对结构性更改进行此操作。

在线示例:stackblitz.com/edit/solidj…

Solid 只会执行一次组件,这些组件永远不会再次执行。与其他框架一样,Solid 需要在水合时执行一次组件,这意味着必须在应用启动时下载和执行所有组件。

Solid 在响应式方面可能是最好的,因为它的响应式可能是最好的形式—— DOM 级别而非组件级别。 但是,它仍然需要水合,这会导致代码下载和执行。

对比

响应式方法

在前端框架中,一般有三种处理响应式的方法:

  • Values: 通过比较当前值和之前的值来检测数据变化。Angular 使用表达式进行比较,React 使用虚拟DOM进行比较,Svelte 使用编译器进行脏数据标记。
  • Observables:在 Angular 中使用RxJS,在Svelte中使用Stores来处理响应式数据。
  • Signals:在 Vue、Qwik和 Solid 框架中使用 Signals。它与 Vue 相连的是组件,Qwik 与 DOM 连接,Solid 使用DOM作为更细粒度的方法。

每种方法都有其特点和适用场景,开发者需要根据具体情况选择合适的方法来处理响应式数据。

组件层次结构

Angular、React、Svelte 和 Vue 在传播对状态的更改时遵循组件层次结构。 (Svelte 和 Vue 也可以直接进行组件更新,但这些并不是“开箱即用”的)而且这些更新始终发生在组件级别。

Qwik 和 Solid 不遵循组件层次结构,而是直接更新 DOM。 在结构变化方面,Solid 比 Qwik 有优势。 Solid 可以进行 DOM 更新,而 Qwik 是进行单个组件更新(不是组件树)。

水合

Qwik 的独特之处在于它是唯一不需要进行水合的框架。 与其他框架不同,Qwik 不需要在启动时执行所有组件来了解状态如何连接到 DOM。 Qwik 将该信息序列化为 SSR/SSG 的一部分,并能够在客户端上恢复。

这种可恢复性使 Qwik 具有独特的优势,因为 Qwik 不需要在启动时下载大部分应用代码。 因此,虽然 Solid 比 Qwik 更细粒度,但 Qwik 的可恢复性使它更具优势。

4445.webp.jpg

参考文章:www.builder.io/blog/reacti…



相关文章
|
2月前
|
JavaScript 前端开发 开发者
Vue.js 框架大揭秘:响应式系统、组件化与路由管理,震撼你的前端世界!
【8月更文挑战第27天】Vue.js是一款备受欢迎的前端JavaScript框架,以简洁、灵活和高效著称。本文将从三个方面深入探讨Vue.js:响应式系统、组件化及路由管理。响应式系统为Vue.js的核心特性,能自动追踪数据变动并更新视图。例如,通过简单示例代码展示其响应式特性:`{{ message }}`,当`message`值改变,页面随之自动更新。此外,Vue.js支持组件化设计,允许将复杂界面拆分为独立且可复用的组件,提高代码可维护性和扩展性。如创建一个包含标题与内容的简单组件,并在其他页面中重复利用。
57 3
|
5天前
|
移动开发 前端开发 JavaScript
浅谈前端路由原理hash和history
该文章详细解析了前端路由的两种模式——Hash模式与History模式的工作原理及其实现方式,并通过实例代码展示了如何在实际项目中运用这两种路由模式。
|
13天前
|
移动开发 缓存 前端开发
构建高效的前端路由系统:从原理到实践
在现代Web开发中,前端路由系统已成为构建单页面应用(SPA)不可或缺的核心技术之一。不同于传统服务器渲染的多页面应用,SPA通过前端路由技术实现了页面的局部刷新与无缝导航,极大地提升了用户体验。本文将深入剖析前端路由的工作原理,包括Hash模式与History模式的实现差异,并通过实战演示如何在Vue.js框架中构建一个高效、可维护的前端路由系统。我们还将探讨如何优化路由加载性能,确保应用在不同网络环境下的流畅运行。本文不仅适合前端开发者深入了解前端路由的奥秘,也为后端转前端或初学者提供了从零到一的实战指南。
|
6天前
|
JavaScript 前端开发 开发者
深入浅出 Vue.js:构建响应式前端应用
Vue.js 是一个流行的前端框架,以其简洁、高效和易学著称。它采用响应式和组件化设计,简化了交互式用户界面的构建。本文详细介绍 Vue.js 的核心概念、基本用法及如何构建响应式前端应用,包括实例、模板、响应式数据和组件等关键要素,并介绍了项目结构、Vue CLI、路由管理和状态管理等内容,帮助开发者高效地开发现代化前端应用。
|
2月前
|
编解码 前端开发 开发者
【前端设计达人必备】揭秘CSS尺寸单位的魔力:从基础到实战,打造灵动响应式网页!
【8月更文挑战第26天】本文深入探讨了CSS中常用的尺寸单位,包括像素(px)、百分比(%)、视窗单位(vw/vh/vmin/vmax)、可伸缩相对单位(em/rem)以及Flexbox和Grid中的fr单位。通过具体案例展示了每种单位的特点及其适用场景。像素适用于固定尺寸元素;百分比和em/rem利于构建响应式布局;视窗单位适合全屏设计;fr单位则能有效管理复杂网格布局的空间分配。掌握这些单位有助于开发者设计出更加灵活、高质量的网页布局。
40 4
|
28天前
|
前端开发 JavaScript 开发者
现代前端框架激烈交锋,高效响应式 Web 界面的归属扑朔迷离!
【9月更文挑战第6天】本文通过实际案例,比较了主流前端框架 Vue.js、React 和 Angular 的特点与优势。Vue.js 以简洁的语法和灵活的组件化架构著称,适合小型到中型项目;React 强调性能和可扩展性,适用于大型应用;Angular 凭借全面的功能和严格架构,适合企业级开发。开发者应根据项目需求和技术栈选择合适的框架。
33 0
|
2月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
22 0
|
2月前
|
开发者 容器 Docker
JSF与Docker,引领容器化浪潮!让你的Web应用如虎添翼,轻松应对高并发!
【8月更文挑战第31天】在现代Web应用开发中,JSF框架因其实用性和灵活性被广泛应用。随着云计算及微服务架构的兴起,容器化技术变得日益重要,Docker作为该领域的佼佼者,为JSF应用提供了便捷的部署和管理方案。本文通过基础概念讲解及示例代码展示了如何利用Docker容器化JSF应用,帮助开发者实现高效、便携的应用部署。同时也提醒开发者注意JSF与Docker结合使用时可能遇到的限制,并根据实际情况做出合理选择。
30 0
|
2月前
|
前端开发 UED 开发者
深度剖析Angular表单控件:从模板驱动到响应式表单的最佳实践,带你全面掌握Angular表单处理机制,提升前端开发效率与用户体验的终极指南
【8月更文挑战第31天】本文通过代码示例详细介绍了 Angular 中两种主要的表单处理方式:模板驱动表单和响应式表单。模板驱动表单适用于简单场景,可在 HTML 模板中直接定义表单控件并实现数据绑定和验证。响应式表单基于 RxJS,提供更灵活的表单管理和复杂的逻辑处理。通过具体示例展示了每种方式的最佳实践,帮助开发者简化表单处理,提高开发效率和用户体验。
23 0
|
2月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
21 0
下一篇
无影云桌面