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

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

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


前置准备

响应式

响应式可以被广义地定义为应用状态变化时自动更新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…



相关文章
|
19天前
|
编解码 前端开发 JavaScript
构建高效响应式Web界面:现代前端框架的比较
【4月更文挑战第9天】在移动设备和多样屏幕尺寸盛行的时代,构建能够适应不同视口的响应式Web界面变得至关重要。本文深入探讨了几种流行的前端框架——Bootstrap、Foundation和Tailwind CSS,分析它们在创建响应式设计中的优势与局限。通过对比这些框架的栅格系统、组件库和定制化能力,开发者可以更好地理解如何选择合适的工具来优化前端开发流程,并最终实现高性能、跨平台兼容的用户界面。
|
21天前
|
Web App开发 前端开发 JavaScript
前端应用实现 image lazy loading 的原理介绍
前端应用实现 image lazy loading 的原理介绍
29 0
|
1月前
|
Web App开发 前端开发 JavaScript
构建响应式Web界面:现代前端开发的最佳实践
【2月更文挑战第15天】 在多设备浏览时代,响应式Web设计成为前端开发者的必备技能。本文将深入探讨实现响应式界面的核心概念、技术栈以及工具,帮助读者掌握从布局到交互的全方位解决方案。通过灵活运用CSS框架、媒体查询及JavaScript,开发者可以创建出适应不同屏幕尺寸和分辨率的网站。文章不仅涵盖理论分析,还包含实际案例,确保读者能够将知识应用于实际项目中。
|
1月前
|
开发框架 Dart 前端开发
构建响应式Web界面:Flutter的跨界前端技术
【2月更文挑战第23天】随着移动互联网的飞速发展,响应式Web设计成为现代前端开发的重要趋势。在众多框架中,Google推出的Flutter以其高效的渲染性能、跨平台能力及丰富的组件生态,为前端开发者带来了新的选择。本文将深入探讨如何利用Flutter进行高效、美观的响应式界面构建,同时剖析其与传统前端技术的差异和优势。
|
1月前
|
Web App开发 编解码 前端开发
构建响应式Web界面:现代前端开发的实用指南
【2月更文挑战第22天】 随着移动互联网的兴起,响应式网页设计已成为前端开发者必须掌握的核心技能之一。本文将深入探讨如何通过灵活运用HTML5、CSS3和JavaScript等技术,构建出能够适应不同屏幕尺寸和设备的Web界面。文章不仅涉及理论概念,还包含具体实践案例,旨在帮助读者理解并应用响应式设计的核心原则,从而提升网站的用户体验和访问效率。
|
1月前
|
Web App开发 前端开发 JavaScript
前端技术探索与应用:构建高性能响应式网页
本文将介绍前端技术的最新发展和应用,重点探讨如何构建高性能响应式网页。通过深入解析前端框架、优化技巧以及调试工具等方面的内容,帮助读者提升网页的交互体验和加载速度,实现用户友好的界面设计。
|
1月前
|
缓存 前端开发 开发者
构建响应式Web界面:现代前端开发的实用指南
【2月更文挑战第19天】 在多设备浏览的时代,为不同屏幕尺寸和分辨率优化网站变得至关重要。本文将深入探讨响应式Web设计的核心概念、关键技术和实现策略,旨在引导前端开发者通过灵活布局、媒体查询和现代化工具,打造出能够适应各种终端的界面。我们将从基础理论出发,逐步过渡到实战技巧,最后讨论当前趋势与未来展望,以帮助读者构建出高效、美观且用户友好的响应式Web界面。
|
1月前
|
编解码 前端开发 JavaScript
构建高效响应式Web界面:现代前端开发的最佳实践
【2月更文挑战第18天】 在多设备浏览的时代,创建一个既高效又具有适应性的响应式Web界面已成为前端开发者的核心任务。本文将深入探讨实现流畅响应式体验的关键策略,包括灵活的布局设计、图像优化技巧以及性能考量。通过实例分析和技术深度剖析,我们将揭示如何利用HTML5、CSS3和JavaScript的最新特性,为不同尺寸的设备提供无缝的用户体验。文章不仅聚焦于代码实现,还将讨论开发流程中的协作与测试最佳实践,旨在为前端开发人员提供一个全面的指南,帮助他们在构建响应式Web界面时做出明智的技术选择。
52 4
|
1月前
|
前端开发 JavaScript 开发者
构建响应式Web界面:现代前端开发实践
【2月更文挑战第17天】 随着移动设备用户群体的激增,为各种屏幕尺寸和分辨率构建兼容且优雅的Web界面变得至关重要。本文将深入探讨响应式设计的核心概念,并通过实际案例分析展示如何利用HTML5、CSS3以及JavaScript框架创建流畅的用户体验。我们将着重讨论媒体查询、弹性布局和网格系统等技术的应用,并分享优化响应式网站性能的最佳实践。通过阅读本文,开发者将获得设计和实现适应不同设备的前端项目所需的知识和技能。
|
1月前
|
编解码 前端开发 JavaScript
构建响应式Web界面:现代前端开发的最佳实践
【2月更文挑战第16天】 在当今快速发展的数字时代,用户通过多种设备访问互联网已成为常态。因此,为不同屏幕尺寸和分辨率优化Web界面变得至关重要。本文将深入探讨如何采用最新的前端技术栈实现响应式Web设计,确保无论是在桌面、平板还是手机上,用户都能获得一致的高质量体验。我们将讨论使用CSS框架、灵活布局、媒体查询以及现代JavaScript框架等技术来创建流畅、可扩展且维护性高的响应式Web界面的策略和最佳实践。