Vue Task Scheduling and Batch Updates
Purpose
Vue's task scheduling system enables batch updates or asynchronous execution of reactive effects. When reactive data changes trigger side effect functions to re-execute, the system provides control over when, how many times, and in what manner these effects execute.
Example
const appState = reactive({
counter: 1
})
effect(() => {
console.log('counter value:', appState.counter)
})
appState.counter++
console.log('execution finished')
Implementing Schedulable Effects
const appState = reactive({
counter: 1
})
effect(() => {
console.log(appState.counter)
}, {
// When counter changes, the scheduler function executes instead
// This causes 'execution finished' to run first due to setTimeout wrapping
scheduler (task) {
// Execute asynchronously
setTimeout(() => {
task()
}, 0)
}
})
appState.counter++
console.log('execution finished')
The scheduler controls when side effects execute. Previously, console.log(appState.counter) would execute immediately after appState.counter++. With the scheduler, changes trigger the scheduler logic instead.
Batch Updates and Asynchronous Execution
Consider this code - how many times will the counter print? 100 or 101?
const appState = reactive({
counter: 1
})
effect(() => {
console.log('counter:', appState.counter)
})
let iterations = 100
while (iterations--) {
appState.counter++
}
For UI rendering, values 2-100 are intermediate states, not final results. For performance reasons, Vue only renders the final value of 101.
Implemantation Strategy
Using schedulability combined with event loop concepts achieves this behavior:
- Each counter change triggers the scheduler, adding the registered affect function to the
pendingTasksqueue. Due to Set's deduplication, only one function instance remains - Using Promise microtask characteristics, when the counter changes 100 times and all synchronous code completes, the then callback executes. At this point, the counter equals 101, and
pendingTaskscontains only one function, resulting in a single 101 output
const appState = reactive({
counter: 1
})
const pendingTasks = new Set()
const promiseRef = Promise.resolve()
let isProcessing = false
const processPendingTasks = () => {
if (isProcessing) {
return
}
isProcessing = true
// Microtask execution
promiseRef.then(() => {
pendingTasks.forEach((task) => task())
}).finally(() => {
// Reset flag after completion
isProcessing = false
})
}
effect(() => {
console.log('counter:', appState.counter)
}, {
scheduler (task) {
// Add effect function to queue on each data change
pendingTasks.add(task)
// Attempt to flush tasks, but microtasks execute once per event loop
// Even with 100 counter changes, only one effect executes
processPendingTasks()
}
})
let iterations = 100
while (iterations--) {
appState.counter++
}