您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

从Lisp到Vue、React再到 Qwit:响应式编程的发展历程

时间:2023-04-06 12:07:06  来源:  作者:大迁世界

本文介绍了响应式编程的历史和发展,响应式编程是一种编程范式,它强调了数据流和变化的传递。文章从早期的编程语言开始讲述,比如Lisp和Smalltalk,它们的数据结构和函数式编程的特性促进了响应式编程的发展。然后,文章提到了响应式编程框架的出现,如React和Vue.js等。这些框架使用虚拟DOM(Virtual DOM)技术来跟踪数据变化,并更新界面。文章还讨论了响应式编程的优点和缺点,如可读性和性能等。最后,文章预测了未来响应式编程的发展方向。

总的来说,本文很好地介绍了响应式编程的历史和发展,深入浅出地讲述了它的优点和缺点。文章提到了很多实际应用和框架的例子,让读者更好地理解响应式编程的概念和实践。文章还预测了未来响应式编程的发展方向,这对读者和开发者有很大的启示作用。

下面是正文。。。

这篇文章并不是关于响应式的权威历史,而是关于我个人在这方面的经历和观点。

Flex

我的旅程始于 macromedia Flex,后来被 Adobe 收购。Flex 是基于 Flash 上的 ActionScript 的一个框架。ActionScript 与 JAVAScript 非常相似,但它具有注解功能,允许编译器为订阅包装字段。我不记得确切的语法了,也在网上找不到太多信息,但它看起来是这样的:

 
class MyComponent {
[Bindable] public var name: String;
}

[Bindable] 注解会创建一个 getter/setter,当属性发生变化时,它会触发事件。然后你可以监听属性的变化。Flex 附带了用于渲染 UI 的 .mxml 文件模板。如果属性发生变化,.mxml 中的任何数据绑定都是细粒度的响应式,因为它通过监听属性的变化。

 
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
  <mx:MyComponent>
    <mx:Label text="{name}"/></mx:Label>
  </mx:MyComponent>
</mx:Applicatio>

我怀疑 Flex 并不是响应式最早出现的地方,但它是我第一次接触到响应式。

在 Flex 中,响应式有点麻烦,因为它容易创建更新风暴。更新风暴是指当单个属性变化触发许多其他属性(或模板)变化,从而触发更多属性变化,依此类推。有时,这会陷入无限循环。Flex 没有区分更新属性和更新 UI,导致大量的 UI 抖动(渲染中间值)。

事后看来,我可以看到哪些架构决策导致了这种次优结果,但当时我并不清楚,我对响应式系统有点不信任。

AngularJS

AngularJS 的最初目标是扩展 html 词汇,以便设计师(非开发人员)可以构建简单的 Web 应用程序。这就是为什么 AngularJS 最终采用了 HTML 标记的原因。由于 AngularJS 扩展了 HTML,它需要绑定到任何 JavaScript 对象。那时候既没有 Proxy、getter/setters,也没有 Object.observe() 这些选项可供选择。所以唯一可用的解决方案就是使用脏检查。

脏检查通过在浏览器执行任何异步工作时读取模板中绑定的所有属性来工作。

 
<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}!</h1>
    </div>
  </body>
</html>

这种方法的好处是,任何 JavaScript 对象都可以在模板中用作数据绑定源,更新也能正常工作。

缺点是每次更新都要执行大量的 JavaScript。而且,因为 AngularJS 不知道何时可能发生变化,所以它运行脏检查的频率远远超过理论上所需。

因为 AngularJS 可以与任何对象一起工作,而且它本身是 HTML 语法的扩展,所以 AngularJS 从未将任何状态管理形式固化。

React

React在AngularJS(Angular之前)之后推出,并进行了几项改进。

首先,React引入了setState()。这使得React知道何时应该对vDOM进行脏检查。这样做的好处是,与每个异步任务都运行脏检查的AngularJS不同,React只有在开发人员告诉它要运行时才会执行。因此,尽管React vDOM的脏检查比AngularJS更耗费计算资源,但它会更少地运行。

 
function Counter() {
  const [count, setCount] = useState();
  return <button onClick={() => setCount(count+1)}>{count}</button>
}

其次,React引入了从父组件到子组件的严格数据流。这是朝着框架认可的状态管理迈出的第一步,而AngularJS则没有这样做。

粗粒度响应性

React 和 AngularJS 都是粗粒度响应式的。这意味着数据的变化会触发大量的 JavaScript 执行。框架最终会将所有的更改合并到 UI 中。这意味着快速变化的属性,如动画,可能会导致性能问题。

细粒度响应性

解决上述问题的方法是细粒度响应性,状态改变只更新与状态绑定的 UI 部分。

难点在于如何以良好的开发体验(DX)来监听属性变化。

Backbone.js

Backbone 早于 AngularJS,它具有细粒度的响应性,但语法非常冗长。

 
var MyModel = Backbone.Model.extend({
  initialize: function() {
    // Listen to changes on itself.
    this.on('change:name', this.onAsdChange);
  },
  onNameChange: function(model, value) {
    console.log('Model: Name was changed to:', value);
  }
});
var myModel = new MyModel();
myModel.set('name', 'something');

我认为冗长的语法是像 AngularJS 和后来的 React 这样的框架取而代之的原因之一,因为开发者可以简单地使用点符号来访问和设置状态,而不是一组复杂的函数回调。在这些较新的框架中开发应用程序更容易,也更快。

Knockout

Knockout 和 AngularJS 出现在同一时期。我从未使用过它,但我的理解是它也受到了更新风暴问题的困扰。虽然它在 Backbone.js 的基础上有所改进,但与可观察属性一起使用仍然很笨拙,这也是我认为开发者更喜欢像 AngularJS 和 React 这样的点符号框架的原因。

但是 Knockout 有一个有趣的创新 —— 计算属性,它可能已经存在过,但这是我第一次听说。它们会自动在输入上创建订阅。

 
var ViewModel = function(first, last) {
  this.firstName = ko.observable(first);
  this.lastName = ko.observable(last);
  this.fullName = ko.pureComputed(function() {
    // Knockout tracks dependencies automatically.
    // It knows that fullName depends on firstName and lastName,
    // because these get called when evaluating fullName.
    return this.firstName() + " " + this.lastName();
  }, this);
};

请注意,当 ko.pureComputed() 调用 this.firstName() 时,值的调用会隐式地创建一个订阅。这是通过 ko.pureComputed() 设置一个全局变量来实现的,这个全局变量允许 this.firstName() 与 ko.pureComputed() 通信,并将订阅信息传递给它,而无需开发者进行任何额外的工作。

Svelte

Svelte使用编译器实现了响应式。这里的优势在于,有了编译器,语法可以是任何你想要的。你不受JavaScript的限制。对于组件,Svelte具有非常自然的响应式语法。但是,Svelte并不会编译所有文件,只会编译以.svelte结尾的文件。如果你希望在未经过编译的文件中获得响应性,则Svelte提供了一个存储API,它缺少已编译响应性所具有的魔力,并需要更明确地注册使用subscribe和unsubscribe。

 
const count = writable(0);
const unsubscribe = count.subscribe(value => {
  countValue = value;
});

我认为拥有两种不同的方法来实现同样的事情并不理想,因为你必须在脑海中保持两种不同的思维模式并在它们之间做出选择。一种统一的方法会更受欢迎。

RxJS

RxJS 是一个不依赖于任何底层渲染系统的响应式库。这似乎是一个优势,但它也有一个缺点。导航到新页面需要拆除现有的 UI 并构建新的 UI。对于 RxJS,这意味着需要进行很多取消订阅和订阅操作。这些额外的工作意味着在这种情况下,粗粒度响应式系统会更快,因为拆除只是丢弃 UI(垃圾回收),而构建不需要注册/分配监听器。我们需要的是一种批量取消订阅/订阅的方法。

 
const observable1 = interval(400);
const observable2 = interval(300);
const subscription = observable1.subscribe(x => console.log('[first](https://rxjs.dev/api/index/function/first): ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
subscription.add(childSubscription);
setTimeout(() => {
  // Unsubscribes BOTH subscription and childSubscription
  subscription.unsubscribe();
}, 1000);

Vue 和 MobX

大约在同一时间,Vue 和 MobX 都开始尝试基于代理的响应式。代理的优势在于,你可以使用开发者喜欢的干净的点表示法语法,同时可以像 Knockout 一样使用相同的技巧来创建自动订阅 —— 这是一个巨大的胜利!

 
<template>
  <button @click="count = count + 1">{{ count }}</button>
</template>

<script setup>
import { ref } from "vue";

const count = ref(1);
</script>

在上面的示例中,模板在渲染期间通过读取 count 值自动创建了一个对 count 的订阅。开发者无需进行任何额外的工作。

SolidJS

SolidJS 的缺点是无法将引用传递给 getter/setter。你要么传递整个代理,要么传递属性的值,但是你无法从存储中剥离一个 getter 并传递它。以此为例来说明这个问题。

 
function App() {
  const state = createStateProxy({count: 1});
  return (
    <>
      <button onClick={() => state.count++}>+1</button>
      <Wrapper value={state.count}/>
    </>
  );
}

function Wrapper(props) {
  return <Display value={state.value}/>
}
function Display(props) {
  return <span>Count: {props.value}</span>
}

当我们读取 state.count 时,得到的数字是原始的,不再是可观察的。这意味着 Middle 和 Child 都需要在 state.count 改变时重新渲染。我们失去了细粒度的响应性。理想情况下,只有 Count: 应该被更新。我们需要的是一种传递值引用而不是值本身的方法。

signals

signals 允许你不仅引用值,还可以引用该值的 getter/setter。因此,你可以使用信号解决上述问题:

function App() {
  const [count, setCount] = createSignal(1);
  return (
    <>
      <button onClick={() => setCount(count() + 1)}>+1</button>
      <Wrapper value={count}/>
    </>
  );
}
function Wrapper(props: {value: Accessor<number>}) {
  return <Display value={props.value}/>
}
function Display(props: {value: Accessor<number>}) {
  return <span>Count: {props.value}</span>
}

这种解决方案的好处在于,我们不是传递值,而是传递一个 Accessor(一个 getter)。这意味着当 count 的值发生更改时,我们不必经过 Wrapper 和 Display,可以直接到达 DOM 进行更新。它的工作方式非常类似于 Knockout,但在语法上类似于 Vue/MobX。

假设我们想要绑定到一个常量作为组件的用户,则会出现 DX 问题。

 
<Display value={10}/>

这样做不会起作用,因为 Display 被定义为 Accessor:

 
function Display(props: {value: Accessor<number>});

这是令人遗憾的,因为组件的作者现在定义了使用者是否可以发送getter或 value。无论作者选择什么,总会有未涵盖的用例。这两者都是合理的事情。

<Display value={10}/>
<Display value={createSignal(10)}/>

以上是使用 Display 的两种有效方式,但它们都不能同时成立!我们需要一种方法来将类型声明为基本类型,但可以同时与基本类型和 Accessor 一起使用。这时编译器就出场了。

 
function App() {
  const [count, setCount] = createSignal(1);
  return (
    <>
      <button onClick={() => setCount(count() + 1)}>+1</button>
      <Wrapper value={count()}/>
    </>
  );
}
function Wrapper(props: {value: number}) {
  return <Display value={props.value}/>
}
function Display(props: {value: number}) {
  return <span>Count: {props.value}</span>
}

请注意,现在我们声明的是 number,而不是 Accessor。这意味着这段代码将正常工作

 
<Display value={10}/>
<Display value={createSignal(10)()}/> // Notice the extra ()

但这是否意味着我们现在已经破坏了响应性?答案是肯定的,除非我们可以让编译器执行一个技巧来恢复我们的响应性。问题就出在这行代码上:

 
<Wrapper value={count()}/>

count()的调用会将访问器转换为原始值并创建一个订阅。因此编译器会执行这个技巧。

 
Wrapper({
  get value() { return count(); }
})

通过在将count()作为属性传递给子组件时,在getter中包装它,编译器成功地延迟了对count()的执行,直到DOM实际需要它。这使得DOM可以创建基础信号的订阅,即使对开发人员来说似乎是传递了一个值。

好处有:

  • 清晰的语法
  • 自动订阅和取消订阅
  • 组件接口不必选择原始类型或Accessor。
  • 响应性即使开发人员将Accessor转换为原始类型也能正常工作。

我们还能在此基础上做出什么改进吗?

响应性和渲染

让我们想象一个产品页面,有一个购买按钮和一个购物车。

 

图片

 

在上面的示例中,我们有一个树形结构中的组件集合。用户可能采取的一种可能的操作是点击购买按钮,这需要更新购物车。对于需要执行的代码,有两种不同的结果。

在粗粒度响应式系统中,它是这样的:

 

图片

 

我们必须找到 Buy  和 Cart 组件之间的共同根,因为状态很可能附加在那里。然后,在更改状态时,与该状态相关联的树必须重新渲染。使用 memoization 技术,可以将树剪枝成仅包含上述两个最小路径。尤其是随着应用程序变得越来越复杂,需要执行大量代码。

在细粒度反应式系统中,它看起来像这样:

 

图片

 

请注意,只有目标 Cart 需要执行。无需查看状态是在哪里声明的或共同祖先是什么。也不必担心数据记忆化以修剪树。精细的反应式系统的好处在于,开发人员无需任何努力,运行时只执行最少量的代码!

精细的反应式系统的手术精度使它们非常适合懒惰执行代码,因为系统只需要执行状态的侦听器(在我们的例子中是 Cart)。

但是,精细的反应式系统有一个意外的角落案例。为了建立反应图,系统必须至少执行所有组件以了解它们之间的关系!一旦建立起来,系统就可以进行手术。这是初始执行的样子:

 

图片

 

你看出问题了吗?我们想懒惰地下载和执行,但反应图的初始化强制执行应用程序的完整下载。

Qwik

这就是 Qwik 发挥作用的地方。Qwik 是精细的反应式,类似于 SolidJS,意味着状态的变化直接更新 DOM。(在某些角落情况下,Qwik 可能需要执行整个组件。)但是 Qwik 有一个诡计。记得精细的反应性要求所有组件至少执行一次以创建反应图吗?好吧,Qwik 利用了组件在 SSG 期间已经在服务器上执行的事实。Qwik 可以将这个图形序列化为 HTML。这使得客户端完全可以跳过最初的“执行世界以了解反应图”的步骤。我们称这种能力为可恢复性。由于组件在客户端上不会执行或下载,因此 Qwik 的好处是应用程序的即时启动。一旦应用程序正在运行,反应就像 SolidJS 一样精确。

原文:https://www.builder.io/blog/history-of-reactivity



Tags:编程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
腾讯首款自研 Switch 游戏上线:《腾讯扣叮-编程第一课》
IT之家 4 月 11 日消息,腾讯 Nintendo Switch 今日官宣,Nintendo Switch 趣味编程学习软件 &mdash;&mdash;《腾讯扣叮-编程第一课》已于今日上架 Nintendo e 商店,建议零售价 4...【详细内容】
2024-04-11  Search: 编程  点击:(2)  评论:(0)  加入收藏
“不懂编程就是文盲”?别用焦虑论调裹挟家长
据3月17日中新网报道,随着科技的发展和社会的进步,科创教育逐渐走入千家万户,少儿编程作为代表,近年来更是备受瞩目。然而,“未来的文盲,就是不懂编程的人”的论调也甚嚣尘上。必...【详细内容】
2024-03-19  Search: 编程  点击:(9)  评论:(0)  加入收藏
编程二十年,38岁谷歌程序员的16条建议,涉创业、技术淘汰、拿大厂offer……
以能够让项目自负盈亏,并在这一约束下优化其增长的方式来管理和调整项目,这是这个世上最厉害的本事。距离我正式开始编程的工作已经过去二十年了。在这些年里,我有以下收获: 获...【详细内容】
2024-03-10  Search: 编程  点击:(4)  评论:(0)  加入收藏
C#异步编程:Task.Run vs. async-await,掌握基础与高级用法
概述:C#中的异步编程有两主要方式:Task.Run用于在后台线程执行同步操作,而async-await更适用于清晰表达异步流程。基础用法展示了它们的简单应用,高级用法则演示了它们的结合使...【详细内容】
2024-03-09  Search: 编程  点击:(23)  评论:(0)  加入收藏
C++多线程编程:解锁性能与并发的奥秘
今天我们将深入探讨C++中的多线程编程,揭示多线程如何解锁性能潜力,提高程序的并发性能。什么是多线程?在计算机科学中,多线程是指一个进程(程序的执行实例)中的多个线程同时执行...【详细内容】
2024-02-03  Search: 编程  点击:(69)  评论:(0)  加入收藏
主流编程语言哪个更容易学?
主流编程语言哪个更容易学?在当今数字化时代,编程语言已成为一项重要的技能,越来越多的人开始学习编程。然而,对于初学者来说,选择一门容易入门的编程语言是至关重要的。在本文中...【详细内容】
2024-01-31  Search: 编程  点击:(138)  评论:(0)  加入收藏
用于人工智能开发的主流编程语言都有哪些?
在人工智能开发领域,编程语言的选择至关重要。目前,主流的编程语言主要包括Python、Java、C++、JavaScript和Swift等。这些语言各具特色,适用于不同的人工智能开发场景。首先,Py...【详细内容】
2024-01-31  Search: 编程  点击:(132)  评论:(0)  加入收藏
不容错过的4款宝藏GPTs:程序员新宠,让编程不再枯燥!
原文来源:硬 AI自从GPT Store“开业”以来,我们就一直在使用不同的GPTs,尝试自动化一些重复繁琐的日常工作。在浩如烟海的GPTs中,虽然真正有用的并不多,很多GPTs的功能都比较局限...【详细内容】
2024-01-24  Search: 编程  点击:(46)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  Search: 编程  点击:(106)  评论:(0)  加入收藏
在 Rust 编程中使用泛型
本文的内容将涉及泛型定义函数、结构体、枚举和方法, 还将讨论泛型如何影响代码性能。1.摘要Rust中的泛型可以让我们为像函数签名或结构体这样的项创建定义, 这样它们就可以...【详细内容】
2024-01-09  Search: 编程  点击:(89)  评论:(0)  加入收藏
▌简易百科推荐
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(0)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
站内最新
站内热门
站内头条