大数跨境

Fluid 简介:现代 JavaScript 的明确且稳健的反应性

Fluid 简介:现代 JavaScript 的明确且稳健的反应性 索引目录
2025-09-09
2
导读:关注【索引目录】服务号,更多精彩内容等你来探索!响应式是现代前端开发中最具影响力的概念之一。

关注【索引目录】服务号,更多精彩内容等你来探索!

响应式是现代前端开发中最具影响力的概念之一。应用程序需要能够快速更新,通过复杂的数据关系传递变更,并在不断发展过程中保持可靠性。

大多数成熟的响应式库(例如MobXVue的响应式系统以及Svelte)都为开发人员提供了极大的便利和强大的功能。我已经广泛使用过它们,其中大多数(例如 MobX)都能以最少的样板代码让你的值“神奇地”具有响应式。它的便利性令人印象深刻!然而……

在使用它们开发复杂系统时,我遇到了诸如隐式行为层级、语法限制以及控制力不足等问题。其他解决方案,例如RxJS,虽然提供了精细的控制力,但其函数式响应式编程的学习曲线却非常陡峭,开发人员必须完全掌握。使用不当通常会导致代码混乱且难以调试。

Fluid 的诞生源于一种愿望,即重新平衡响应式,以提升清晰度、可预测性和更高程度的控制力,即便这会牺牲一些便利性。它旨在揭示状态变化的完整路径,使每个突变、每个依赖项、每个通知以及执行顺序都清晰可见,并完全符合开发者的意图。

为什么需要这个?对于拥有庞大而复杂代码库的实时应用程序来说,像 MobX 这样的工具的“简单性”可能会引发一些难题:“这个计算属性的完整依赖列表是什么?”
如果我将这个 getter 设为计算属性会发生什么?”,或者“我如何确保 Response在Response之后但在Response之前C执行?”AB。在这种情况下,感知到的简单性反而会成为瓶颈。


概述

Fluid通过一系列核心原则实现其目标:

  1. 每个反应对象都是一个类型构造器(如Promise)。
  2. 一切都很清楚
  3. 一切都可控
  4. 一切都是同步的
  5. Proxy使用;仅使用普通对象和函数
  6. 性能
    :由于不需要对侧面和普通数据结构进行大量计算,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

通常,同步库会构建依赖树,检测并解决循环依赖,并自动重新平衡执行顺序。

设想一下以下购物车商店,它计算商品priceshipping费用并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 有三个依赖项:pricetaxshippingshippingtax依赖于单个可观察对象price,并且它还知道应该仅在和更新showTotal后重新评估。抽象的依赖图如下所示:shippingtax

_price_
 /    |    \
⌄     |     ⌄
tax   |  shipping
 \    |    /
  ⌄   ⌄   ⌄
  showTotal

最重要的是,更新 之后priceshowTotal应该只更新一次!如果每个依赖项都触发了自己的更新,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来正确地取消订阅并清理未使用的响应式对象,以防止潜在的内存泄漏。

关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444