Batch Updates
Função batch() para Agrupar Atualizações
Section titled “Função batch() para Agrupar Atualizações”A função batch() agrupa múltiplas atualizações de estado, notificando watchers apenas uma vez ao final do batch em vez de notificar após cada set().
Assinatura
Section titled “Assinatura”function batch(fn: () => void): voidParâmetros:
fn: Função que contém as atualizações de estado a serem agrupadas
Retorno: void
Uso Básico
Section titled “Uso Básico”import { createState, batch } from '@ezbug/slash'
const count = createState(0)const name = createState('Alice')
count.watch(() => console.log('Count changed'))name.watch(() => console.log('Name changed'))
// Sem batch: 2 notificaçõescount.set(5) // Log: "Count changed"name.set('Bob') // Log: "Name changed"
// Com batch: 2 notificações agrupadasbatch(() => { count.set(10) // Não notifica ainda name.set('Charlie') // Não notifica ainda}) // Notifica ambos aqui// Log: "Count changed"// Log: "Name changed"Implementação: src/batch.ts
Como Funciona e Quando Usar
Section titled “Como Funciona e Quando Usar”Como Funciona
Section titled “Como Funciona”- Início do Batch:
batch()marca o contexto global como “batching” - Acúmulo: Cada
state.set()registra um update mas não notifica watchers - Fim do Batch: Ao finalizar a função, todos os watchers são notificados uma vez
const state = createState({ count: 0, name: 'John' })
state.watch((value) => { console.log('State changed:', value)})
batch(() => { state.set({ count: 1, name: 'John' }) // Registrado state.set({ count: 2, name: 'John' }) // Registrado state.set({ count: 3, name: 'Jane' }) // Registrado})// Log: "State changed: { count: 3, name: 'Jane' }" (apenas uma vez)Estado Interno
Section titled “Estado Interno”Batch mantém um contador de updates pendentes:
interface BatchContext { status: 'IDLE' | { type: 'BATCHING', pendingUpdates: number }}Fluxo:
batch()called → status = BATCHING, pendingUpdates = 0state.set()→ pendingUpdates++state.set()→ pendingUpdates++- Batch ends → Notifica watchers se pendingUpdates > 0 → status = IDLE
Implementação Core: src/batch-core.ts
Quando Usar
Section titled “Quando Usar”Use batch() quando você precisa atualizar múltiplos states ou o mesmo state várias vezes:
1. Atualizar Múltiplos States
Section titled “1. Atualizar Múltiplos States”const user = createState({ name: '', age: 0 })const isLoading = createState(false)const error = createState<string | null>(null)
// ❌ Sem batch: 3 notificações separadasconst loadUser = async (id: number) => { isLoading.set(true) error.set(null)
try { const data = await fetchUser(id) user.set(data) } catch (err) { error.set(err.message) } finally { isLoading.set(false) }}
// ✅ Com batch: 1 ou 2 notificações (início + fim/erro)const loadUser = async (id: number) => { batch(() => { isLoading.set(true) error.set(null) })
try { const data = await fetchUser(id) batch(() => { user.set(data) isLoading.set(false) }) } catch (err) { batch(() => { error.set(err.message) isLoading.set(false) }) }}2. Loops com Múltiplas Atualizações
Section titled “2. Loops com Múltiplas Atualizações”const items = createState<number[]>([])
// ❌ Sem batch: 1000 notificaçõesfor (let i = 0; i < 1000; i++) { items.set([...items.get(), i])}
// ✅ Com batch: 1 notificaçãobatch(() => { for (let i = 0; i < 1000; i++) { items.set([...items.get(), i]) }})
// ✅ Ainda melhor: Acumular em variável localconst newItems = []for (let i = 0; i < 1000; i++) { newItems.push(i)}items.set(newItems) // 1 notificação, sem batch necessário3. Inicialização de Múltiplos States
Section titled “3. Inicialização de Múltiplos States”const formState = createState({ email: '', password: '' })const validationErrors = createState({})const isSubmitting = createState(false)
const initializeForm = () => { batch(() => { formState.set({ email: '', password: '' }) validationErrors.set({}) isSubmitting.set(false) })}4. Sincronização de States Dependentes
Section titled “4. Sincronização de States Dependentes”const celsius = createState(0)const fahrenheit = createState(32)
const setCelsius = (value: number) => { batch(() => { celsius.set(value) fahrenheit.set((value * 9/5) + 32) })}Quando NÃO Usar
Section titled “Quando NÃO Usar”- Single update: Não há benefício em usar batch para um único
set() - Updates assíncronos:
batch()é síncrono, não funciona com async/await
// ❌ Batch não funciona aqui (assíncrono)batch(async () => { const data = await fetchData() state.set(data) // Executado APÓS batch finalizar})
// ✅ Corretoconst data = await fetchData()state.set(data)Performance Optimization
Section titled “Performance Optimization”Medindo Performance
Section titled “Medindo Performance”const state = createState(0)
state.watch(() => { // Simulando operação custosa console.time('render') // ... heavy DOM updates console.timeEnd('render')})
// Sem batch: 100 rendersconsole.time('without-batch')for (let i = 0; i < 100; i++) { state.set(i)}console.timeEnd('without-batch')
// Com batch: 1 renderconsole.time('with-batch')batch(() => { for (let i = 0; i < 100; i++) { state.set(i) }})console.timeEnd('with-batch')Benchmarks Típicos
Section titled “Benchmarks Típicos”| Operação | Sem Batch | Com Batch | Ganho |
|---|---|---|---|
| 100 updates | 100ms | 1ms | 100x |
| 1000 updates | 1000ms | 1ms | 1000x |
| 10 states, 10 updates cada | 100ms | 10ms | 10x |
Nota: Ganhos reais dependem da complexidade dos watchers e do DOM.
Otimizações Automáticas
Section titled “Otimizações Automáticas”Slash já otimiza internamente:
- Deep Equality: Não notifica se valor não mudou
- Lazy Evaluation: Props reativas são avaliadas apenas quando necessário
- Granular Updates: Apenas elementos afetados são atualizados
Batch adiciona uma camada extra de otimização para cenários específicos.
Exemplos Práticos
Section titled “Exemplos Práticos”Exemplo 1: Form com Múltiplos Campos
Section titled “Exemplo 1: Form com Múltiplos Campos”import { createState, batch, html, render } from '@ezbug/slash'
interface FormData { name: string email: string age: number}
const form = createState<FormData>({ name: '', email: '', age: 0})
const resetForm = () => { batch(() => { form.set({ name: '', email: '', age: 0 }) })}
const loadUserData = (userId: number) => { // Simular fetch const userData = { name: 'Alice', email: 'alice@example.com', age: 30 }
batch(() => { form.set(userData) })}
const FormComponent = () => html` <form> <input type="text" placeholder="Name" value=${form.get().name} oninput=${(e: Event) => form.set({ ...form.get(), name: (e.target as HTMLInputElement).value }) } /> <input type="email" placeholder="Email" value=${form.get().email} oninput=${(e: Event) => form.set({ ...form.get(), email: (e.target as HTMLInputElement).value }) } /> <input type="number" placeholder="Age" value=${form.get().age} oninput=${(e: Event) => form.set({ ...form.get(), age: Number((e.target as HTMLInputElement).value) }) } /> <button type="button" onclick=${resetForm}>Reset</button> <button type="button" onclick=${() => loadUserData(1)}>Load User</button> </form>`
render(FormComponent(), '#app')Exemplo 2: Lista com Filtros
Section titled “Exemplo 2: Lista com Filtros”import { createState, batch, html, render } from '@ezbug/slash'
interface Todo { id: number text: string completed: boolean}
const todos = createState<Todo[]>([])const filter = createState<'all' | 'active' | 'completed'>('all')const searchQuery = createState('')
const filteredTodos = createState<Todo[]>([])
// Recomputar filteredTodos quando qualquer dependência mudaconst updateFilteredTodos = () => { const allTodos = todos.get() const currentFilter = filter.get() const query = searchQuery.get().toLowerCase()
let result = allTodos
// Aplicar filtro if (currentFilter === 'active') { result = result.filter(t => !t.completed) } else if (currentFilter === 'completed') { result = result.filter(t => t.completed) }
// Aplicar busca if (query) { result = result.filter(t => t.text.toLowerCase().includes(query)) }
filteredTodos.set(result)}
// Watch all dependenciestodos.watch(updateFilteredTodos)filter.watch(updateFilteredTodos)searchQuery.watch(updateFilteredTodos)
const addTodo = (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false }
batch(() => { todos.set([...todos.get(), newTodo]) // filteredTodos será atualizado automaticamente via watcher })}
const setFilter = (newFilter: 'all' | 'active' | 'completed') => { batch(() => { filter.set(newFilter) // filteredTodos será atualizado automaticamente })}
const TodoApp = () => html` <div> <input type="text" placeholder="Search..." oninput=${(e: Event) => searchQuery.set((e.target as HTMLInputElement).value)} /> <div> <button onclick=${() => setFilter('all')}>All</button> <button onclick=${() => setFilter('active')}>Active</button> <button onclick=${() => setFilter('completed')}>Completed</button> </div> <ul> ${filteredTodos.get().map(todo => html` <li>${todo.text}</li> `)} </ul> </div>`
render(TodoApp(), '#app')Exemplo 3: Data Fetching com Loading States
Section titled “Exemplo 3: Data Fetching com Loading States”import { createState, batch, html, render } from '@ezbug/slash'
interface User { id: number name: string email: string}
const user = createState<User | null>(null)const isLoading = createState(false)const error = createState<string | null>(null)
const fetchUser = async (id: number) => { batch(() => { isLoading.set(true) error.set(null) })
try { const response = await fetch(`https://api.example.com/users/${id}`) if (!response.ok) throw new Error('Failed to fetch')
const data = await response.json()
batch(() => { user.set(data) isLoading.set(false) }) } catch (err) { batch(() => { error.set((err as Error).message) isLoading.set(false) }) }}
const UserProfile = () => { const currentUser = user.get() const loading = isLoading.get() const errorMsg = error.get()
if (loading) { return html`<div>Loading...</div>` }
if (errorMsg) { return html`<div class="error">Error: ${errorMsg}</div>` }
if (!currentUser) { return html`<div>No user loaded</div>` }
return html` <div> <h1>${currentUser.name}</h1> <p>${currentUser.email}</p> </div> `}
render(html` <div> ${UserProfile()} <button onclick=${() => fetchUser(1)}>Load User 1</button> </div>`, '#app')Exemplo 4: Animações com Múltiplos Estados
Section titled “Exemplo 4: Animações com Múltiplos Estados”import { createState, batch, html, render } from '@ezbug/slash'
const position = createState({ x: 0, y: 0 })const rotation = createState(0)const scale = createState(1)
const animateElement = () => { let frame = 0
const animate = () => { frame++
batch(() => { position.set({ x: Math.sin(frame * 0.05) * 100, y: Math.cos(frame * 0.05) * 100 }) rotation.set(frame * 2) scale.set(1 + Math.sin(frame * 0.1) * 0.5) })
requestAnimationFrame(animate) }
animate()}
const AnimatedElement = () => html` <div style=${{ position: 'absolute', left: '50%', top: '50%', transform: ` translate(${position.get().x}px, ${position.get().y}px) rotate(${rotation.get()}deg) scale(${scale.get()}) `, width: '50px', height: '50px', backgroundColor: 'blue' }} ></div>`
render(AnimatedElement(), '#app')animateElement()Integração com State Management
Section titled “Integração com State Management”Batch é Automático em Alguns Casos
Section titled “Batch é Automático em Alguns Casos”Slash integra batch automaticamente no sistema de estado:
// state.ts (interno)const set = (payload: S) => { // ...
if (isInBatch()) { __recordBatchUpdate() // Apenas registra } else { _notifyHandlers(deepClone(_state)) // Notifica imediatamente }}Implementação: src/state.ts
Verificar se Está em Batch
Section titled “Verificar se Está em Batch”import { isInBatch } from '@ezbug/slash'
if (isInBatch()) { console.log('Currently batching updates')}Próximos Passos
Section titled “Próximos Passos”Agora que você domina batch updates, explore:
- Componentes - Criar componentes reutilizáveis
- Performance e Best Practices - Otimizações avançadas
- Router - Roteamento com state management