observable
主要用于创建不同响应式行为的 observable 对象,同时可以作为 annotation 给 define 用于标记响应式属性
核心概念
observable 是整个响应式模型的基础。通过创建可订阅对象,@formily/reactive 可以在属性被读取时收集依赖,在属性被写入时通知订阅者。底层主要基于 ES Proxy 实现,因此可以完整拦截对象上的数据操作。
除了直接使用 observable 系列 API,也可以结合 define 和 model 组织领域模型,本质上仍然是在组合 observable、computed 与 action/batch 的能力。
observable/observable.deep
描述
创建深度劫持响应式对象
签名
interface observable<T extends object> {
(target: T): T
}
interface deep<T extends object> {
(target: T): T
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, pushLog, useAutorunEffect } from './shared'
const logs = ref<string[]>([])
const rawValue = ref(123)
const trackedValue = ref(123)
const rawSnapshot = ref('')
const trackedSnapshot = ref('')
const runCount = ref(0)
const obs = observable({
aa: {
bb: 123,
},
})
function syncRawState() {
rawValue.value = obs.aa.bb
rawSnapshot.value = formatValue(obs)
}
useAutorunEffect(() => {
runCount.value += 1
trackedValue.value = obs.aa.bb
trackedSnapshot.value = formatValue(obs)
pushLog(logs, `autorun #${runCount.value}: aa.bb = ${obs.aa.bb}`)
})
syncRawState()
function increaseNested() {
obs.aa.bb += 1
syncRawState()
}
function replaceParent() {
obs.aa = { bb: obs.aa.bb + 10 }
syncRawState()
}
function reset() {
obs.aa = { bb: 123 }
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
深度模式会追踪嵌套属性,所以修改 <code>aa.bb</code> 和替换 <code>aa</code> 都会触发
<code>autorun</code>。
</p>
<div class="toolbar">
<button class="btn" @click="increaseNested">
obs.aa.bb += 1
</button>
<button class="btn" @click="replaceParent">
obs.aa = { bb: ... }
</button>
<button class="btn secondary" @click="reset">
reset
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
autorun 次数
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
当前值
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
最近一次追踪值
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panelTitle">
Raw Snapshot
</div>
<pre>{{ rawSnapshot }}</pre>
</div>
<div class="panel">
<div class="panelTitle">
Last Tracked Snapshot
</div>
<pre>{{ trackedSnapshot }}</pre>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="./shared.css"></style> 深度模式会追踪嵌套属性,所以修改 aa.bb 和替换 aa 都会触发 autorun。
{
"aa": {
"bb": 123
}
}{
"aa": {
"bb": 123
}
}- autorun #1: aa.bb = 123
示例代码
import { autorun, observable } from '@formily/reactive'
const obs = observable({
aa: {
bb: 123,
},
})
autorun(() => {
console.log(obs.aa.bb)
})
obs.aa.bb = 321observable.shallow
描述
创建浅劫持响应式对象,也就是只会对目标对象的第一级属性操作响应
签名
interface shallow<T extends object> {
(target: T): T
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, pushLog, useAutorunEffect } from './shared'
const logs = ref<string[]>([])
const rawValue = ref(111)
const trackedValue = ref(111)
const rawSnapshot = ref('')
const trackedSnapshot = ref('')
const runCount = ref(0)
const obs = observable.shallow({
aa: {
bb: 111,
},
})
function syncRawState() {
rawValue.value = obs.aa.bb
rawSnapshot.value = formatValue(obs)
}
useAutorunEffect(() => {
runCount.value += 1
trackedValue.value = obs.aa.bb
trackedSnapshot.value = formatValue(obs)
pushLog(logs, `autorun #${runCount.value}: aa.bb = ${obs.aa.bb}`)
})
syncRawState()
function increaseNested() {
obs.aa.bb += 1
syncRawState()
}
function replaceParent() {
obs.aa = { bb: obs.aa.bb + 10 }
syncRawState()
}
function reset() {
obs.aa = { bb: 111 }
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
浅模式只追踪第一层属性。直接修改 <code>aa.bb</code> 会改变原始值,但不会重新触发
<code>autorun</code>;替换 <code>aa</code> 才会。
</p>
<div class="toolbar">
<button class="btn" @click="increaseNested">
obs.aa.bb += 1
</button>
<button class="btn" @click="replaceParent">
obs.aa = { bb: ... }
</button>
<button class="btn secondary" @click="reset">
reset
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
autorun 次数
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
当前值
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
最近一次追踪值
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panelTitle">
Raw Snapshot
</div>
<pre>{{ rawSnapshot }}</pre>
</div>
<div class="panel">
<div class="panelTitle">
Last Tracked Snapshot
</div>
<pre>{{ trackedSnapshot }}</pre>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="./shared.css"></style> 浅模式只追踪第一层属性。直接修改 aa.bb 会改变原始值,但不会重新触发 autorun;替换 aa 才会。
{
"aa": {
"bb": 111
}
}{
"aa": {
"bb": 111
}
}- autorun #1: aa.bb = 111
示例代码
import { autorun, observable } from '@formily/reactive'
const obs = observable.shallow({
aa: {
bb: 111,
},
})
autorun(() => {
console.log(obs.aa.bb)
})
obs.aa.bb = 222 // 不会响应
obs.aa = { bb: 333 } // 可以响应observable.computed
描述
创建一个计算缓存器
核心概念
computed 可以理解为一个带缓存的 reaction。只要它依赖的 observable 数据没有变化,就会一直复用上一次的计算结果;只有依赖发生变化时才会重新计算。
这也要求 computed 尽量保持纯函数:内部依赖的数据要么是 observable 数据,要么是外部常量。如果依赖的是普通外部变量,外部变量变化不会触发重新计算。
签名
interface computed {
<T extends () => any>(target: T): { value: ReturnType<T> }
<T extends { get?: () => any, set?: (value: any) => void }>(target: T): {
value: ReturnType<T['get']>
}
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, parseNumber, pushLog, useAutorunEffect } from './shared'
const logs = ref<string[]>([])
const rawSnapshot = ref('')
const aaValue = ref(11)
const bbValue = ref(22)
const getterRuns = ref(0)
const autorunRuns = ref(0)
const totalValue = ref(0)
let getterSeed = 0
const obs = observable({
aa: 11,
bb: 22,
})
const total = observable.computed(() => {
getterSeed += 1
getterRuns.value = getterSeed
return obs.aa + obs.bb
})
function syncRawState() {
aaValue.value = obs.aa
bbValue.value = obs.bb
rawSnapshot.value = formatValue(obs)
}
useAutorunEffect(() => {
autorunRuns.value += 1
const nextTotal = total.value
totalValue.value = nextTotal
pushLog(logs, `autorun #${autorunRuns.value}: sum = ${nextTotal}`)
})
syncRawState()
function setAa(event: Event) {
const target = event.target as HTMLInputElement | null
obs.aa = parseNumber(target?.value, obs.aa)
syncRawState()
}
function setBb(event: Event) {
const target = event.target as HTMLInputElement | null
obs.bb = parseNumber(target?.value, obs.bb)
syncRawState()
}
function readTwice() {
const first = total.value
const second = total.value
pushLog(logs, `连续读取两次 computed:${first} / ${second}`)
}
function reset() {
obs.aa = 11
obs.bb = 22
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
修改依赖后会重新计算;如果依赖没变,重复读取 <code>computed.value</code> 会直接命中缓存。
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-computed-aa">aa</label>
<input
id="observable-computed-aa"
class="input"
type="number"
:value="aaValue"
@input="setAa"
>
</div>
<div class="inputGroup">
<label for="observable-computed-bb">bb</label>
<input
id="observable-computed-bb"
class="input"
type="number"
:value="bbValue"
@input="setBb"
>
</div>
</div>
<div class="toolbar">
<button class="btn" @click="readTwice">
read computed twice
</button>
<button class="btn secondary" @click="reset">
reset
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
当前结果
</div>
<div class="value">
{{ totalValue }}
</div>
</div>
<div class="metric">
<div class="label">
getter 次数
</div>
<div class="value">
{{ getterRuns }}
</div>
</div>
<div class="metric">
<div class="label">
autorun 次数
</div>
<div class="value">
{{ autorunRuns }}
</div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panelTitle">
Raw Snapshot
</div>
<pre>{{ rawSnapshot }}</pre>
</div>
<div class="panel">
<div class="panelTitle">
Cache Hint
</div>
<pre>依赖不变时,多次读取 total.value 不会让 getter 次数继续增长。</pre>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="./shared.css"></style> 修改依赖后会重新计算;如果依赖没变,重复读取 computed.value 会直接命中缓存。
{
"aa": 11,
"bb": 22
}依赖不变时,多次读取 total.value 不会让 getter 次数继续增长。
- autorun #1: sum = 33
示例代码
import { autorun, observable } from '@formily/reactive'
const obs = observable({
aa: 11,
bb: 22,
})
const computed = observable.computed(() => obs.aa + obs.bb)
autorun(() => {
console.log(computed.value)
})
obs.aa = 33observable.ref
描述
创建引用劫持响应式对象
签名
interface ref<T extends object> {
(target: T): { value: T }
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { parseNumber, pushLog, useAutorunEffect } from './shared'
const logs = ref<string[]>([])
const rawValue = ref(1)
const trackedValue = ref(1)
const runCount = ref(0)
const numberRef = observable.ref(1)
function syncRawState() {
rawValue.value = numberRef.value
}
useAutorunEffect(() => {
runCount.value += 1
trackedValue.value = numberRef.value
pushLog(logs, `autorun #${runCount.value}: ref.value = ${numberRef.value}`)
})
syncRawState()
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
numberRef.value = parseNumber(target?.value, numberRef.value)
syncRawState()
}
function increment() {
numberRef.value += 1
syncRawState()
}
function reset() {
numberRef.value = 1
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
<code>observable.ref</code> 适合包裹基础值或整块引用。直接修改 <code>value</code> 就会触发响应。
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-ref-value">ref.value</label>
<input
id="observable-ref-value"
class="input"
type="number"
:value="rawValue"
@input="handleInput"
>
</div>
</div>
<div class="toolbar">
<button class="btn" @click="increment">
ref.value += 1
</button>
<button class="btn secondary" @click="reset">
reset
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
autorun 次数
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
当前值
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
最近一次追踪值
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="./shared.css"></style>observable.ref 适合包裹基础值或整块引用。直接修改 value 就会触发响应。
- autorun #1: ref.value = 1
示例代码
import { autorun, observable } from '@formily/reactive'
const ref = observable.ref(1)
autorun(() => {
console.log(ref.value)
})
ref.value = 2observable.box
描述
与 ref 相似,只是读写数据是通过 get/set 方法
签名
interface box<T extends object> {
(target: T): { get: () => T, set: (value: T) => void }
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { parseNumber, pushLog, useAutorunEffect } from './shared'
const logs = ref<string[]>([])
const rawValue = ref(1)
const trackedValue = ref(1)
const runCount = ref(0)
const numberBox = observable.box(1)
function syncRawState() {
rawValue.value = numberBox.get()
}
useAutorunEffect(() => {
runCount.value += 1
trackedValue.value = numberBox.get()
pushLog(logs, `autorun #${runCount.value}: box.get() = ${numberBox.get()}`)
})
syncRawState()
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
numberBox.set(parseNumber(target?.value, numberBox.get()))
syncRawState()
}
function increment() {
numberBox.set(numberBox.get() + 1)
syncRawState()
}
function reset() {
numberBox.set(1)
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
<code>observable.box</code> 和 <code>ref</code> 类似,只是通过 <code>get</code> / <code>set</code>
读写数据。
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-box-value">box.get()</label>
<input
id="observable-box-value"
class="input"
type="number"
:value="rawValue"
@input="handleInput"
>
</div>
</div>
<div class="toolbar">
<button class="btn" @click="increment">
box.set(box.get() + 1)
</button>
<button class="btn secondary" @click="reset">
reset
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
autorun 次数
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
当前值
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
最近一次追踪值
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="./shared.css"></style>observable.box 和 ref 类似,只是通过 get / set 读写数据。
- autorun #1: box.get() = 1
示例代码
import { autorun, observable } from '@formily/reactive'
const box = observable.box(1)
autorun(() => {
console.log(box.get())
})
box.set(2)