action
Core Idea
action can be seen as a batch that does not collect dependencies. It still groups a set of writes into a single transaction, but reads inside the action do not participate in dependency tracking, which makes it a better fit for imperative update logic.
Description
Define a batch action. The only difference with batch is that dependencies cannot be collected inside an action
Relationship with batch
Interactive Comparison
<script setup lang="ts">
import { action, autorun, observable } from '@formily/reactive'
import { onBeforeUnmount, ref } from 'vue'
import { pushLog } from '../observable/shared'
const obs = observable({
aa: 0,
bb: 0,
})
const runCount = ref(0)
const rawAa = ref(obs.aa)
const rawBb = ref(obs.bb)
const logs = ref<string[]>([])
const handler = action.bound(() => {
obs.aa += 1
obs.bb += 1
})
const dispose = autorun(() => {
runCount.value += 1
rawAa.value = obs.aa
rawBb.value = obs.bb
pushLog(logs, `autorun #${runCount.value}: aa = ${obs.aa}, bb = ${obs.bb}`)
})
function runDirect() {
obs.aa += 1
obs.bb += 1
rawAa.value = obs.aa
rawBb.value = obs.bb
}
function runAction() {
handler()
rawAa.value = obs.aa
rawBb.value = obs.bb
}
function reset() {
obs.aa = 0
obs.bb = 0
rawAa.value = 0
rawBb.value = 0
runCount.value = 0
logs.value = []
}
onBeforeUnmount(() => dispose())
</script>
<template>
<div class="playground">
<p class="hint">
Compare direct writes with <code>action.bound</code>: both update the data, but
<code>action</code> groups the writes into one transaction.
</p>
<div class="toolbar">
<button class="btn" @click="runDirect">
direct writes
</button>
<button class="btn" @click="runAction">
action.bound()
</button>
<button class="btn secondary" @click="reset">
reset display
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
aa / bb
</div>
<div class="value">
{{ rawAa }} / {{ rawBb }}
</div>
</div>
<div class="metric">
<div class="label">
autorun runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="(log, index) in logs" :key="`${index}-${log}`">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style> Compare direct writes with action.bound: both update the data, but action groups the writes into one transaction.
- autorun #1: aa = 0, bb = 0
Example Code
When you want to package update logic into a reusable method, action.bound gives you batching behavior similar to batch:
import { action, autorun, observable } from '@formily/reactive'
const obs = observable({})
const handler = action.bound(() => {
obs.aa = 123
obs.bb = 321
})
autorun(() => {
console.log(obs.aa, obs.bb)
})
handler()Only one extra reaction is triggered, even if handler contains more write operations.
Signature
interface action {
<T>(callback?: () => T): T // In-situ action
scope: <T>(callback?: () => T) => T // In-situ local action
bound: <T extends (...args: any[]) => any>(callback: T, context?: any) => T // High-level binding
}Example
<script setup lang="ts">
import { action, autorun, observable } from '@formily/reactive'
import { onBeforeUnmount, ref } from 'vue'
import { pushLog } from '../observable/shared'
const obs = observable({
count: 0,
})
const countValue = ref(obs.count)
const runCount = ref(0)
const logs = ref<string[]>([])
const increase = action.bound(() => {
obs.count += 1
})
const dispose = autorun(() => {
runCount.value += 1
countValue.value = obs.count
pushLog(logs, `autorun #${runCount.value}: count = ${obs.count}`)
})
function increment() {
increase()
}
function reset() {
obs.count = 0
countValue.value = 0
runCount.value = 0
logs.value = []
}
onBeforeUnmount(() => dispose())
</script>
<template>
<div class="playground">
<p class="hint">
<code>action.bound</code> is useful for packaging imperative update logic into reusable
methods. Each click on <code>increase()</code> produces only one extra reaction.
</p>
<div class="toolbar">
<button class="btn" @click="increment">
increase()
</button>
<button class="btn secondary" @click="reset">
reset display
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
count
</div>
<div class="value">
{{ countValue }}
</div>
</div>
<div class="metric">
<div class="label">
autorun runs
</div>
<div class="value">
{{ runCount }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="(log, index) in logs" :key="`${index}-${log}`">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style>action.bound is useful for packaging imperative update logic into reusable methods. Each click on increase() produces only one extra reaction.
- autorun #1: count = 0
Example Code
import { action, observable } from '@formily/reactive'
const obs = observable({
count: 0,
})
const increase = action.bound(() => {
obs.count++
})
increase()