watch与watchEffect的实现

3 阅读10分钟

在前几篇文章中,我们逐步构建起了 Vue3 响应式系统的完整图景:从 reactive/ref 创建响应式数据,到 track 收集依赖,trigger 触发更新,再到 computed 实现懒计算。今天,我们将探索响应式系统的最后一个核心 API——watchwatchEffect,看看它们如何监听数据变化并执行回调。

前言:为什么需要 watch?

在 Vue3 中,我们有多种方式响应数据变化:

  1. computed:派生数据
const double = computed(() => count.value * 2);
  1. 渲染函数:自动响应
render();
  1. effect:底层副作用
effect(() => {
  console.log(count.value);
});
  1. watch/watchEffect:监听变化执行回调
watchEffect(() => {
  console.log(count.value); // 立即执行,自动收集依赖
});

watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`); // 懒执行,需要指定源
});
API执行时机适用场景特点
computed依赖变化时派生数据缓存结果,惰性计算
render数据变化时视图更新自动触发,不可控
effect立即执行底层副作用最基础,需手动管理
watchEffect立即执行+自动追踪需要自动收集依赖简洁但不够灵活
watch懒执行+指定源需要精确控制灵活,可获取新旧值

其中,watchwatchEffect 的核心区别在于:

  • watchEffect:立即执行,会自动收集所有依赖,不需要指定要监听的数据
  • watch:懒执行,需要明确指定监听的数据源,可以获取新旧值

watchEffect 的实现

核心原理

watchEffect 本质上是 effect 的一个封装,但它有几个特点:

  • 立即执行一次(收集依赖)
  • 自动跟踪所有响应式依赖
  • 依赖变化时重新执行

watchEffect 的简化版本

/**
 * watchEffect 最小实现
 * @param {Function} effect - 要执行的副作用函数
 * @returns {Function} stop - 停止监听的函数
 */
function watchEffect(effect) {
  // 创建一个 ReactiveEffect,传入调度器
  const _effect = new ReactiveEffect(effect, () => {
    // 调度器:当依赖变化时,触发 effect 重新运行
    _effect.run();
  });
  
  // 立即执行一次,收集依赖
  _effect.run();
  
  // 返回停止监听的函数
  return () => {
    _effect.stop();
  };
}

完整实现

/**
 * 完整的 watchEffect 实现
 * 包含:调度控制、停止监听、清理机制、错误处理、调试钩子
 */
function watchEffect(effect, options = {}) {
  // 1. 参数标准化与默认值
  const {
    flush = 'pre',      // 执行时机:pre/post/sync
    onTrack,            // 调试钩子:依赖追踪时
    onTrigger,          // 调试钩子:触发更新时
    onStop              // 停止回调
  } = options;
  
  // 2. 清理函数管理
  let cleanup;
  function onInvalidate(fn) {
    cleanup = fn;
  }
  
  // 3. 包装用户 effect,注入清理能力
  const wrappedEffect = () => {
    // 执行前清理
    if (cleanup) {
      cleanup();
    }
    
    // 执行用户 effect,传入清理注册函数
    effect(onInvalidate);
  };
  
  // 4. 创建调度器
  const scheduler = createScheduler(flush, () => {
    _effect.run();
  });
  
  // 5. 创建 ReactiveEffect
  const _effect = new ReactiveEffect(wrappedEffect, scheduler);
  
  // 6. 附加调试钩子
  if (onTrack) _effect.onTrack = onTrack;
  if (onTrigger) _effect.onTrigger = onTrigger;
  
  // 7. 立即执行收集依赖
  _effect.run();
  
  // 8. 返回增强的停止函数
  const stop = () => {
    _effect.stop();
    if (cleanup) {
      cleanup();
    }
    onStop?.();
  };
  
  return stop;
}

/**
 * 创建调度器
 * @param {string} flush - 执行时机
 * @param {Function} job - 要执行的任务
 */
function createScheduler(flush, job) {
  // 同步执行
  if (flush === 'sync') {
    return job;
  }
  
  // 异步队列
  const queue = new Set();
  let isFlushing = false;
  
  const queueJob = () => {
    queue.add(job);
    
    if (!isFlushing) {
      isFlushing = true;
      
      if (flush === 'post') {
        // 组件更新后执行
        Promise.resolve().then(flushJobs);
      } else {
        // 'pre' 或默认:组件更新前执行
        queueMicrotask(flushJobs);
      }
    }
  };
  
  function flushJobs() {
    try {
      queue.forEach(j => j());
    } finally {
      queue.clear();
      isFlushing = false;
    }
  }
  
  return queueJob;
}

源码原理分析

Vue3 源码中 watchEffect 的关键设计:

  1. 为什么使用 ReactiveEffect 而不是直接调用 effect?
    • ReactiveEffect 提供了完整的生命周期管理
    • 支持暂停、恢复、停止等操作
    • 内置调度器机制,实现批量更新
  2. 依赖收集的时机:
    • 首次执行:收集所有访问的响应式属性
    • 后续执行:每次运行都会重新收集(解决条件分支问题)
  3. 调度器的设计哲学:
    • 异步批量:避免重复执行,提升性能
    • 优先级控制:pre(组件前)/post(组件后)/sync(同步)
    • 微任务队列:利用事件循环机制

watch 的实现

核心原理

watchwatchEffect 更复杂,需要处理:

  • 多种类型的 source(ref、reactive、数组、函数)
  • 获取新旧值
  • 懒执行(默认不立即执行)
  • 深度监听
  • 清理机制

处理多种 source 类型

/**
 * 标准化 watch 的 source 参数
 * 支持的类型:
 * - ref
 * - reactive
 * - 数组
 * - 函数
 */
function normalizeSource(source) {
  if (isRef(source)) {
    // ref: 直接读取 .value
    return () => source.value;
  } else if (isReactive(source)) {
    // reactive: 深度监听,需要递归遍历
    return () => traverse(source);
  } else if (Array.isArray(source)) {
    // 数组: 每个元素单独处理
    return () => source.map(s => normalizeSource(s)());
  } else if (isFunction(source)) {
    // 函数: 直接使用,但需要处理响应式依赖
    return source;
  } else {
    // 无效 source
    return () => {};
  }
}

/**
 * 递归遍历对象,收集所有属性作为依赖
 * 用于实现 deep: true
 */
function traverse(value, seen = new Set()) {
  if (!isObject(value) || seen.has(value)) {
    return value;
  }
  
  seen.add(value);
  
  if (Array.isArray(value)) {
    value.forEach(item => traverse(item, seen));
  } else if (value instanceof Map) {
    value.forEach((v, k) => {
      traverse(v, seen);
      traverse(k, seen);
    });
  } else if (value instanceof Set) {
    value.forEach(v => traverse(v, seen));
  } else {
    Object.keys(value).forEach(key => traverse(value[key], seen));
  }
  
  return value;
}

新旧值的获取

/**
 * watch 的核心实现
 * @param {any} source - 监听的数据源
 * @param {Function} cb - 回调函数
 * @param {Object} options - 配置选项
 */
function watch(source, cb, options = {}) {
  let getter;
  
  // 1. 标准化 source 为 getter 函数
  if (isRef(source)) {
    getter = () => source.value;
  } else if (isReactive(source)) {
    getter = () => source;
    // reactive 默认深度监听
    options.deep = options.deep ?? true;
  } else if (Array.isArray(source)) {
    getter = () => source.map(s => normalizeSource(s)());
  } else if (isFunction(source)) {
    if (cb) {
      // watch 形式:函数作为数据源
      getter = source;
    } else {
      // watchEffect 形式:函数就是 effect
      return watchEffect(source, options);
    }
  }
  
  // 2. 处理 deep 选项
  if (options.deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }
  
  // 3. 存储旧值
  let oldValue;
  
  // 4. 清理函数
  let cleanup;
  function onInvalidate(fn) {
    cleanup = fn;
  }
  
  // 5. 创建调度器
  const scheduler = () => {
    if (cleanup) {
      cleanup();
    }
    
    const newValue = getter();
    
    // 调用回调,传入新旧值
    cb(newValue, oldValue, onInvalidate);
    
    // 更新旧值
    oldValue = newValue;
  };
  
  // 6. 创建 effect
  const _effect = new ReactiveEffect(getter, scheduler);
  
  // 7. 立即执行一次,获取初始值
  if (options.immediate) {
    // immediate 模式:立即执行回调
    scheduler();
  } else {
    // 普通模式:只获取旧值,不执行回调
    oldValue = _effect.run();
  }
  
  // 8. 返回停止函数
  return () => {
    _effect.stop();
  };
}

完整实现

function watch(source, cb, options = {}) {
  // 如果没有提供回调,当作 watchEffect 处理
  if (!isFunction(cb)) {
    return watchEffect(source, options);
  }
  
  let getter;
  let isMultiSource = false;
  
  // 处理多种 source 类型
  if (isRef(source)) {
    getter = () => source.value;
  } else if (isReactive(source)) {
    getter = () => source;
    options.deep = options.deep ?? true;
  } else if (isArray(source)) {
    isMultiSource = true;
    getter = () => source.map(s => {
      if (isRef(s)) return s.value;
      if (isReactive(s)) return traverse(s);
      if (isFunction(s)) return s();
      return s;
    });
  } else if (isFunction(source)) {
    getter = source;
  } else {
    getter = () => {};
  }
  
  // 处理 deep 监听
  if (options.deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }
  
  // 存储旧值
  let oldValue;
  
  // 清理函数
  let cleanup;
  function onInvalidate(fn) {
    cleanup = fn;
  }
  
  // 调度器
  const scheduler = () => {
    if (cleanup) {
      cleanup();
    }
    
    const newValue = getter();
    
    // 比较新值和旧值(简化处理,未实现值比较)
    if (newValue !== oldValue) {
      cb(newValue, oldValue, onInvalidate);
    }
    
    oldValue = newValue;
  };
  
  // 创建 effect
  const _effect = new ReactiveEffect(getter, scheduler);
  
  // 处理 flush 选项
  if (options.flush === 'sync') {
    // 同步执行,使用原调度器
    _effect.scheduler = scheduler;
  } else {
    // 异步执行(post/pre),包装调度器
    const originalScheduler = scheduler;
    _effect.scheduler = () => {
      if (options.flush === 'post') {
        // 组件更新后执行
        Promise.resolve().then(originalScheduler);
      } else {
        // 'pre' 或默认,简化处理
        Promise.resolve().then(originalScheduler);
      }
    };
  }
  
  // 立即执行或获取初始值
  if (options.immediate) {
    // immediate 模式:立即执行一次回调
    scheduler();
  } else {
    // 首次运行 effect,获取旧值
    oldValue = _effect.run();
  }
  
  // 返回停止函数
  return () => {
    _effect.stop();
    if (cleanup) {
      cleanup();
    }
  };
}

回调调度时机控制

flush 配置原理

Vue3 提供了三种调度时机:

// 1. 'pre':组件更新前执行(默认)
watch(source, callback, { flush: 'pre' });

// 2. 'post':组件更新后执行
watch(source, callback, { flush: 'post' });

// 3. 'sync':同步执行
watch(source, callback, { flush: 'sync' });

实现原理

// 调度器工厂
function createScheduler(effect, cb, options) {
  const { flush } = options;
  
  if (flush === 'sync') {
    // 同步:直接执行
    return () => {
      effect.run();
    };
  }
  
  // 异步队列
  const queue = [];
  let isFlushing = false;
  
  function queueFlush() {
    if (!isFlushing) {
      isFlushing = true;
      
      if (flush === 'post') {
        // 组件更新后执行(使用微任务)
        Promise.resolve().then(() => {
          flushJobs();
        });
      } else {
        // 'pre':组件更新前执行
        // 简化处理,也使用微任务
        Promise.resolve().then(() => {
          flushJobs();
        });
      }
    }
  }
  
  function flushJobs() {
    try {
      queue.forEach(job => job());
    } finally {
      queue.length = 0;
      isFlushing = false;
    }
  }
  
  return () => {
    if (!queue.includes(cb)) {
      queue.push(cb);
      queueFlush();
    }
  };
}

深度监听(deep)的实现

递归遍历实现

/**
 * 深度遍历对象,收集所有嵌套属性作为依赖
 */
function traverse(value, seen = new Set()) {
  // 基本类型或已访问过,直接返回
  if (!isObject(value) || seen.has(value)) {
    return value;
  }
  
  seen.add(value);
  
  // 处理数组
  if (Array.isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen);
    }
  } 
  // 处理 Map
  else if (value instanceof Map) {
    value.forEach((v, k) => {
      traverse(v, seen);
      traverse(k, seen);
    });
  } 
  // 处理 Set
  else if (value instanceof Set) {
    value.forEach(v => traverse(v, seen));
  } 
  // 处理普通对象
  else {
    for (const key in value) {
      if (value.hasOwnProperty(key)) {
        traverse(value[key], seen);
      }
    }
  }
  
  return value;
}

深度监听带来的性能问题

const state = reactive({
  user: {
    profile: {
      name: '张三',
      address: {
        city: '北京',
        street: '长安街'
      }
    }
  }
});

// 不推荐:深度监听大对象
watch(state, () => {
  console.log('state 变化了');
}, { deep: true });

// 推荐:只监听需要的路径
watch(() => state.user.profile.name, (newVal) => {
  console.log('name 变化了', newVal);
});

// 或者使用 watchEffect 自动追踪
watchEffect(() => {
  console.log(state.user.profile.name); // 只依赖 name
});

清理机制(onInvalidate)

为什么需要清理?

// 异步操作的竞态问题
watch(id, async (newId, oldId) => {
  const data = await fetchData(newId);
  // 问题:如果 id 快速变化,前一个请求可能覆盖后一个
  renderData(data);
});

onInvalidate 的实现

function watch(source, cb, options = {}) {
  let cleanup;
  
  function onInvalidate(fn) {
    cleanup = fn;
  }
  
  const scheduler = () => {
    // 执行清理
    if (cleanup) {
      cleanup();
    }
    
    const newValue = getter();
    
    // 调用回调,传入新旧值和 onInvalidate
    cb(newValue, oldValue, onInvalidate);
    
    oldValue = newValue;
  };
  
  // 在停止监听时也要清理
  const stop = () => {
    _effect.stop();
    if (cleanup) {
      cleanup();
    }
  };
  
  return stop;
}

实战:watch 的高级用法

监听多个数据源

const firstName = ref('张');
const lastName = ref('三');
const age = ref(25);

// 监听数组
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`);
});

// 监听 getter 返回的对象
watch(
  () => ({
    name: `${firstName.value}${lastName.value}`,
    age: age.value
  }),
  (newVal, oldVal) => {
    console.log('用户信息变化:', newVal, oldVal);
  },
  { deep: true }
);

实现防抖 watch

function debouncedWatch(source, cb, delay = 300, options = {}) {
  let timeoutId;
  
  const debouncedCb = (newVal, oldVal) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      cb(newVal, oldVal);
    }, delay);
  };
  
  return watch(source, debouncedCb, options);
}

// 使用
const searchText = ref('');
const stop = debouncedWatch(searchText, (newVal) => {
  console.log('发送搜索请求:', newVal);
}, 500);

实现可暂停的 watch

function pausableWatch(source, cb, options = {}) {
  let isPaused = false;
  
  const pause = () => {
    isPaused = true;
  };
  
  const resume = () => {
    isPaused = false;
  };
  
  const wrappedCb = (newVal, oldVal) => {
    if (!isPaused) {
      cb(newVal, oldVal);
    }
  };
  
  const stop = watch(source, wrappedCb, options);
  
  return {
    stop,
    pause,
    resume
  };
}

// 使用
const count = ref(0);
const { pause, resume } = pausableWatch(count, (newVal) => {
  console.log('count 变化:', newVal);
});

count.value = 1; // 输出
pause();
count.value = 2; // 不输出
resume();
count.value = 3; // 输出

实现条件 watch

function conditionalWatch(source, condition, cb, options = {}) {
  return watch(source, (newVal, oldVal) => {
    if (condition(newVal, oldVal)) {
      cb(newVal, oldVal);
    }
  }, options);
}

// 使用
const count = ref(0);
conditionalWatch(count, 
  (newVal, oldVal) => newVal > oldVal && newVal % 2 === 0,
  (newVal) => {
    console.log('count 增加了且为偶数:', newVal);
  }
);

count.value = 1; // 不输出
count.value = 2; // 输出
count.value = 3; // 不输出
count.value = 4; // 输出

性能优化与最佳实践

1. 精确依赖

// 不推荐:依赖太多
watch(user, () => {
  console.log(user.value.name, user.value.age);
}, { deep: true });

// 推荐:只依赖需要的属性
watch(() => user.value.name, (name) => {
  console.log(name);
});

watch(() => user.value.age, (age) => {
  console.log(age);
});

2. 避免副作用累积

// 问题:每次执行都创建新监听
watch(id, (newId) => {
  // 每次 id 变化都会创建一个新的 watch
  watch(detailId, () => {
    // ...
  });
});

// 正确:使用 onInvalidate 清理
watch(id, (newId, oldId, onInvalidate) => {
  let stopDetailWatch;
  
  onInvalidate(() => {
    if (stopDetailWatch) {
      stopDetailWatch();
    }
  });
  
  stopDetailWatch = watch(detailId, () => {
    // ...
  });
});

3. 合理使用 immediate

// 需要初始值:使用 immediate
watch(source, (newVal) => {
  initializeWithValue(newVal);
}, { immediate: true });

// 不需要初始值:默认懒执行
watch(source, (newVal) => {
  handleChange(newVal);
});

4. 选择正确的时机

// 需要访问更新后的 DOM:使用 post
watch(source, () => {
  console.log('DOM 已更新');
}, { flush: 'post' });

// 需要组件更新前处理:使用 pre
watch(source, () => {
  console.log('DOM 即将更新');
}, { flush: 'pre' });

// 需要同步响应:使用 sync
watch(source, () => {
  console.log('立即响应');
}, { flush: 'sync' });

常见陷阱与解决方案

陷阱1:异步更新问题

const count = ref(0);

watch(count, (newVal) => {
  console.log('Current DOM:', document.getElementById('counter')?.textContent);
  // 可能不是最新的
});

// 解决方案:使用 nextTick
watch(count, async (newVal) => {
  await nextTick();
  console.log('Updated DOM:', document.getElementById('counter')?.textContent);
});

陷阱2:无限循环

const user = ref({ name: 'John' });

// 错误:会触发无限循环
watch(user, (newUser) => {
  newUser.name = newUser.name.toUpperCase(); // 修改会触发再次监听
}, { deep: true });

// 正确:使用拷贝
watch(user, (newUser) => {
  const updated = { ...newUser, name: newUser.name.toUpperCase() };
  user.value = updated; // 只在必要时更新
});

陷阱3:对象引用丢失

const state = reactive({ user: { name: 'John' } });

// 错误:oldVal 和 newVal 引用相同
watch(() => state.user, (newVal, oldVal) => {
  console.log(newVal === oldVal); // true - 同一个对象
});

// 正确:深拷贝比较
watch(() => ({ ...state.user }), (newVal, oldVal) => {
  console.log(newVal === oldVal); // false - 不同对象
});

陷阱4:监听器的执行顺序

const data = ref(0);

// watch 1
watch(data, () => {
  console.log('Watch 1');
});

// watch 2
watch(data, () => {
  console.log('Watch 2');
});

// watchEffect
watchEffect(() => {
  console.log('WatchEffect:', data.value);
});

data.value = 1;
// 输出顺序取决于创建顺序和 flush 配置

陷阱5:内存泄漏

// 错误:在组件内创建但不清理
export default {
  setup() {
    const data = ref(0);
    
    // 组件销毁后仍然存在
    watch(data, () => {
      console.log('Changed');
    });
  }
}

// 正确:使用 onUnmounted 清理
export default {
  setup() {
    const data = ref(0);
    
    const stop = watch(data, () => {
      console.log('Changed');
    });
    
    onUnmounted(() => {
      stop();
    });
  }
}

结语

watch 和 watchEffect 是 Vue3 响应式系统中面向开发者的核心 API,它们基于底层的 effect 系统,提供了更友好、更强大的数据监听能力。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!