阿里云主机折上折
  • 微信号
Current Site:Index > The implementation principle of asynchronous rendering

The implementation principle of asynchronous rendering

Author:Chuan Chen 阅读数:30981人阅读 分类: Vue.js

Core Concepts of Asynchronous Rendering

Vue3's asynchronous rendering mechanism is implemented through a Scheduler, with the core goal of optimizing rendering performance. When data changes, Vue does not immediately perform DOM updates but instead places update tasks into a queue, which are processed in batches during the next event loop. This mechanism avoids unnecessary repeated rendering, particularly evident in high-frequency data change scenarios.

const queue = []
let isFlushing = false
const resolvedPromise = Promise.resolve()

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
  }
  if (!isFlushing) {
    isFlushing = true
    resolvedPromise.then(() => {
      let job
      while (job = queue.shift()) {
        job()
      }
      isFlushing = false
    })
  }
}

Reactive System and Rendering Scheduling

Vue3's reactive system uses Proxy to implement data interception. When data changes are detected, it triggers the component's update function. However, the update function does not execute immediately but is pushed into a queue:

class ReactiveEffect {
  run() {
    // Trigger dependency collection
    activeEffect = this
    const result = this.fn()
    activeEffect = undefined
    return result
  }
  
  // Scheduler interface
  scheduler?() {
    queueJob(this)
  }
}

When an effect is marked as requiring scheduling, data changes trigger the scheduler instead of directly executing the run method. This allows multiple synchronous data changes to be merged into a single render.

Implementation Details of the Task Queue

Vue3 internally maintains various queues to handle different types of tasks:

  1. Pre-queue (preQueue): Handles tasks that need to be completed before rendering.
  2. Rendering queue (queue): The main update queue.
  3. Post-queue (postQueue): Handles tasks that need to be executed after rendering.
const queue: SchedulerJob[] = []
let flushIndex = 0

function flushJobs() {
  // 1. Preprocess the pre-queue
  flushPreFlushCbs()
  
  // 2. Sort the main queue
  queue.sort((a, b) => getId(a) - getId(b))
  
  // 3. Execute the main queue
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job) {
        job()
      }
    }
  } finally {
    // 4. Clean up the queue
    flushIndex = 0
    queue.length = 0
    
    // 5. Process the post-queue
    flushPostFlushCbs()
  }
}

Component Update Lifecycle

Asynchronous rendering affects the execution order of the component update lifecycle:

  1. The beforeUpdate hook is executed synchronously before queue processing.
  2. The actual DOM update is deferred to the microtask queue.
  3. The updated hook is executed after queue processing.
const instance = {
  update() {
    // 1. Execute the beforeUpdate hook
    if (instance.beforeUpdate) {
      instance.beforeUpdate()
    }
    
    // 2. Add the rendering task to the queue
    queueJob(() => {
      const nextTree = renderComponent(instance)
      patch(instance._vnode, nextTree)
      instance._vnode = nextTree
      
      // 3. Execute the updated hook
      if (instance.updated) {
        instance.updated()
      }
    })
  }
}

Rendering Priority Control

Vue3 implements priority control by assigning IDs to different tasks:

  • Parent components always update before child components (smaller ID).
  • User-defined watchEffect can specify priority.
  • Transition effects have special priority handling.
function queueJob(job: SchedulerJob) {
  // Calculate priority ID
  const id = (job.id == null ? Infinity : job.id)
  
  // Insert into the queue based on priority
  if (queue.length === 0) {
    queue.push(job)
  } else {
    let i = queue.length - 1
    while (i >= 0 && getId(queue[i]) > id) {
      i--
    }
    queue.splice(i + 1, 0, job)
  }
  
  queueFlush()
}

Special Handling for Suspense Components

Suspense components have special logic in asynchronous rendering:

  1. Asynchronous dependencies are collected by the Suspense instance.
  2. Updates are triggered only after all asynchronous dependencies are resolved.
  3. Fallback content is displayed during the waiting period.
function setupSuspense(props, { slots }) {
  const promises = []
  
  return {
    async setup() {
      // Collect asynchronous dependencies
      const res = await someAsyncOperation()
      if (res.error) {
        promises.push(Promise.reject(res.error))
      } else {
        promises.push(Promise.resolve(res.data))
      }
      
      // Return the rendering function
      return () => {
        if (promises.some(p => p.status !== 'fulfilled')) {
          return slots.fallback()
        }
        return slots.default()
      }
    }
  }
}

Comparison with React's Scheduler

Vue3's scheduler differs significantly from React's scheduler:

Feature Vue3 React
Task Priority Simple numeric ID Lane model
Time Slicing Not supported Supported
Task Interruption/Resumption Not supported Supported
Microtask Usage Heavy usage Limited usage

Performance Optimization Practices

Based on the asynchronous rendering mechanism, various optimizations can be implemented:

  1. Batched State Updates:
// Bad practice
data.value = 1
data.value = 2
data.value = 3

// Optimized practice
batch(() => {
  data.value = 1
  data.value = 2
  data.value = 3
})
  1. Proper Use of nextTick:
import { nextTick } from 'vue'

async function handleClick() {
  // Modify reactive data
  state.count++
  
  // Wait for DOM updates to complete
  await nextTick()
  
  // Manipulate the DOM
  console.log(document.getElementById('count').textContent)
}
  1. Avoid Synchronously Triggering Multiple Computations:
const double = computed(() => count.value * 2)
const triple = computed(() => count.value * 3)

// Synchronous modification triggers two computations
count.value++

// Use effect for batch processing
effect(() => {
  count.value++
  // Now double and triple are computed only once
})

Debugging Asynchronous Rendering Issues

Common asynchronous rendering issues and debugging methods during development:

  1. Unexpected Rendering Order:
import { getCurrentInstance } from 'vue'

function useDebug() {
  const instance = getCurrentInstance()
  onUpdated(() => {
    console.log(`[${instance.type.name}] updated`)
  })
}
  1. Using DevTools Timeline:
  • Enable performance markers in Vue DevTools.
  • View rendering tasks in the "Timeline" panel.
  1. Manually Checking Queue Status:
import { queuePostRenderEffect } from 'vue'

// Check the post-queue
queuePostRenderEffect(() => {
  console.log('Post queue flushed')
})

Custom Scheduler

For advanced scenarios, custom scheduling strategies can be implemented:

import { effect, reactive } from 'vue'

const obj = reactive({ count: 0 })

// Custom scheduler
const myEffect = effect(
  () => {
    console.log(obj.count)
  },
  {
    scheduler(effect) {
      // Use requestAnimationFrame instead of microtasks
      requestAnimationFrame(effect.run.bind(effect))
    }
  }
)

// Modifications trigger the scheduler instead of direct execution
obj.count++

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.