发布于 2026-01-06 0 阅读
0

响应式 Svelte(探索 Svelte 的响应式特性)

响应式 Svelte(探索 Svelte 的响应式特性)

Svelte真是一种全新且革命性的 Web 开发方法!它在自动化应用程序的响应式方面做得非常出色。

本文旨在揭示Svelte 响应式的一些“幕后”细节。我们将从“观察者视角”出发——直接在我们的应用程序中可视化 Svelte 的响应式!这些概念背后确实隐藏着一些谜团。我们将深入探讨一些鲜为人知的细节(您可能从未考虑过)!希望您能从中获得更深刻的理解,并更好地了解如何使用这款强大的产品!

本文并非 Svelte 教程。大多数开发者应该都能理解这里讨论的概念。理想情况下,你应该了解 Svelte 的基础知识。虽然你不需要成为专家,但我们也不会时间解释 Svelte 的基本结构。

非常感谢Mark Volkmann对我的作品的审阅。我才刚刚开始使用 Svelte,所以 Mark 的见解对我来说弥足珍贵!您或许也想看看他的新书:《Svelte 和 Sapper 实战》

概览

太长不看

一般来说,我建议您通读全文。不过,如果您想“直奔主题”,我在文末总结了我的发现……欢迎直接跳到总结部分

视频演示

如果您对视觉内容感兴趣,我曾在2021 年秋季的Svelte Summit 大会做过以下演讲:

完整课程大纲及资源链接

苗条的反应

作为 Svelte 的新手,我对“响应式设计已经直接集成到我的应用中”这一点感到无比着迷这真是太酷了

在 React 等框架中,你的应用程序必须触发响应式(例如使用 `onReaction` setState(),而重点是:“我的组件何时重新渲染”

在 Svelte 中,响应式设计是自动实现的,而且比组件级别的响应式设计更加精细。在 Svelte 中,任何组件的单个代码片段都可以随时动态重新生成

Svelte 精心管理每个代码片段,根据其依赖状态的变化进行监控并按需重新执行。Svelte 的精妙之处就在于此:它通过其编译器理念自动完成这一切……将我们声明式的组件代码转换为可以直接增量操作 DOM 的 JavaScript!Svelte 不仅消除了样板代码,而且开箱即用,真正实现了响应式设计,无需使用臃肿的内存运行时框架。是不是很酷?

那么(你可能会问)什么 是代码片段这其实是我自己创造的术语。在本文中,“代码片段”指的是 Svelte 响应式管理并在适当的时候(例如,当依赖状态发生变化时)重新执行的任何 JavaScript 表达式。最终,代码片段用于赋予 HTML 标记动态性(即响应式特性)

代码片段可以在以下两个地方找到

  1. 代码片段
    代码片段位于组件的 JavaScript 代码中(在 ` <script>`<script>标签内),并带有 Svelte 的$:标签。这被称为响应式声明响应式语句

    $: {name, phone} = user;
    

    一般来说,代码片段通常比较轻量级,因为它们只会改变 JavaScript 状态变量。然而,这些状态改变的唯一目的就是为了在我们的 HTML 标记中被引用(无论是直接引用还是间接引用)

    在上面的示例中,每当user对象发生变化时(代码片段的依赖项)name ,代码片段都会重新执行,并重新赋值phone变量。

  2. html 代码片段
    html 代码片段位于组件的 html 标记中,用花括号括起来:{...}。这通常被称为插值

    <p>Hello {name}</p>
    <p>May we call you at {phone}</p>
    

    HTML 代码片段通常比较繁重,因为它们会导致 HTML DOM 发生变化但是,嘿……这正是我们存在的意义……也就是我们响应式设计的核心所在

    在上面的示例中,第一个代码片段会在name内容更改时重新生成其 html,第二个代码片段会在phone内容更改时重新生成其 html。

术语: 代码片段、代码片段和 HTML 代码片段

在本文中,“代码片段”一词指的是 Svelte 在其依赖状态发生变化时响应式地管理和调用的任何 JavaScript 表达式。

代码片段可以是以下两种类型之一:

  • 代码片段 (也称为响应式声明响应式语句
  • 或者html 代码片段 (通常称为插值

最终,代码片段为我们的 html 标记提供了动态性(即响应性)

反应触发器

那么,让我们更深入地探讨一下这个话题,你可能会问:Svelte 如何确定何时触发代码片段的重新执行?

简而言之,Svelte 会监控每个代码片段中引用的依赖状态,并在该状态发生变化时触发重新执行。

后续问题是:Svelte 如何确定状态引用已发生变化?

Svelte 文档中提到“赋值是‘响应式的’”以及“Svelte 的响应式由赋值触发”。他们的意思是,Svelte 通过赋值语义(识别各种形式的赋值)来触发响应式。

对于组件的局部状态来说,情况也是如此。Svelte 编译器会识别赋值(各种形式),并将被赋值的变量标记为已更改(即“过时”)

然而,我发现赋值目标是原始类型还是对象 (包括数组)之间存在很大的区别

基本类型

对于原始类型(字符串、数字、布尔值等),响应式仅在值发生变化时才会发生。换句话说,它也包含了 JavaScript 的身份语义(即priorState === nextState)。

因此,myNum = (x + y) / 2只有当其值实际发生变化时,才会被标记为“过期”。如果先前的值为 10,且计算结果也为 10,则不会发生任何反应。

这当然在意料之中,而且我认为这一点显而易见。然而,Svelte 的文档(据我所知)并没有提及这一点。我之所以强调这一点,是因为它与基于对象的响应式编程形成了鲜明的对比。

对象类型

大多数应用程序需要更复杂的状态,通常用对象(或数组)建模。

事实证明,在 Svelte 中,任何修改对象的操作都会将整个对象标记为“过时”。这包括局部组件对象、Svelte 对象存储、组件对象属性等等。即使你修改了一个对象,并通过将其赋值给自身来通知 Svelte 对象已更改,情况也是如此。

这意味着所跟踪的变化粒度范围更广。换句话说,即使只有一个属性发生了变化,整个对象也会被视为“过时”。

洞察力反应性是基于依赖状态的变化而产生的

当依赖状态发生变化时,代码片段的执行将被触发。

  • 原始类型仅在值发生变化时才会触发响应式(基于标识语义)。

  • 对象触发响应是基于整个对象,而不是单个内容。

陈旧总结

下表列出了 Svelte 认为哪些内容“过时”:

Given:
   let prim  = 1945; // a primitive
   let myObj = {     // an object
     foo: 1945,
     bar: 1945,
   };

Operation                      Marks this as "stale"
=============================  ======================================
prim++                         prim
prim = 1945                    prim (ONLY if prior value is NOT 1945)

myObj.foo++                    myObj (all content of myObj)
myObj = {...myObj, foo:1945}   myObj (all content of myObj)
myObj.foo = 1945               myObj (all content of myObj)
myObj = myObj                  myObj (all content of myObj)
incrementFooIndirectly(myObj)  NOTHING
Enter fullscreen mode Exit fullscreen mode

您可以在我的Reactive Triggers REPL中看到演示。它可视化了各种操作(如上所列)的反射计数(黄色高亮显示)。为了完全理解此 REPL 的工作原理,您需要了解 a是什么(这是您工具箱中的新工具)。这在“高级诊断”部分有详细讨论。阅读下一节后,您可以再回到此 REPL。ReflectiveCounter

探索应用响应式

我生性好奇,所以很想看看我的应用程序的响应速度。毕竟,我可是密苏里州人—— “给我看看”之州

你可能会说:“当然,你可以通过生产应用程序可视化的状态来看到它的响应结果”

但是,不……我说的不是这个。我想确切地确定 Svelte 何时触发我的代码片段的执行!换句话说,我想亲眼看看 Svelte 的响应式特性是如何运作的

这样做将会:

  • 请帮助我更好地理解Svelte的理念

  • 请您深入讲解一下 Svelte 的各种启发式方法(依赖监控、响应式触发器、DOM 更新等)。

  • 让我更好地理解“我周围(自动地)发生的所有反应”。

  • 我们或许会发现一些我们之前没有考虑到的细节

当然,这仅限于“诊断探针”这一类别,而不是我们生产应用程序的一部分。

乍一看,这似乎是一项“艰巨的任务”,因为Svelte掌控着这一切(而不是我们)。而且Svelte Devtools也没有提供任何相关信息(它的重点在于查看特定时间点的状态)

诊断日志探测器

事实证明,我们可以使用一个常见的“开发者技巧”,在每个代码片段前加上console.log() 前缀,从而实现逻辑或运算。

请考虑以下情况:

原来的:

<p>Hello {name}</p>
<p>May we call you at {phone}</p>
Enter fullscreen mode Exit fullscreen mode

使用日志探测器:

<p>Hello {console.log('Name section fired) || name}</p>
<p>May we call you at {console.log('Phone section fired) || phone}</p>
Enter fullscreen mode Exit fullscreen mode

现在,我们在每个生产表达式前都加上了一个console.log()逻辑或运算的前缀。因为该console.log()前缀不返回任何值(即为undefined假值),所以后续表达式将无条件执行(从而产生原始的 HTML 输出)。

换句话说,这将生成相同的 html (与我们原始的生产代码相同),但会添加诊断日志,这些日志仅在执行代码片段时发出

例如,假设我们的phone状态发生变化……我们将在日志中看到以下内容:

日志:

Phone section fired
Enter fullscreen mode Exit fullscreen mode

您可以在“日志探测”讨论区看到此功能的实时演示

每次探测时使用不同的文本非常重要这样才能将每个日志条目与其对应的片段关联起来。

添加这些诊断探针后,我们的日志将明确显示 Svelte 何时重新执行每个代码片段……真是太棒了!

外卖通过逻辑或运算前缀表达式监控 Svelte 代码片段调用

你可以通过在代码片段前加上逻辑或监视器来检测 Svelte 何时执行该代码片段

这可以是一个简单的日志探针,也可以是更高级的反射计数器。

高级诊断

对于大多数应用程序而言,这些简单的诊断日志探测将足以提供有关应用程序反射性的深入见解。

但是,根据您需要的探测数量,将这些日志与各个部分关联起来可能会变得很繁琐。

在这些情况下,我们可以用一个简单的监视器来代替日志,该监视器会显示每个部分的反射计数,并直接显示在我们的页面上

以下是该实用程序:

createReflectiveCounters.js

export default function createReflectiveCounter(logMsg) {
  // our base writable store
  // ... -1 accounts for our initial monitor reflection (bumping it to 0)
  const {subscribe, set, update} = writable(-1);

  // expose our newly created custom store
  return {
    subscribe,
    monitor(...monitorDependents) {
      update((count) => count + 1);  // increment our count
      logMsg && console.log(logMsg); // optionally log (when msg supplied)
      return ''; // prevent rendering `undefined` on page (when used in isolation)
                 // ... still `falsy` when logically-ORed
    },
    reset: () => set(0)
  };
}
Enter fullscreen mode Exit fullscreen mode

这将创建一个ReflectiveCounter(自定义存储),适用于监控 Svelte 反射计数。

在其基本形式中,aReflectiveCounter只是一个简单的计数器,但是它的 API 被设计为用作反射监视器。

monitor()方法应作为“Svelte 调用”代码片段的前缀(通过逻辑或表达式或JS 逗号运算符)。它会统计 Svelte 执行此代码片段的次数。

例子<i>{fooSectionReflexiveCount.monitor() || $foo}</i>

这些数据可以直接汇总在您的页面上!

例子<mark>{$fooSectionReflexiveCount}:</mark>

monitor()方法还可以选择性地接受一组monitorDependent参数。当需要监控的依赖项尚未包含在生产代码片段中时,可以使用此功能。实际上,该工具并不使用这些参数,而只是通知 Svelte 监控这些依赖项,以此作为重新调用代码片段的依据。以下示例监控 Svelte store 的更改次数:

例子$: fooStateChangeCount.monitor($foo);

您还可以选择在每次执行 monitor() 函数时,通过logMsg向创建者提供一个参数,在控制台中记录一条消息:

例子const fooSectionReflexiveCount = createReflectiveCounter('Foo section fired');

reset()方法可用于重置给定的计数。

用法:

有两种截然不同的ReflectiveCounter使用方法:

  1. 监控 HTML 自反函数计数(在 HTML 代码片段中):

    <script>
      const fooReflexiveCount = createReflectiveCounter('foo section fired');
    </script>
    
    <!-- diagnostic reporter -->
    <mark>{$fooReflexiveCount}:</mark>
    
    <!-- monitor this section -->
    <i>{fooReflexiveCount.monitor() || $foo}</i>
    
    <!-- reset counts -->
    <button on:click={fooReflexiveCount.reset}>Reset</button>
    
  2. 监控状态更改次数(代码片段):

    <script>
      const fooChangeCount = createReflectiveCounter();
      $: fooChangeCount.monitor($foo);
    </script>
    
    <!-- reporter/resetter -->
    <i>$foo state change counts: {$fooChangeCount}</i>
    <button on:click={fooChangeCount.reset}>Reset</button>
    

您可以在“高级探针”讨论区看到实时演示ReflectiveCounters

洞察力诊断探针是临时性的。

诊断探针应视为临时性的,分析完成后应将其移除(或注释掉) 。

有些技术可以在运行时禁用它们(开销极小),但这项练习留给读者自行完成。

演示应用程序

在开始任何分析之前,我们需要一些代码来进行测试。代码应该简单明了,重点突出,这样我们才能专注于它的响应式设计。

我创建了一个交互式演示(Svelte REPL),我们可以使用它。

演示 REPL

问候用户操作

这个演示的基本思路是,您可以维护已登录用户的特征 (上半部分EditUser.svelte,并将其显示出来 (下半部分GreetUser.svelte ……非常简单:-) 您只需更改文本并单击按钮,即可更新用户的一个或多个属性Apply Change现在就来体验一下这个交互式演示吧

该演示程序分为多个模块。这里我就不详细介绍了……它们在App.svelte演示程序 REPL的)文档中有概述。

侧边栏:通常情况下,EditUser/GreetUser组件是互斥的(即在不同的时间显示) ……我只是将它们合并在一起,以便我们更好地看到两者之间的“自反关系”

在我们的讨论中,我们将专注于一个模块:GreetUser组件。

GreetUser.svelte (参见GU1_original.svelte演示REPL

<script>
 import user from './user.js';
</script>

<hr/>
<p><b>Greet User <mark><i>(original)</i></mark></b></p>

<p>Hello {$user.name}!</p>
<p>
  May we call you at:
  <i class:long-distance={$user.phone.startsWith('1-')}>
    {$user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>
Enter fullscreen mode Exit fullscreen mode

该组件仅用于向已登录用户(基于对象的 Svelte 存储)致意,并可视化用户的个人属性。长途电话号码将被高亮显示(当它们以“1-”开头时)

还有什么比这更简单的呢?这应该能为我们的讨论奠定良好的基础 :-)

检查应用程序响应性

GreetUser我们使用诊断探针 (在“探索应用程序响应性”中讨论过)来增强该组件,看看它的表现如何。

日志探测器

以下是应用了诊断日志探测功能GreetUser的组件

GreetUser.svelte (参见GU2_logDiag.svelte演示REPL

<script>
 import user from './user.js';

 // diagnostic probes monitoring reflection
 const probe1 = () => console.log('Name  section fired');
 const probe2 = () => console.log('Phone class   fired');
 const probe3 = () => console.log('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with reflexive diagnostic logs)</i></mark></b></p>

<p>Hello {probe1() || $user.name}!</p>
<p>
  May we call you at:
  <i class:long-distance={probe2() || $user.phone.startsWith('1-')}>
    {probe3() || $user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>
Enter fullscreen mode Exit fullscreen mode

您可以通过选择以下方式运行此版本的演示 REPL :带有反射性诊断日志

日志显示代码片段已执行!

非常好……通过分析日志,我们可以准确地确定各个 html 代码片段何时重新执行!

高级探针

我们不妨也应用一下高级诊断功能 (纯粹为了好玩),看看结果如何:

GreetUser.svelte (参见GU3_advancedDiag.svelte演示REPL

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with advanced on-screen diagnostics)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || $user.name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class:long-distance={probe2.monitor() || $user.phone.startsWith('1-')}>
    {probe3.monitor() || $user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>
Enter fullscreen mode Exit fullscreen mode

您可以通过选择以下方式运行此版本的演示 REPL :具备先进的屏幕诊断功能

屏幕探测

太棒了……我们组件的响应式特性现在可以直接在页面上看到了!

重新渲染分析

引入诊断探针后,似乎出现了一些意想不到的结果。我们发现,即使 HTML 代码片段的状态没有改变,它们也会重新执行(糟糕!

没有变化,但仍然重新开火

您可以通过更改单个属性(例如名称)来观察这一点,并注意到我们所有三个 HTML 代码片段都会重新执行!即使您单击Apply Change按钮而不更改任何属性,仍然……我们所有三个 HTML 代码片段都会重新执行!附注:我知道我可以优化user商店以避免最后这种情况,但就本次讨论而言,它更能突出我们想要表达的观点。

到底发生了什么?

取消对象引用

如果你还记得我们之前对响应式触发器的讨论,这实际上是一个对象引用在依赖粒度方面过于宽泛的例子。

<p>Hello {$user.name}!</p>
Enter fullscreen mode Exit fullscreen mode

由于 Svelte 已$user将该对象标记为已过时,因此任何引用该对象的 html 代码片段都会重新执行,无论被取消引用的对象是否.name已更改!

乍一看,这似乎有悖常理。Svelte为什么要这样做? 这真的会导致 DOM 中出现冗余且不必要的重新渲染吗?……剧透一下:实际上并没有发生冗余的重新渲染,但我们将在下一节讨论这个问题!

仔细想想,为了让 Svelte 监视对象的解引用内容,它必须预先执行代码片段中的子表达式,并监视结果值。

在我们这个简单的例子中,这在技术上或许可行,但一般来说,出于各种原因,这是一个不好的主意。

主要原因是,为了实现这一点,这些子表达式必须始终执行,但这违背了 Svelte 的基本原则(即其响应式触发机制)……也就是说:这段代码片段是否应该重新执行?如果 Svelte 必须预先执行代码片段的某些部分来做出判断,则可能会产生负面影响!例如,子表达式可能调用了一个会应用不必要的变更的方法等等。

侧边栏:我在这里的解释只是基于直觉的“最佳猜测”。如果我收到来自 Svelte 维护者的“内部人士”的评论,我会对解释进行必要的更正并删除此侧边栏 :-) 无论解释如何,这实际上就是 Svelte 的工作原理!

Svelte 的重新渲染优化

那这意味着什么呢?

真正的问题在于:这是否真的会在我们的 DOM 中产生冗余且不必要的重新渲染?记住:DOM 更新很耗费资源!这是真的吗?还是背后还有更多不为人知的运作机制

我突然意识到,即使 Svelte 决定重新执行我的 html 代码片段,也不一定意味着它导致了 DOM 更新。

Svelte 是否通过确保 HTML 代码片段的实际效果得到改变来进一步优化这个过程呢?仔细想想,Svelte 这样做确实很有道理。

  • 在这个特殊情况下,由于依赖粒度过宽,导致不必要的 html 代码片段被重新执行……即对象与其单个内容之间的依赖关系(我们在响应式触发器部分讨论过这一点)

  • 然而,在某些情况下,即使依赖项发生合理变化,我们的 HTML 代码片段也可能返回相同的结果。想想看:这是应用程序代码(不受 Svelte 控制)。假设我们的应用程序需求会将一组枚举值归为一类,从而导致多个值产生相同的结果。

事实证明,Svelte 的确通过确保内容确实已更改来优化其 DOM 更新。所以不会出现重复渲染的情况

Svelte 又一次帮了我们大忙

我最初是通过进入我的一个诊断探针的调试会话来确定这一点的。

  • 通过进一步深入(进入 Svelte 世界),我发现自己置身于一些相当晦涩难懂的代码中,其中一个相当复杂的条件语句正在执行一个辅助函数,该函数实际上执行了底层 DOM 更新。

  • 由于对这个复杂的条件语句不太确定,我决定只在这个辅助函数上设置一个断点。

  • 这使我能够与我的应用程序进行交互,并确定:果然如此……只有当 html 代码片段的结果实际发生变化时,DOM 片段才会更新(即重新渲染

这太酷了

Svelte 编译器输出

好吧,我开始有点得意忘形了。我开始琢磨:Svelte 在判断“内容变更”方面效率如何?我反复琢磨着我在调试过程中遇到的这段晦涩难懂的代码

这段代码会不会是 Svelte 编译器的输出结果?……你知道的,就是那个一直不敢点开的“JS 输出” REPL 标签页?

果然我的预感是对的

有了这份新获得的自信,我敢尝试解读这段神秘的代码吗?……嗯,值得一试

注意本部分完全可选。我们已经讨论了您需要了解的关于此主题的关键要点。因此,本部分仅供参考。仅限额外学分 (虽然对真正的极客来说非常有趣)!您可以直接跳到下一节

供您参考:我不会在文章中堆砌大量晦涩难懂的代码……您可以查看Demo REPL中的“JS 输出”选项卡来了解详情。

好了,开始吧……

神秘名称:

首先你会注意到,这段代码中的变量名并不十分直观……大多是带单个字母前缀的数字变量。但是,嘿:这是机器生成的代码!我们可不想冗长、直观的变量名让程序包变得臃肿!实际上,一旦你掌握了诀窍,就会发现这些变量名中存在一些有用的模式……继续阅读

DOM 片段:

这段代码最重要的意义在于,Svelte 成功地将我们的 html 分解成可以在 DOM 树的最底层重新构建的片段。

这是关键所在!一旦这一点实现,逐步推进变革就变得相当容易了!

我的直觉告诉我,这可能是编译器中最复杂的部分。

  • 对于静态 html (不变的),它甚至采用了一种简单的方法innerHTML

    例如,这个:

    <p><b>Greet User <mark><i>(original)</i></mark></b></p>
    

    生成了以下内容:

    p0 = element("p");
    p0.innerHTML = `<b>Greet User <mark><i>(original)</i></mark></b>`;
    

    这个我可以应付:-)

  • 对于动态 html 内容(由 html 片段/插值驱动),它会将 html 进一步分解为所需的单个 DOM 元素(可以逐步更新)

    例如,这个:

    <p>Hello {$user.name}!</p>
    

    生成了以下内容:

    // from the c() method ...
    p1 = element("p");
    t4 = text("Hello ");
    t5 = text(t5_value);
    t6 = text("!");
    
    // from the m() method ...
    insert(target, p1, anchor);
    append(p1, t4);
    append(p1, t5);
    append(p1, t6);
    

    请注意,对于动态内容,Svelte 会跟踪两件事:

    • 文本t5DOM 元素
    • 以及t5_value文本内容……这一定是我们的 HTML 代码片段的输出结果。

命名规则:

你开始对一些命名规则有所了解了吗?

  • p是段落
  • t是用于文本节点
  • ETC。

组件方法:

该组件包含多个方法。通过分析它们的实现,我认为可以推断出以下特点:

// appears to be initializing our internal state
c() {
  ... snip snip
}

// appears to be the initial build-up of our DOM
m(target, anchor) {
  ... snip snip
}

// appears to be the incremental update of our DOM fragments
// ... THIS IS THE KEY FOCUS OF OUR REACTIVITY (analyzed below)
p(ctx, [dirty]) {
  ... snip snip
}

// appears to be removing our DOM
d(detaching) {
  ... snip snip
}
Enter fullscreen mode Exit fullscreen mode

关于命名规则的更多信息:

嘿……一旦你意识到我们正在玩芝麻街字母游戏,这些名字就开始变得有意义了

  • c()是为constructor()
  • m()是为mount()
  • p()partiallyPutinProgressivePermutations()……我显然对此一无所知:-(后来马克告诉我它代表update() (用第二个字母表示),并提供了一个谭立豪的资料……我需要的时候它在哪儿呢? :-)
  • d()是为destroy()
  • 还有一些方法无法正常运行(例如i: noop,等等),所以显然我们这个非常简单的组件还没有涵盖更高级的情况 :-)

增量更新:

我们主要关注的是这个p()方法。增量 DOM 更新就是在这里进行的。在调试过程中,我正是在这里发现 DOM 更新已经过优化。

  • 请注意,它包含 3 个代码段(每个代码段都以条件符号 - 开头if)。

  • 哇哦……我们的组件定义中还有 3 个 HTML 代码片段。(真是巧合)

  • 我们来看其中一个例子(我稍微修改了一下JS代码,并添加了//注释)

    HTML代码片段

    <p>Hello {$user.name}!</p>
    

    编译输出

    p(ctx, [dirty]) {
      // one of 3 sections ...
      if (dirty & /*$user*/ 1 &&                                  // conditional Part I
          t5_value !== (t5_value = /*$user*/ ctx[0].name + "")) { // conditional Part II
        set_data(t5, t5_value);                                   // the payload - update the DOM!
      }
      ... snip snip
    },
    

以下是我的分析:

  • ctx[]数组包含我们所有的依赖项。ctx[0]恰好是我们的$user对象(多亏了编译器保留的注释提示)

  • dirty包含所有因变量“陈旧程度”的按位累加值(每个因变量一位)

  • 条件语句的第一部分是提取$user因变量的脏标记(使用按位与运算符 - &。这可以判断我们的$user变量是否已过期。如果过期,我们将继续执行第二部分(通过logical-AND运算符 - &&)。

  • 条件语句的第二部分实际上做了两件事:它将t5_valueHTML 代码片段中的最新内容(在将其转换为字符串之后+ ""赋值给变量,并且比较前一个/后一个代码片段的输出(使用身份语义!==)。只有当前一个/后一个代码片段发生变化时,它才会执行条件语句的有效负载(即更新 DOM。归根结底,这个条件语句就是一个非常简单的原始字符串比较!

  • set_data()函数是一个 Svelte 辅助工具,用于实际更新 DOM!你可以在 GitHub 上找到这些工具(链接在此),或者直接从你安装的 Svelte 项目中打开它们node_modules/svelte/internal/index.js。这个工具的作用仅仅是将提供的数据设置到一个 DOM 文本元素中:

  function set_data(text, data) {
    data = '' + data;
    if (text.data !== data)
      text.data = data;
  }
Enter fullscreen mode Exit fullscreen mode

Svelte的自反性非常高效

真有趣!这是一次非常有趣的练习我们学到了什么

  • 不要害怕打开REPL 的“JS 输出”选项卡!

  • Big Bird 很适合进行 Svelte 代码审查!

  • 最重要的是以下见解

洞察力Svelte 的自反性非常高效!

该组件的 DOM 表示经过高度优化,被分解成可以在 DOM 树的最底层重新构建的片段。

动态内容是通过执行 html 代码片段获取的。

反射是由依赖状态的变化触发的。

只有当内容实际发生变化时才会进行 DOM 更新(使用非常轻量级的字符串比较)

还能要求什么呢

Rich Harris核心贡献者们致敬,他们真是太聪明、太细致了!

应用响应式调整

我们了解到反射 (Svelte 执行 html 片段)重新渲染 (应用 DOM 更新)之间存在微妙的区别。

即使 Svelte通过依赖监控机制运行了一段 HTML 代码片段,也不意味着 DOM 一定会更新(尽管通常情况下会更新) ……因为这段代码片段可能返回相同的结果。Svelte 优化了这一过程,以确保 DOM 更新仅在实际发生变化时才会发生。

因此,我们的反射次数可能略大于重新渲染次数。原因有二:

  1. 依赖粒度过宽(例如,对象和基本类型之间的区别)。这主要是 Svelte 的问题。举个例子,Svelte 因为对象发生了变化而调用了我们的代码片段,但对象本身的子内容(我们的代码片段所使用的内容)实际上并没有改变。我们将在“更细粒度的依赖管理”一节中进一步讨论这个问题。

  2. 该 HTML 代码片段可能对多个依赖值返回相同的结果。这取决于我们的应用程序。例如,假设我们的应用程序需求是将一组枚举值归为一类,那么多个值都可能生成相同的结果。我们将在“预解析变体”部分进一步讨论这个问题。

无论这些问题是由谁造成的,我们都可以通过一些针对特定应用的技术来缩小差距 (甚至消除差距)。那么,我们该如何应对呢?毕竟,Svelte 才是真正控制 HTML 代码片段执行的一方。我们该如何改变它呢?

我们接下来要做的,其基本重点是将一部分反思性操作从 HTML 代码片段转移到代码片段。记住,我们提到过代码片段通常开销更小(因为它们仅仅会导致 JavaScript 状态变量的改变)

你为什么要这样做?这真的能带来显著的优化吗?不妨考虑一下:

  1. 如果这种差异计数很大(我们多次不必要地重复执行相同的 html 代码片段,却得到相同的输出) ,那该怎么办?
  2. 如果执行这段 HTML 代码的开销非常高呢?
  3. 如果我们需要在 HTML 代码中的多个地方使用相同的 HTML 代码片段该怎么办?

请记住,我们的简单玩具应用程序中没有这些条件……但为了举例说明,让我们假设我们有这些条件!

关于优化问题……说实话,我们接下来要讨论的技术很可能不会对您的应用性能产生显著影响。在某些情况下,我们只是将 Svelte 已经实现的优化转移到了应用层面。话虽如此,最佳的优化机会在于上述第 3 点。

那么,为什么要进行这项练习呢?原因很简单:为了更好地了解Svelte反应性的更精细特征掌握这些知识可以让你比资深开发人员更有优势……了解细微调整的影响……赚取高薪……我们只能寄希望于此

极限优化代码片段比 HTML 片段更倾向于响应式设计

代码片段的开销通常很小,因为它们只是改变 JavaScript 状态变量。

虽然变量的变化确实会导致 html DOM 的变化(通过刺激依赖的html 代码片段) ,但当依赖变量保持不变时(在代码片段中),这种变化将被抵消

更细粒度的依赖关系管理

本节讨论 Svelte 过宽的依赖粒度问题,特别是与原始类型对象类型相关的问题。

我们的GreetUser组件目前正在$user其 HTML 代码中解引用对象。这导致 Svelte 在被解引用的属性未发生变化的情况下仍然执行我们的 HTML 代码片段。

我们可以通过简单地将引用的状态规范化为原始类型来改变这一点。

外卖通过使用基本类型来微调 Svelte 的依赖管理

当 html 代码片段取消引用可能发生变化的对象(例如user.name)时,这可能会导致误报。

Svelte 会根据顶级对象是否已更改(例如user)来重新执行代码片段,而不是根据解引用的结果来执行!

你可以通过依赖从对象规范化的原始类型(例如通过代码片段)来改变这一点,从而使 html 代码片段能够更精细地控制触发其执行的条件。

依赖于原始类型的 html 代码片段只有在其依赖值发生变化时才会执行(基于标识语义)

这实际上是……的应用极限优化: 代码片段比 HTML 片段更倾向于响应式设计

以下是GreetUser应用更改后的组件:

GreetUser.svelte (参见GU4_primNorm.svelte演示REPL

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // FOCUS: with primitive normalization
 // normalize our referenced state with primitive types
 // ... html-snippets will only fire when values actually change
 // ... using JS identity semantics
 $: ({name, phone} = $user);

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with primitive normalization)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class:long-distance={probe2.monitor() || phone.startsWith('1-')}>
    {probe3.monitor() || phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>
Enter fullscreen mode Exit fullscreen mode

您可以通过选择以下方式运行此版本的演示 REPL :原始归一化

无需更改,无需重新开火……已修复

太棒了:请注意,反射计数 (Svelte 对 html 代码片段的执行)现在能够正确反映相应状态的实际变化!

在这个例子中,“原始归一化”是在组件代码片段中实现的:

$: ({name, phone} = $user);
Enter fullscreen mode Exit fullscreen mode

$user对象发生变化时,这段规范化代码将会重新执行。但是,由于我们的 HTML 代码片段使用了name`/`phone基本元素,因此只有那些依赖于真正发生变化的属性的代码片段才会重新执行!……真是太棒了!

这种“原始规范化”可以通过多种方式实现。在我们的示例中,它是在组件代码中执行的。另一种实现方法是提升派生存储,使其只提取单个值。例如:

user.js (已修改)

import {writable, derived} from 'svelte/store';

export const user = writable({
  name:  '',
  phone: '',
});

export const name  = derived(user, (u) => u.name);
export const phone = derived(user, (u) => u.phone);
Enter fullscreen mode Exit fullscreen mode

预解析变体

本节讨论 HTML 代码片段对多个依赖值生成相同结果的情况。这种情况通常发生在代码片段包含条件逻辑时。

在我们的示例中,长途电话号码(以“1-”开头)将被高亮显示。这是通过 HTML 代码片段中的条件逻辑实现的:

<i class:long-distance={phone.startsWith('1-')}>
  ... snip snip
</i>
Enter fullscreen mode Exit fullscreen mode

这里的问题在于,phone无论 CSS 类是否发生变化,Svelte 都会根据依赖项是否发生变化来重新执行 html 代码片段。

您可以通过更改电话号码的后半部分(保持前缀不变)在演示中看到这一点:

远距离射击次数未改变,但仍会再次射击。

如您所见,这导致了反射计数 (Svelte 执行 html 代码片段)的增加

解决方案:

如果我们把这个逻辑条件放到代码片段中,生成的 HTML 代码片段执行次数就会减少!

外卖通过将 HTML 代码片段的各种变体移至代码片段,来微调条件逻辑

通过允许在代码片段中解析条件表达式,生成的 html 代码片段触发的频率会降低。

这实际上是……的应用极限优化: 代码片段比 HTML 片段更倾向于响应式设计

以下是GreetUser应用更改后的组件:

GreetUser.svelte (参见GU5_variations.svelte演示REPL

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // normalize our referenced state with primitive types
 // ... html-snippets will only fire when values actually change
 // ... using JS identity semantics
 $: ({name, phone} = $user);

 // FOCUS: with variations in code
 // by allowing conditional expressions to be resolved in a code-snippet,
 // the resulting html-snippet will fire less often.
 $: classes = phone.startsWith('1-') ? 'long-distance' : '';

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with variations in code)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class="{probe2.monitor() || classes}">
    {probe3.monitor() || phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>
Enter fullscreen mode Exit fullscreen mode

您可以通过选择以下方式运行此版本的演示 REPL :代码有多种变体。

远距离射击不变,无需再次射击……已修复

太好了:请注意,反射计数 (Svelte 对 html 代码片段的执行)现在可以正确反映 CSS 类是否真的发生了变化!

在这个版本中,可变性现在是在组件代码片段中实现的:

$: classes = phone.startsWith('1-') ? 'long-distance' : '';
Enter fullscreen mode Exit fullscreen mode

因此,只有当classes变量实际发生变化时,html 代码片段才会执行。

优化注意事项

以下是一些关于优化的“额外”考虑因素:

洞察力优化仅在活性成分发生反应时才具有意义。

组件的初始渲染(即挂载时)将无条件执行其所有 html 代码片段(无论依赖关系或条件如何)

如果之后没有反应,那么就没有必要关注这些优化技术了。

但请记住,响应式特性有时难以预测。此外,代码的后续修改也可能会引入响应式特性。所以务必谨慎!始终遵循这些技巧总是有益无害的。

洞察力优化是首选,但并非必须。

即使您忽略这些优化技巧,您的应用仍然可以正常运行,只是性能不会达到最佳状态。

额外加分练习

对于那些想要获得额外加分的人,让我提出一个改进方案ReflectiveCounter (在高级诊断中讨论过)

目前,它ReflectiveCounter为我们提供了一个自反计数 (html 代码片段执行计数)

你能想到一种方法,既能提供反射计数,又能提供重新渲染计数 (即 DOM 更新计数)吗?

这个小练习应该能区分真正的极客和那些想成为极客的人

我不会直接告诉你答案,但这里有一个非常重要的提示……调用方式会改变:

从:

<i>{fooProbe.monitor() || $foo}</i>
Enter fullscreen mode Exit fullscreen mode

到:

<i>{fooProbe.monitor( () => $foo )}</i>
Enter fullscreen mode Exit fullscreen mode

你准备好接受挑战了吗?友情提示:这里藏着一个彩蛋(藏在某个地方),它会揭示答案!如果你找不到,请在下方评论区留言告诉我

这是谁?

简单介绍一下我的背景(与软件工程相关) ……

我在软件行业工作了40多年,可能是在座各位里年纪最大的了 (2015年退休)。我喜欢说自己是来自不同时代的“当代”开发者,但哎,要跟上时代真的越来越难了!举个例子:我现在才开始学Svelte,它都发布多久了

毋庸置疑,我在互联网普及之前的 25 年(70 年代中期)就开始接触编程了。

我记得伟大的计算机先驱格蕾丝·霍珀曾来做客座讲师,当时她已经73岁高龄,分享了当时计算机领域的一些见解(其实和今天并没有太大区别)。她使用了非常棒的视觉辅助工具……比如分发纳秒等等。霍珀上将在那时(70年代中期)就已经是位资深人士了,所以我想我也不应该太在意:-)顺便提一句:她还创造了“bug” (漏洞)这个词

当我最终开始从事网页开发时(90 年代中期),我全身心投入到Netscape 的这项名为 JavaScript 的新技术中!即使在那时,我们也利用这项新技术在页面级别实现了响应式设计。

多年来,我用纯 JavaScript (也就是说当时没有使用任何框架)编写了许多大型单页应用程序(SPA )(甚至早于 SPA 这个术语的出现) !相信我,实现大规模应用程序的响应式功能是一项艰巨的任务,需要良好的底层架构,最终也需要编写大量的代码!

我其实直接跳过了 jQuery 的热潮,直接进入了新的声明式框架领域……先是 Angular,然后是 React。这种声明式方法总是让我惊叹不已……用如此少的代码就能实现如此多的功能:-)

Svelte 只是将这种发展提升到了一个新的水平!它提供了声明式方法的所有优点,而无需臃肿的内存运行时框架!

自2015年退休以来,我一直为开源项目做贡献。我最近的作品是一个名为feature-u的产品:一个用于简化特性驱动开发的React实用工具。

我是一个崭新的苗条身材者

我的第一个 Svelte 项目(现在发布还为时尚早)是我最引以为豪的项目(90 年代初)的重制版。那是一个“工程分析”工具,用 C++ 在 Unix/X-Windows 系统下编写。它包含:

  • 原理图捕获:对主原理图进行多种功能分解
  • 可执行控制律:通过可执行的图形流程图
  • 仿真:由控制律驱动(动画演示一个或多个原理图和控制律)
  • 符号调试器:也由控制律驱动
  • 自动生成嵌入式系统代码(源自可执行控制律)
  • 毋庸置疑,该系统的反应能力非常强

你可以在以下平台找到我LinkedInTwitterGitHub

概括

嗯,这次探索比我最初设想的要深入得多 :-) 我们聊了很多!希望你们喜欢这次小小的旅程,也从中有所收获!

非常感谢Rich Harris和所有核心贡献者,是他们让 Svelte 成为如此出色的产品!我迫不及待地想看看下一个版本会带来什么!

祝您使用愉快!

</Kevin>

PS:为了方便您阅读,我已将我的发现总结如下。每一点都包含一个简短的概要,并链接到更详细的讨论。

  1. 术语: 代码片段、代码片段和 HTML 代码片段

    在本文中,“代码片段”一词指的是 Svelte 在其依赖状态发生变化时响应式地管理和调用的任何 JavaScript 表达式。

    代码片段可以是以下两种类型之一:

    • 代码片段 (也称为响应式声明响应式语句
    • 或者html 代码片段 (通常称为插值

    最终,代码片段为我们的 html 标记提供了动态性(即响应性)

  2. 洞察力反应性是基于依赖状态的变化而产生的

    当依赖状态发生变化时,代码片段的执行将被触发。

    • 原始类型仅在值发生变化时才会触发响应式(基于标识语义)。

    • 对象触发响应是基于整个对象,而不是单个内容。

  3. 外卖通过逻辑或运算前缀表达式监控 Svelte 代码片段调用

    你可以通过在代码片段前加上逻辑或监视器来检测 Svelte 何时执行该代码片段

    这可以是一个简单的日志探针,也可以是更高级的反射计数器。

  4. 洞察力诊断探针是临时性的。

    诊断探针应视为临时性的,分析完成后应将其移除(或注释掉) 。

    有些技术可以在运行时禁用它们(开销极小),但这项练习留给读者自行完成。

  5. 洞察力Svelte 的自反性非常高效!

    该组件的 DOM 表示经过高度优化,被分解成可以在 DOM 树的最底层重新构建的片段。

    动态内容是通过执行 html 代码片段获取的。

    反射是由依赖状态的变化触发的。

    只有当内容实际发生变化时才会进行 DOM 更新(使用非常轻量级的字符串比较)

  6. 极限优化代码片段比 HTML 片段更倾向于响应式设计

    代码片段的开销通常很小,因为它们只是改变 JavaScript 状态变量。

    虽然变量的变化确实会导致 html DOM 的变化(通过刺激依赖的html 代码片段) ,但当依赖变量保持不变时(在代码片段中),这种变化将被抵消

  7. 外卖通过使用基本类型来微调 Svelte 的依赖管理

    当 html 代码片段取消引用可能发生变化的对象(例如user.name)时,这可能会导致误报。

    Svelte 会根据顶级对象是否已更改(例如user)来重新执行代码片段,而不是根据解引用的结果来执行!

    你可以通过依赖从对象规范化的原始类型(例如通过代码片段)来改变这一点,从而使 html 代码片段能够更精细地控制触发其执行的条件。

    依赖于原始类型的 html 代码片段只有在其依赖值发生变化时才会执行(基于标识语义)

    这实际上是……的应用极限优化: 代码片段比 HTML 片段更倾向于响应式设计

  8. 外卖通过将 HTML 代码片段的各种变体移至代码片段,来微调条件逻辑

    通过允许在代码片段中解析条件表达式,生成的 html 代码片段触发的频率会降低。

    这实际上是……的应用极限优化: 代码片段比 HTML 片段更倾向于响应式设计

  9. 洞察力优化仅在活性成分发生反应时才具有意义。

    组件的初始渲染(即挂载时)将无条件执行其所有 html 代码片段(无论依赖关系或条件如何)

    如果之后没有反应,那么就没有必要关注这些优化技术了。

    但请记住,响应式特性有时难以预测。此外,代码的后续修改也可能会引入响应式特性。所以务必谨慎!始终遵循这些技巧总是有益无害的。

  10. 洞察力优化是首选,但并非必须。

    即使您忽略这些优化技巧,您的应用仍然可以正常运行,只是性能不会达到最佳状态。

文章来源:https://dev.to/kevinast/responsive-svelte-exploring-svelte-s-reactivity-5cen