Welcome to Aaron Blog! en and jp version is now updating! 🎉

Watch & Computed

Exploring the differences between Watch and Computed, and examining their source code

Loading comments...

Which executes faster: Watch or Computed?

A friend recently got asked this question in an interview - super interesting! I want to dive deep into Vue's source code to study the differences between Watch and Computed and see which one is actually faster!

First, Let's Understand Vue's Reactivity System

Before we can compare them, we need to understand how Vue's reactivity works. Simply put, Vue 3 uses Proxy to listen for data changes, and when data updates, the page updates accordingly.

Proxy basically helps Vue intercept object read (get) and modify (set) operations. Let's look at a simplified implementation from the official docs:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key) // Key point: track who is using this property
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key) // Key point: notify dependencies that they need to update
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value') // Same tracking mechanism
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value') // Same trigger mechanism
    }
  }
  return refObject
}

There are two key concepts here that are fundamental to understanding Watch and Computed:

  • track: Records "who" depends on this property (like component render functions or computed properties)
  • trigger: When a property changes, notifies all dependents to update

Looking at the actual source code implementation, you'll find the same track and trigger mechanisms:

reactiveProxy Handler

Understanding the reactivity system helps us dive deeper into Watch and Computed!

What is Watch?

Simply put, Watch is a "tracker." You tell it which data to watch, and when that data changes, it executes the callback function you provided.

Watch characteristics:

  • Explicitly tracks specific data sources (can be ref, reactive objects, or functions)
  • Executes callbacks when data changes, usually providing new and old values
  • Suitable for executing "side effects" like async operations or complex logic

Let's see how Vue's source code implements Watch:

export function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ,
): WatchHandle {
  // ... omitting earlier parts ...

  // Core part: Create ReactiveEffect to handle watching
  effect = new ReactiveEffect(getter) // Key point: when triggered, schedules user's callback function

  // ... omitting middle parts ...

  // Initial execution
  if (cb) {
    if (immediate) {
      job(true) // If immediate: true is set, execute callback immediately
    } else {
      oldValue = effect.run() // Otherwise run once to get initial value, don't execute callback
    }
  } else if (scheduler) {
    scheduler(job.bind(null, true), true)
  } else {
    effect.run() // watchEffect case
  }

  // ... omitting later parts ...
}

From the source code, we can see that Watch's core is creating a ReactiveEffect instance. When dependencies change, it executes the callback function you provided. This is why Watch is suitable for handling "side effects" - its design purpose is to execute operations when data changes, not to compute new values.

What is Computed?

Computed "calculates" new data based on existing data. It's like a smart assistant that remembers calculation results until the dependent data changes.

Computed characteristics:

  • Calculates new values based on existing reactive data
  • Has caching mechanism to avoid redundant calculations
  • Suitable for simple operations, keeping templates cleaner

Let's look at the key implementation of Computed in the source code:

// Part of ComputedRefImpl class methods

// When notifying dependencies to update, doesn't immediately recalculate
notify(): true | void {
  this.flags |= EffectFlags.DIRTY // Key point: only marks as "dirty", doesn't calculate immediately
  if (
    !(this.flags & EffectFlags.NOTIFIED) &&
    // Avoid infinite self-recursion
    activeSub !== this
  ) {
    batch(this, true) // Batch process updates
    return true
  } else if (__DEV__) {
    // Development environment warning
  }
}

// Method to get value
get value(): T {
  const link = __DEV__
    ? this.dep.track({
        target: this,
        type: TrackOpTypes.GET,
        key: 'value',
      })
    : this.dep.track()
  refreshComputed(this) // Key point: only recalculates when accessed and marked as dirty
  // Sync version
  if (link) {
    link.version = this.dep.version
  }
  return this._value // Return calculation result
}

From the source code, we can clearly see Computed's two core features:

  1. When dependencies change, it only marks as "dirty" (EffectFlags.DIRTY), doesn't immediately recalculate
  2. Only when the value is actually needed (someone reads .value) does it decide whether to recalculate based on the "dirty" flag

This is the secret to Computed's efficiency - lazy evaluation plus caching mechanism, avoiding unnecessary redundant calculations, especially suitable for scenarios where calculation results are used multiple times.

Watch vs Computed: Detailed Comparison

Now that we've seen the source code implementations, let's summarize the core differences between Watch and Computed:

FeatureComputedWatch
PurposeCalculate new dataExecute side effects
Return ValueHas return valueNo return value
CachingHas caching mechanismExecutes every time
Calculation TimingCalculated when needed (lazy evaluation)Executes when data changes
Side EffectsNot suitable (anti-pattern)Designed for this
Async OperationsNot suitablePerfectly suitable
Programming StyleDeclarativeImperative
Performance ConsiderationAutomatically efficient (caching)Developer needs to optimize manually

These differences directly reflect their implementation approaches in the source code: Computed focuses on efficiently generating and caching values, while Watch focuses on executing callbacks at the right time.

Practical Application Guidelines

Based on source code analysis, here are some practical guidelines:

When to use Computed:

  • When you need to calculate new data from other data
  • When calculation results will be used multiple times (leveraging cache for performance)
  • When you want to make templates cleaner by moving complex logic out
  • When you need to use the same calculation logic in multiple places

When to use Watch:

  • When you need to call APIs when data changes
  • When executing operations with "side effects"
  • When you need to implement debouncing or throttling (watch supports options like deep and immediate)
  • When you need to compare values before and after data changes (watch callback provides new and old values)
  • When you need to monitor multiple data sources and execute operations when any one changes

Who's Faster? The Source Code's Answer

Back to our original question: At the source code level, Computed usually executes faster. Why? Let's find the answer in the source code:

  1. Computed has caching mechanism: From the source code, we can see that Computed uses refreshComputed(this) to determine whether recalculation is needed. It only recalculates when:
    • Dependent data has changed (marked as "dirty")
    • Someone actually accesses this computed property
  2. Watch has no caching: Watch's callback executes every time dependencies change, with no skip mechanism (unless you implement it yourself)

But in reality, the speed of both depends on your application scenario:

  • For calculation results that need to be accessed multiple times, Computed is definitely faster (thanks to its caching mechanism)
  • For async operations or complex side effects, Watch is more suitable (though not necessarily faster)

Best Practices Revealed by Source Code

From Vue's source code design, we can learn an important principle: Consider using Computed first (declarative), and only use Watch when you truly need side effects or async operations (imperative).

This not only aligns with Vue's design philosophy but also gives your application better performance and maintainability. The carefully designed caching mechanism in Vue's source code for Computed not only improves performance but also "prevents unnecessary downstream updates," which is especially important in complex applications.

Choose the right tool to make your Vue application more efficient!

Loading comments...