batch
Core Idea
In a Proxy-based reactive system, each atomic write is treated as an individual update. If multiple observable properties are changed within the same flow, Reaction can be triggered repeatedly and waste work.
batch merges a group of updates into a single dispatch, which helps reduce unnecessary reaction executions during transactional updates.
Description
Define batch operations, internal dependencies can be collected
Why batch matters
Interactive Comparison
<script setup lang="ts">
import { autorun, batch, observable } from '@formily/reactive'
import { onBeforeUnmount, ref } from 'vue'
import { pushLog } from '../observable/shared'
const obs = observable({
aa: 0,
bb: 0,
})
const rawAa = ref(obs.aa)
const rawBb = ref(obs.bb)
const runCount = ref(0)
const logs = ref<string[]>([])
let disposeCurrent: null | (() => void) = null
function syncRawState() {
rawAa.value = obs.aa
rawBb.value = obs.bb
}
function start() {
disposeCurrent = autorun(() => {
runCount.value += 1
rawAa.value = obs.aa
rawBb.value = obs.bb
pushLog(logs, `autorun #${runCount.value}: aa = ${obs.aa}, bb = ${obs.bb}`)
})
}
function stop() {
disposeCurrent?.()
disposeCurrent = null
}
function handler() {
obs.aa += 1
obs.bb += 1
}
function runDirect() {
handler()
syncRawState()
}
function runBatched() {
batch(() => {
handler()
})
syncRawState()
}
function reset() {
stop()
obs.aa = 0
obs.bb = 0
rawAa.value = 0
rawBb.value = 0
runCount.value = 0
logs.value = []
start()
}
start()
onBeforeUnmount(() => stop())
</script>
<template>
<div class="playground">
<p class="hint">
The same <code>handler()</code> writes two properties in sequence. Running it directly makes
<code>autorun</code> rerun twice; wrapping it in <code>batch</code> produces only one extra
rerun.
</p>
<div class="toolbar">
<button class="btn" @click="runDirect">
run handler()
</button>
<button class="btn" @click="runBatched">
batch(handler)
</button>
<button class="btn secondary" @click="reset">
reset
</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> The same handler() writes two properties in sequence. Running it directly makes autorun rerun twice; wrapping it in batch produces only one extra rerun.
- autorun #1: aa = 0, bb = 0
Without batching, changing several properties in sequence can trigger repeated reactions:
import { autorun, observable } from '@formily/reactive'
const obs = observable({})
function handler() {
obs.aa = 123
obs.bb = 321
}
autorun(() => {
console.log(obs.aa, obs.bb)
})
handler()This example prints 3 times: once for the initial autorun, once for the obs.aa assignment, and once for the obs.bb assignment. The more atomic updates you have, the more repeated work you create.
With batch, the whole update group produces only one extra reaction:
import { autorun, batch, observable } from '@formily/reactive'
const obs = observable({})
function handler() {
obs.aa = 123
obs.bb = 321
}
autorun(() => {
console.log(obs.aa, obs.bb)
})
batch(() => {
handler()
})Signature
interface batch {
<T>(callback?: () => T): T // In-place batch
scope: <T>(callback?: () => T) => T // In-situ local batch
bound: <T extends (...args: any[]) => any>(callback: T, context?: any) => T // High-level binding
endpoint: (callback?: () => void) => void // Register batch endpoint callback
}Example
<script setup lang="ts">
import { autorun, batch, observable } from '@formily/reactive'
import { onBeforeUnmount, ref } from 'vue'
import { formatValue, pushLog } from '../observable/shared'
const obs = observable<Record<string, unknown>>({})
const runCount = ref(0)
const snapshot = ref('{}')
const logs = ref<string[]>([])
let disposeCurrent: null | (() => void) = null
function syncSnapshot() {
snapshot.value = formatValue({
aa: obs.aa,
bb: obs.bb,
cc: obs.cc,
dd: obs.dd,
})
}
function start() {
disposeCurrent = autorun(() => {
runCount.value += 1
syncSnapshot()
pushLog(logs, `autorun #${runCount.value}: ${snapshot.value.replace(/\s+/g, ' ')}`)
})
}
function stop() {
disposeCurrent?.()
disposeCurrent = null
}
function runExample() {
batch(() => {
batch.scope(() => {
obs.aa = 123
})
batch.scope(() => {
obs.cc = 'ccccc'
})
obs.bb = 321
obs.dd = 'dddd'
})
syncSnapshot()
}
function reset() {
stop()
obs.aa = undefined
obs.bb = undefined
obs.cc = undefined
obs.dd = undefined
runCount.value = 0
logs.value = []
snapshot.value = '{}'
start()
}
start()
onBeforeUnmount(() => stop())
</script>
<template>
<div class="playground">
<p class="hint">
This reproduces the <code>batch</code> + <code>batch.scope</code> example from the docs.
Even though several writes happen inside, only one update is flushed outward.
</p>
<div class="toolbar">
<button class="btn" @click="runExample">
run batch example
</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>
<div class="panels">
<div class="panel">
<div class="panelTitle">
Current Values
</div>
<pre>{{ snapshot }}</pre>
</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> This reproduces the batch + batch.scope example from the docs. Even though several writes happen inside, only one update is flushed outward.
{}- autorun #1: {}
Example Code
import { autorun, batch, observable } from '@formily/reactive'
const obs = observable({})
autorun(() => {
console.log(obs.aa, obs.bb, obs.cc, obs.dd)
})
batch(() => {
batch.scope(() => {
obs.aa = 123
})
batch.scope(() => {
obs.cc = 'ccccc'
})
obs.bb = 321
obs.dd = 'dddd'
})