observable
Mainly used to create observable objects with different responsive behaviors, and can be used as an annotation to define to mark responsive attributes
Core Idea
observable is the foundation of the reactive model. By creating a subscribable object, @formily/reactive can collect dependencies when a property is read and notify subscribers when a property is written. Internally it is mainly implemented with ES Proxy, so data operations on objects can be intercepted completely.
Besides using the observable APIs directly, you can also build domain models with define and model. Under the hood, they are still combining observable, computed, and action/batch capabilities.
observable/observable.deep
Description
Create deep hijacking responsive objects
Signature
interface observable<T extends object> {
(target: T): T
}
interface deep<T extends object> {
(target: T): T
}Example
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, pushLog, useAutorunEffect } from '../observable/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">
Deep mode tracks nested properties, so mutating <code>aa.bb</code> and replacing
<code>aa</code> will both trigger <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 runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
current value
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
last tracked value
</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">
Run Log
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style> Deep mode tracks nested properties, so mutating aa.bb and replacing aa will both trigger autorun.
{
"aa": {
"bb": 123
}
}{
"aa": {
"bb": 123
}
}- autorun #1: aa.bb = 123
Example Code
import { autorun, observable } from '@formily/reactive'
const obs = observable({
aa: {
bb: 123,
},
})
autorun(() => {
console.log(obs.aa.bb)
})
obs.aa.bb = 321observable.shallow
Description
Create shallow hijacking responsive objects, that is, only respond to the first-level attribute operations of the target object
Signature
interface shallow<T extends object> {
(target: T): T
}Example
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, pushLog, useAutorunEffect } from '../observable/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">
Shallow mode only tracks first-level properties. Mutating <code>aa.bb</code> changes the raw
value, but it does not rerun <code>autorun</code>; replacing <code>aa</code> does.
</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 runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
current value
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
last tracked value
</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">
Run Log
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style> Shallow mode only tracks first-level properties. Mutating aa.bb changes the raw value, but it does not rerun autorun; replacing aa does.
{
"aa": {
"bb": 111
}
}{
"aa": {
"bb": 111
}
}- autorun #1: aa.bb = 111
Example Code
import { autorun, observable } from '@formily/reactive'
const obs = observable.shallow({
aa: {
bb: 111,
},
})
autorun(() => {
console.log(obs.aa.bb)
})
obs.aa.bb = 222 // will not respond
obs.aa = { bb: 333 } // can respondobservable.computed
Description
Create a calculation buffer
Core Idea
computed can be understood as a cached reaction. As long as the observable data it depends on does not change, it keeps reusing the previous calculation result. It only recalculates when its dependencies change.
That also means a computed function should stay pure whenever possible. Its dependencies should be observable data or external constants. If it depends on ordinary external variables, changing those variables will not trigger a recalculation.
Signature
interface computed {
<T extends () => any>(target: T): { value: ReturnType<T> }
<T extends { get?: () => any, set?: (value: any) => void }>(target: T): {
value: ReturnType<T['get']>
}
}Example
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { formatValue, parseNumber, pushLog, useAutorunEffect } from '../observable/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, `read computed twice: ${first} / ${second}`)
}
function reset() {
obs.aa = 11
obs.bb = 22
syncRawState()
}
</script>
<template>
<div class="playground">
<p class="hint">
The getter recalculates when its dependencies change. If <code>aa</code> and <code>bb</code>
stay the same, reading <code>computed.value</code> repeatedly will hit the cache.
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-computed-aa-en">aa</label>
<input
id="observable-computed-aa-en"
class="input"
type="number"
:value="aaValue"
@input="setAa"
>
</div>
<div class="inputGroup">
<label for="observable-computed-bb-en">bb</label>
<input
id="observable-computed-bb-en"
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">
current sum
</div>
<div class="value">
{{ totalValue }}
</div>
</div>
<div class="metric">
<div class="label">
getter runs
</div>
<div class="value">
{{ getterRuns }}
</div>
</div>
<div class="metric">
<div class="label">
autorun runs
</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>If the dependencies do not change, repeated reads reuse the cached value.</pre>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style> The getter recalculates when its dependencies change. If aa and bb stay the same, reading computed.value repeatedly will hit the cache.
{
"aa": 11,
"bb": 22
}If the dependencies do not change, repeated reads reuse the cached value.
- autorun #1: sum = 33
Example Code
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
Description
Create reference hijacking responsive objects
Signature
interface ref<T extends object> {
(target: T): { value: T }
}Example
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { parseNumber, pushLog, useAutorunEffect } from '../observable/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> is useful for primitive values or whole-object references. Mutate
<code>value</code> directly to trigger updates.
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-ref-value-en">ref.value</label>
<input
id="observable-ref-value-en"
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 runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
current value
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
last tracked value
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style>observable.ref is useful for primitive values or whole-object references. Mutate value directly to trigger updates.
- autorun #1: ref.value = 1
Example Code
import { autorun, observable } from '@formily/reactive'
const ref = observable.ref(1)
autorun(() => {
console.log(ref.value)
})
ref.value = 2observable.box
Description
Similar to ref, except that the data is read and written through the get/set method
Signature
interface box<T extends object> {
(target: T): { get: () => T, set: (value: T) => void }
}Example
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { ref } from 'vue'
import { parseNumber, pushLog, useAutorunEffect } from '../observable/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> behaves like <code>ref</code>, except reads and writes go through
<code>get</code> and <code>set</code>.
</p>
<div class="inputRow">
<div class="inputGroup">
<label for="observable-box-value-en">box.get()</label>
<input
id="observable-box-value-en"
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 runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
<div class="metric">
<div class="label">
current value
</div>
<div class="value">
{{ rawValue }}
</div>
</div>
<div class="metric">
<div class="label">
last tracked value
</div>
<div class="value">
{{ trackedValue }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="log in logs" :key="log">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style>observable.box behaves like ref, except reads and writes go through get and set.
- autorun #1: box.get() = 1
Example Code
import { autorun, observable } from '@formily/reactive'
const box = observable.box(1)
autorun(() => {
console.log(box.get())
})
box.set(2)