batch
核心概念
基于 Proxy 的响应式系统会把每次原子写操作都视为一次独立更新。如果在一个流程里连续修改多个 observable 属性,Reaction 会被重复触发,造成不必要的计算。
batch 用来把一组更新合并成一次派发,在批量修改数据时可以显著减少 Reaction 的执行次数。
描述
定义批量操作,内部可以收集依赖
为什么需要 batch
交互对比
<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">
这里同一个 <code>handler()</code> 会连续写入两个属性。直接执行会让
<code>autorun</code> 重跑两次;包进 <code>batch</code> 后只会多触发一次。
</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 次数
</div>
<div class="value">
{{ runCount }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
运行日志
</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> 这里同一个 handler() 会连续写入两个属性。直接执行会让 autorun 重跑两次;包进 batch 后只会多触发一次。
aa / bb
0 / 0
autorun 次数
1
运行日志
- autorun #1: aa = 0, bb = 0
查看源码
如果不做批处理,连续写入多个属性会导致多次重复响应:
ts
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()上面的示例会打印 3 次:autorun 初始化一次,obs.aa 赋值一次,obs.bb 赋值一次。原子操作越多,重复执行次数越多。
使用 batch 后,整组更新只会触发一次额外响应:
ts
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()
})签名
ts
interface batch {
<T>(callback?: () => T): T // 原地batch
scope: <T>(callback?: () => T) => T // 原地局部batch
bound: <T extends (...args: any[]) => any>(callback: T, context?: any) => T // 高阶绑定
endpoint: (callback?: () => void) => void // 注册批量执行结束回调
}用例
<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">
这个示例复现文档里的 <code>batch</code> + <code>batch.scope</code> 组合。虽然内部做了
多次写入,但最终只会向外派发一次更新。
</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 次数
</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">
运行日志
</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> 这个示例复现文档里的 batch + batch.scope 组合。虽然内部做了 多次写入,但最终只会向外派发一次更新。
autorun 次数
1
Current Values
{} 运行日志
- autorun #1: {}
查看源码
示例代码
ts
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'
})