关注【索引目录】服务号,更多精彩内容等你来探索!
响应式是现代前端开发中最具影响力的概念之一。应用程序需要能够快速更新,通过复杂的数据关系传递变更,并在不断发展过程中保持可靠性。
大多数成熟的响应式库(例如MobX、Vue的响应式系统以及Svelte)都为开发人员提供了极大的便利和强大的功能。我已经广泛使用过它们,其中大多数(例如 MobX)都能以最少的样板代码让你的值“神奇地”具有响应式。它的便利性令人印象深刻!然而……
在使用它们开发复杂系统时,我遇到了诸如隐式行为层级、语法限制以及控制力不足等问题。其他解决方案,例如RxJS,虽然提供了精细的控制力,但其函数式响应式编程的学习曲线却非常陡峭,开发人员必须完全掌握。使用不当通常会导致代码混乱且难以调试。
Fluid 的诞生源于一种愿望,即重新平衡响应式,以提升清晰度、可预测性和更高程度的控制力,即便这会牺牲一些便利性。它旨在揭示状态变化的完整路径,使每个突变、每个依赖项、每个通知以及执行顺序都清晰可见,并完全符合开发者的意图。
为什么需要这个?对于拥有庞大而复杂代码库的实时应用程序来说,像 MobX 这样的工具的“简单性”可能会引发一些难题:“这个计算属性的完整依赖列表是什么?”,“
如果我将这个 getter 设为计算属性会发生什么?”,或者“我如何确保 Response在Response之后但在Response之前C执行?”AB。在这种情况下,感知到的简单性反而会成为瓶颈。
概述
Fluid通过一系列核心原则实现其目标:
-
每个反应对象都是一个类型构造器(如 Promise)。 -
一切都很清楚。 -
一切都可控。 -
一切都是同步的。 -
未 Proxy使用;仅使用普通对象和函数。 - 性能
:由于不需要对侧面和普通数据结构进行大量计算, Fluid因此速度非常快,并且占用内存较少。
const _name_ = Fluid.val("Alice")
const _surname_ = Fluid.val("Liddell")
const _fullName_ = Fluid.derive(
[_name_, _surname_],
(name, surname) => `${name} ${surname}`
)
// Explicit subscription:
Fluid.listen(
_fullName_,
name => console.log("User's full name:", name)
)
Fluid.write(_name_, "Jane") // User's full name: Jane Liddell
Fluid.read(_fullName_) // "Jane Liddell"
这里的核心概念与其他反应系统类似:
- 反应值
:( Fluid.val读写)。 - 派生
:( Fluid.derive只读)。 - 听众
:( Fluid.listen激发变化的影响)。 - 阅读
:( Fluid.read返回反应对象的当前状态)。 - 写入
:( Fluid.write为反应值设置新状态)。
Fluid 的独特之处
从上面的例子来看,人们可能会认为Fluid这只是相同概念的另一种语法。然而,它提供了独特的功能:
1. 控制评估顺序
优先级 API
通常,同步库会构建依赖树,检测并解决循环依赖,并自动重新平衡执行顺序。
设想一下以下购物车商店,它计算商品price、shipping费用并tax显示总计。此处代码使用 MobX 库编写:
import { makeObservable, observable, computed } from 'mobx'
class Cart {
price = 0
constructor() {
makeObservable(this, {
price: observable,
shipping: computed,
tax: computed,
showTotal: computed,
})
}
get shipping() {
return this.price > 50 ? 0 : 5.00
}
get tax() {
return this.price * 0.08 // 8%
}
get showTotal() {
const total = this.price + this.tax + this.shipping
return `Final price: $${total.toFixed(2)} (incl. tax: $${this.tax.toFixed(2)}, shipping: $${this.shipping.toFixed(2)})`
}
}
const cart = new Cart()
cart.price = 20
console.log(cart.showTotal) // Final price: $26.60 (incl. tax: $1.60, shipping: $5.00)
这里,showTotalcomputed 有三个依赖项:price、tax和shipping。shipping和tax依赖于单个可观察对象price,并且它还知道应该仅在和更新showTotal后重新评估。抽象的依赖图如下所示:shippingtax
_price_
/ | \
⌄ | ⌄
tax | shipping
\ | /
⌄ ⌄ ⌄
showTotal
最重要的是,更新 之后price,showTotal应该只更新一次!如果每个依赖项都触发了自己的更新,showTotal就会不必要地重新评估多次!
它会Fluid自动解决这个菱形依赖问题吗?不,它不会!但它提供了一个强大的工具来自己解决这个问题priorities:
您明确声明这些关系:
const _price_ = Fluid.val(0)
const _tax_ = Fluid.derive(
_price_,
price => price * 0.08, // 8% tax
)
const _shipping_ = Fluid.derive(
_price_,
price => price > 50 ? 0 : 5.00, // free shipping over $50
)
const _totalSummary_ = Fluid.derive(
_price_, // We only need to subscribe to the root dependency.
(price) => {
// We read the latest values of other derivations inside.
const tax = Fluid.read(_tax_)
const shipping = Fluid.read(_shipping_)
const total = price + tax + shipping
return `Final price: $${total.toFixed(2)} (incl. tax: $${tax.toFixed(2)}, shipping: $${shipping.toFixed(2)})`
},
// Set priority: execute this derivation *after* the base level.
{ priority: Fluid.priorities.after(Fluid.priorities.base) },
)
Fluid.write(_price_, 20.00)
console.log(Fluid.read(_totalSummary_)) // Final price: $26.60 (incl. tax: $1.60, shipping: $5.00)
上述依赖关系的解析图将构建以下更新时间表:
_price_
|
+---> _tax_
+---> _shipping_
|
+---> _totalSummary_
每个derive和 都listen接受一个priority选项。这允许您声明执行顺序。这里,_totalSummary_依赖于_price_,并且它的更新安排在base优先级池之后,优先级池默认放置像_tax_和这样的依赖项。_shipping_
优先级只是一个数字。Fluid.priorities.after(Fluid.priorities.base)是 的可读助手-1。执行顺序如下:
HIGHER
0: [_tax_, _shipping_]
-1: [_totalSummary_]
LOWER
是的,它实际上只是一个数字,数字越高,优先级越高:
const _msg_ = Fluid.val("")
const log = console.log
Fluid.listen(_msg_, (msg) => log("3: " + msg), { priority: 3 })
Fluid.listen(_msg_, (msg) => log("2: " + msg), { priority: 2 })
Fluid.listen(_msg_, (msg) => log("4: " + msg), { priority: 4 })
Fluid.listen(_msg_, (msg) => log("1: " + msg), { priority: 1 })
Fluid.write(_msg_, "Hi?")
// 4: Hi?
// 3: Hi?
// 2: Hi?
// 1: Hi?
2. 交易
交易 API
批量修改是响应式系统的常见功能。在 中Fluid,这种方法乍一看可能有所不同,但它具有高度可控性和强大的功能。
懒惰写
事务Fluid本质上是一种延迟写入。
const _name_ = Fluid.val('Mike')
const transaction = Fluid.transaction.write(_name_, 'MIKE')
Fluid.read(_name_) // Mike
transaction.run()
Fluid.read(_name_) // MIKE
与标准写入还有其他区别吗?是的——我们也可以拒绝写入,防止状态改变。
const _counter_ = Fluid.val(1)
const inc = () => Fluid.transaction.write(
_counter_,
count => {
if (count < 3)
return Fluid.transaction.success(count + 1)
else
return Fluid.transaction.error()
},
)
let result = inc().run()
console.log(Fluid.read(_counter_), Fluid.transaction.isSuccess(result)) // 2, true
result = inc().run()
console.log(Fluid.read(_counter_), Fluid.transaction.isSuccess(result)) // 3, true
result = inc().run()
console.log(Fluid.read(_counter_), Fluid.transaction.isSuccess(result)) // 3, false
这是一个非常有用的概念。你可以将交易作为对象传递并对其进行操作,而无需知道它们写入的内容——你只需要处理交易本身!
这是交易成功时重新绘制图形的示例!
import { Fluid, ReactiveTransaction } from 'reactive-fluid'
class Graphics {
// ...
addObject(object) {
this.objects.push({
draw() {
// ...
}
})
}
redraw() {
this.ctx.clear()
this.objects.forEach(object => object.draw())
}
}
const graphics = new Graphics()
/**
* Execute transaction and redraw graphics on success
*/
function update(transaction: ReactiveTransaction) {
const res = transaction.run()
if (Fluid.transaction.isSuccess(res)) {
graphics.redraw();
}
}
const player = Fluid.val({ x: 0, y: 0 })
const enemy = Fluid.val({ x: 10, y: 0 })
// register objects
graphics.addObject(player)
graphics.addObject(enemy)
const movePlayer = Fluid.transaction.write(_player_, player => {
player.x += 10;
return Fluid.transaction.success(player)
}
// if moving would be successful - scene would be redrawed!
update(movePlayer)
创作写作
但事务的核心是批量处理更改!惰性写入在这里有什么用呢?答案是:我们可以将compose多个写入合并到一个原子事务中。
const _name_ = Fluid.val("Alice")
const _surname_ = Fluid.val("Liddell")
const _fullName_ = Fluid.derive(
[_name_, _surname_],
(name, surname) => `${name} ${surname}`
)
Fluid.listen(
_fullName_,
(full) => console.log(`The full name is: ${full}`),
)
const nameTransaction = Fluid.transaction.compose(
Fluid.transaction.write(_name_, "Mark"),
Fluid.transaction.write(_surname_, "Smith"),
)
nameTransaction.run()
// The full name is: Mark Smith
如果我们逐个写入,监听器将被触发两次。而使用组合事务,则只需触发一次!
取消交易
事务的另一个重要方面是原子性:如果事务中的任何一个部分出现错误,所有更改都将被拒绝。这可以防止应用程序进入损坏或不一致的状态。Fluid状态遵循此原则error()。
const _name_ = Fluid.val("Alice")
const _surname_ = Fluid.val("Liddell")
const _age_ = Fluid.val(22)
const _userinfo_ = Fluid.derive(
[_name_, _surname_, _age_],
(name, surname, age) => `${name} ${surname}, ${age}`
)
Fluid.listen(
_userinfo_,
(info) => console.log(`user info: ${info}`),
)
const update = Fluid.transaction.compose(
Fluid.transaction.write(_name_, "Mark"),
Fluid.transaction.write(_surname_, () => Fluid.transaction.error("NOT FOUND")),
Fluid.transaction.write(_age_, 30),
)
update.run() // listener wasn't triggered
Fluid.read(_userinfo_) // "Alice Liddell, 22" (state remains unchanged)
这不仅仅是关于交易的。重要的问题可能会出现:如何在组合列表中查看先前成功交易的更新值?如何读取依赖计算的更新状态?
如果您对答案感兴趣,您可以在文档的“撰写交易”部分找到它们。
3. 没有隐藏的记忆
正如你所见,Fluid它不会在你背后执行任何操作。如果你调用write,它会写入新值并通知所有监听器,仅此而已。这也意味着你不必使用不可变的数据结构。
const _cart_ = Fluid.val<Array<number>>([])
const _cartSum_ = Fluid.derive(
_cart_,
cart => cart.reduce((acc, item) => acc + item, 0)
)
// This function mutates the array directly.
const pushToCart = (item: number) => {
Fluid.write(_cart_, cart => {
cart.push(item)
// We must return the mutated array to signal a change.
return cart
})
}
pushToCart(5)
pushToCart(5)
pushToCart(5)
Fluid.read(_cartSum_) // 15
4. 动态依赖
由于 中的反应对象Fluid是“一等公民”,它们可以相互嵌套。这个强大的功能允许你创建动态依赖项,其中派生值可以根据应用程序状态切换其底层源。
想象一下这样一个场景:一个_son_响应式值会根据他的 反映不同的来源_age_。当他未满 18 岁的时候,他的“声音”来自他的父母(_mommy_和_daddy_)。一旦他年满 18 岁,他的声音就变成了他自己的,由一个单独的、可写的响应式来源(_matureSon_)来表示。
const _mommy_ = Fluid.val("Eat your breakfast");
const _daddy_ = Fluid.val("Go to school");
const _age_ = Fluid.val(10);
const _matureSon_ = Fluid.val("...");
const _youngSon_ = Fluid.derive(
[_mommy_, _daddy_],
(mommy, daddy) => `Mommy said: "${mommy}", Daddy said: "${daddy}"`
);
// This derivation returns another reactive object, not a simple value.
const _son_ = Fluid.derive(
_age_,
age => (age >= 18 ? _matureSon_ : _youngSon_)
);
// To get the final value, you must 'unwrap' it twice:
// 1. Fluid.read(_son_) -> returns either _matureSon_ or _youngSon_
// 2. Fluid.read( ... ) -> reads the value from that inner object
console.log(Fluid.read(Fluid.read(_son_))); // Mommy said: "Eat your breakfast", Daddy said: "Go to school"
Fluid.write(_age_, 20);
// Now, _son_ points to _matureSon_
console.log(Fluid.read(Fluid.read(_son_))); // "..."
// We can now write directly to the new source
const currentSonSource = Fluid.read(_son_);
Fluid.write(currentSonSource, "I want to be a musician");
console.log(Fluid.read(Fluid.read(_son_))); // "I want to be a musician"
虽然此示例刻意简化,但在实际应用中,这种模式需要慎重考量。当依赖项切换(例如,从 切换_youngSon_到_matureSon_)时,旧源(_youngSon_)将不再被 跟踪_son_,并且所有对旧源的订阅仍然有效。因此,对于更复杂的对象,您应该注意内存管理,并可能需要使用类似 的工具Fluid.destroy来正确地取消订阅并清理未使用的响应式对象,以防止潜在的内存泄漏。

