直接跳到内容

响应式基础

声明响应式状态

ref()

推荐使用 ref() 函数来声明响应式状态:

python
from vuepy import ref

count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

python
count = ref(0)

print(count) # vuepy.vue.VueRef object at ...
print(count.value) # 0

count.value++
print(count.value) # 1

要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:

python
from vuepy import ref

def setup(*args, **kwargs):
    count = ref(0)

    # 将 ref 暴露给模板
    return locals()
  }
}
template
<div>{{ count.value }}</div>

对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开:

python
from vuepy import ref

def setup(*args, **kwargs):
    count = ref(0)

    def increment(own) {
      # 在 python 中需要 .value
      count.value += 1
    }

    # 不要忘记同时暴露 increment 函数, 也可以直接返回 locals()
    return {
      "count": count,
      "increment": increment,
    }
  }
}

然后,暴露的方法可以被用作事件监听器:

template
<Button @click="increment" 
        :label="f'count {count.value}'">
</Button>

<script lang="py">

setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script lang="py"> 来大幅度地简化代码:

vue
<template>
  <Button @click="increment" 
          :label="f'count is {count.value}'">
  </Button>
</template>

<script lang="py">
from vuepy import ref

count = ref(0)

def increment(own):
    print(own)
    count.value += 1
    
</script>

<script lang="py"> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 Python 函数——它自然可以访问与它一起声明的所有内容。

TIP

在指南的后续章节中,我们基本上都会在组合式 API 示例中使用单文件组件 + <script lang="py"> 的语法,因为大多数 Vue.py 开发者都会这样使用。

如果你没有使用单文件组件,你仍然可以在 setup() 选项中使用组合式 API。

为什么要使用 ref?

你可能会好奇:为什么我们需要使用带有 .value 的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue.py 的响应式系统是如何工作的。

当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue.py 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue.py 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。

在标准的 Python 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

.value 属性给予了 Vue.py 一个机会来检测 ref 何时被访问或修改。在其内部,Vue.py 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:

python
// 伪代码,不是真正的实现
class Ref:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        track()
        return self._value

    @value.setter
    def value(self, val):
        self._value = val
        trigger()

另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。

该响应性系统在深入响应式原理章节中有更详细的讨论。

深层响应性

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 Python 内置的数据结构,比如 dict

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:

js
import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。

也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

阅读更多:

DOM 更新时机

当你修改了响应式状态时,DOM 会被自动更新。DOM 更新是同步的,Jupyter 会阻塞并等待更新完成,所以比较耗时的处理函数最好放到线程中异步执行。

vue
<template>
  <Button @click="async_run" :label="f'run {state.value}'"></Button>
</template>
<script setup>
import Button from "../../../src/ipywui/components/Button";
</script>

<script lang="py">
import time
import threading
from vuepy import ref

state = ref('')

def async_run(own):
    def task_block():
        state.value = 'start'
        time.sleep(2)
        state.value = 'finish'

    # task_block()
    thread = threading.Thread(target=task_block, args=())
    thread.start()
    
</script>

reactive()

还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

python
from vuepy import reactive

state = reactive({ count: 0 })

在模板中使用:

template
<p>
  {{ state.count }}
</p>

响应式对象是 Python 代理(类似 Javascript Proxy),其行为就和普通对象一样。不同的是,Vue.py 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。

Reactive Proxy vs. Original

值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

python
raw = {}
proxy = reactive(raw)

# 代理对象和原始对象不是全等的
proxy is raw # False

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue.py 的响应式系统的最佳实践是仅使用你声明对象的代理版本

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

python
# 在同一个对象上调用 reactive() 会返回相同的代理
reactive(raw) is proxy) # true

# 在一个代理上调用 reactive() 会返回它自己
reactive(proxy) is proxy # true

这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

python
proxy = reactive({})

raw = {}
proxy.nested = raw

proxy.nested is raw # false

reactive() 的局限性

reactive() API 有一些局限性:

  1. 有限的值类型:它只能用于可变类型 (对象、数组和如 MapSet 这样的可变类型)。它不能持有如 stringintboolean 这样的不可变类型

  2. 不能替换整个对象:由于 Vue.py 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    py
    state = reactive({ count: 0 })
    
    # 上面的 ({ count: 0 }) 引用将不再被追踪
    # (响应性连接已丢失!)
    state = reactive({ count: 1 })

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

响应式基础已经加载完毕