Vue3源码深度解析(二):ref、computed与watch实现原理

前言

在上一篇文章中,剖析了Vue3响应式系统的底层原理,理解了reactive、Proxy、track、trigger等核心概念。本文将继续深入,探讨Vue3中最常用的三个响应式API:ref、computed和watch的源码实现。

通过本文,你将掌握:

  • ref为什么需要.value访问
  • computed的缓存机制如何实现
  • watch的依赖收集与触发原理
  • 三者之间的内在联系与区别

一、ref响应式实现原理

1.1 为什么需要ref

在第一篇文章中,我们知道reactive基于Proxy实现,只能代理对象类型。但JavaScript中还有大量的基本类型数据(string、number、boolean等),这些数据无法被Proxy代理。

// reactive只能处理对象
const state = reactive({ count: 0 }); //  可以

// 基本类型无法使用reactive
const count = reactive(0); // 不行

为了解决这个问题,Vue3提供了ref API,它可以:

  • 处理基本类型数据
  • 也可以处理对象类型数据
  • 统一的.value访问方式

1.2 ref的基本使用

import { ref, effect } from 'vue';

// 创建ref
const count = ref(0);
const user = ref({ name: 'Vue3' });

// 访问和修改需要通过.value
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1

// 在模板中会自动解包,不需要.value
// <div>{{ count }}</div>

1.3 RefImpl类的源码实现

ref的核心是RefImpl类,让我们看看简化版的实现:

class RefImpl {
  private _value; // 存储实际值
  private _rawValue; // 存储原始值(未转换的)
  public dep; // 依赖集合
  public __v_isRef = true; // ref标识

  constructor(value, public __v_isShallow = false) {
    // 保存原始值
    this._rawValue = __v_isShallow ? value : toRaw(value);
    // 如果是对象,转换为reactive
    this._value = __v_isShallow ? value : toReactive(value);
    // 创建依赖集合
    this.dep = new Set();
  }

  // getter:依赖收集
  get value() {
    // 收集依赖
    trackRefValue(this);
    return this._value;
  }

  // setter:触发更新
  set value(newVal) {
    // 判断值是否发生变化
    newVal = this.__v_isShallow ? newVal : toRaw(newVal);
    
    if (hasChanged(newVal, this._rawValue)) {
      // 更新原始值
      this._rawValue = newVal;
      // 更新值(对象转reactive)
      this._value = this.__v_isShallow ? newVal : toReactive(newVal);
      // 触发依赖更新
      triggerRefValue(this);
    }
  }
}

// ref函数
function ref(value) {
  return new RefImpl(value);
}

// 判断是否是ref
function isRef(r) {
  return !!(r && r.__v_isRef === true);
}

1.4 toReactive辅助函数

当ref接收对象类型时,会通过toReactive转换为reactive:

function toReactive(value) {
  return isObject(value) ? reactive(value) : value;
}

function isObject(value) {
  return value !== null && typeof value === 'object';
}

function toRaw(observed) {
  // 获取响应式对象的原始对象
  const raw = observed && observed.__v_raw;
  return raw ? toRaw(raw) : observed;
}

1.5 ref的依赖收集与触发

ref有自己独立的依赖收集和触发机制:

// ref的依赖收集
function trackRefValue(ref) {
  if (activeEffect) {
    // 将当前副作用函数添加到ref的dep中
    trackEffects(ref.dep);
  }
}

function trackEffects(dep) {
  if (activeEffect) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

// ref的触发更新
function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep);
  }
}

function triggerEffects(dep) {
  const effects = Array.from(dep);
  effects.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect();
    }
  });
}

1.6 完整示例

// 创建ref
const count = ref(0);
const user = ref({ name: 'Vue', age: 3 });

// 创建副作用函数
effect(() => {
  console.log('count:', count.value);
  console.log('user.name:', user.value.name);
});
// 输出:count: 0
// 输出:user.name: Vue

// 修改基本类型
count.value = 10;
// 输出:count: 10
// 输出:user.name: Vue

// 修改对象属性
user.value.name = 'Vue3';
// 输出:count: 10
// 输出:user.name: Vue3

// 替换整个对象
user.value = { name: 'React', age: 10 };
// 输出:count: 10
// 输出:user.name: React

1.7 shallowRef浅层响应式

shallowRef只对.value的访问是响应式的,不会深度转换对象:

function shallowRef(value) {
  return new RefImpl(value, true); // 第二个参数为true
}

// 使用示例
const state = shallowRef({ count: 0 });

effect(() => {
  console.log(state.value.count);
});

// 这样不会触发更新,因为没有修改.value本身
state.value.count++; // 不会触发

// 这样会触发更新
state.value = { count: 1 }; //  会触发

1.8 toRef和toRefs

toRef用于为响应式对象的某个属性创建ref:

function toRef(object, key, defaultValue) {
  const val = object[key];
  return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue);
}

class ObjectRefImpl {
  public __v_isRef = true;

  constructor(
    private _object,
    private _key,
    private _defaultValue
  ) {}

  get value() {
    const val = this._object[this._key];
    return val === undefined ? this._defaultValue : val;
  }

  set value(newVal) {
    this._object[this._key] = newVal;
  }
}

// toRefs批量转换
function toRefs(object) {
  const ret = {};
  for (const key in object) {
    ret[key] = toRef(object, key);
  }
  return ret;
}

// 使用示例
const state = reactive({ count: 0, name: 'Vue' });
const { count, name } = toRefs(state);

effect(() => {
  console.log(count.value, name.value);
});

count.value++; // 会触发更新

二、computed计算属性实现原理

2.1 computed的特点

computed具有以下特点:

  • 缓存机制:只有依赖变化时才重新计算
  • 懒计算:只有被访问时才会计算
  • 支持getter和setter:可以是只读或可写
// 只读computed
const count = ref(1);
const double = computed(() => count.value * 2);

// 可写computed
const firstName = ref('Zhang');
const lastName = ref('San');
const fullName = computed({
  get: () => firstName.value + ' ' + lastName.value,
  set: (val) => {
    const [first, last] = val.split(' ');
    firstName.value = first;
    lastName.value = last;
  }
});

2.2 ComputedRefImpl类实现

computed的核心是ComputedRefImpl类:

class ComputedRefImpl {
  public dep; // 依赖集合
  private _value; // 缓存的值
  public readonly effect; // 内部effect
  public readonly __v_isRef = true;
  public _dirty = true; // 脏标记,true表示需要重新计算

  constructor(getter, private readonly _setter) {
    // 创建effect,但不立即执行
    this.effect = new ReactiveEffect(getter, () => {
      // scheduler调度器
      if (!this._dirty) {
        this._dirty = true;
        // 触发computed的依赖更新
        triggerRefValue(this);
      }
    });
    
    this.dep = new Set();
  }

  get value() {
    // 收集computed的依赖
    trackRefValue(this);
    
    // 如果是脏的,重新计算
    if (this._dirty) {
      this._dirty = false;
      // 执行getter,收集依赖
      this._value = this.effect.run();
    }
    
    return this._value;
  }

  set value(newValue) {
    this._setter(newValue);
  }
}

// computed函数
function computed(getterOrOptions) {
  let getter;
  let setter;

  // 判断参数类型
  const onlyGetter = typeof getterOrOptions === 'function';
  
  if (onlyGetter) {
    getter = getterOrOptions;
    setter = () => {
      console.warn('Computed property is readonly');
    };
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }

  return new ComputedRefImpl(getter, setter);
}

2.3 调度器(scheduler)的作用

调度器是computed实现缓存的关键:

class ReactiveEffect {
  constructor(
    public fn, // 副作用函数
    public scheduler = null // 调度器
  ) {}

  run() {
    activeEffect = this;
    return this.fn();
  }
}

// 触发更新时的逻辑
function triggerEffect(effect) {
  if (effect.scheduler) {
    // 如果有调度器,执行调度器而不是effect本身
    effect.scheduler();
  } else {
    effect.run();
  }
}

2.4 脏检查机制(dirty flag)

脏检查是实现缓存的核心:

// 工作流程示例
const count = ref(1);
const double = computed(() => {
  console.log('计算执行');
  return count.value * 2;
});

// 第一次访问,_dirty = true,执行计算
console.log(double.value); // 输出:计算执行  2

// 再次访问,_dirty = false,使用缓存
console.log(double.value); // 输出:2(没有"计算执行")

// 修改依赖,触发scheduler,设置_dirty = true
count.value = 2;

// 再次访问,_dirty = true,重新计算
console.log(double.value); // 输出:计算执行  4

2.5 computed的依赖收集链路

computed的依赖收集有两层:

const count = ref(0);
const double = computed(() => count.value * 2);

effect(() => {
  console.log(double.value);
});

/**
 * 依赖关系链:
 * 
 * effect
 *   ↓ (收集依赖)
 * computed (double)
 *   ↓ (收集依赖)
 * ref (count)
 * 
 * 触发链路:
 * count.value = 1
 *   ↓ (触发)
 * computed的scheduler执行
 *   ↓ (设置_dirty = true,触发)
 * effect重新执行
 *   ↓ (访问double.value)
 * computed重新计算
 */

2.6 完整示例

const price = ref(10);
const quantity = ref(2);

// 创建计算属性
const total = computed(() => {
  console.log('计算total');
  return price.value * quantity.value;
});

// 第一层effect
effect(() => {
  console.log('effect执行,total:', total.value);
});
// 输出:计算total
// 输出:effect执行,total: 20

// 多次访问,使用缓存
console.log(total.value); // 20(没有"计算total")
console.log(total.value); // 20(没有"计算total")

// 修改依赖
price.value = 20;
// 输出:计算total
// 输出:effect执行,total: 40

quantity.value = 3;
// 输出:计算total
// 输出:effect执行,total: 60

三、watch侦听器实现原理

3.1 watch的基本用法

watch有多种使用方式:

const count = ref(0);
const state = reactive({ name: 'Vue' });

// 1. 监听ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`);
});

// 2. 监听reactive对象的属性
watch(() => state.name, (newVal, oldVal) => {
  console.log(`name: ${oldVal} -> ${newVal}`);
});

// 3. 监听多个数据源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('多个数据变化');
});

// 4. 深度监听
watch(state, (newVal, oldVal) => {
  console.log('state变化');
}, { deep: true });

// 5. 立即执行
watch(count, (newVal, oldVal) => {
  console.log('立即执行');
}, { immediate: true });

3.2 watch核心实现

function watch(source, cb, options = {}) {
  return doWatch(source, cb, options);
}

function doWatch(source, cb, { immediate, deep, flush } = {}) {
  // 1. 标准化source为getter函数
  let getter;
  let forceTrigger = false;

  if (isRef(source)) {
    // 监听ref
    getter = () => source.value;
    forceTrigger = isShallow(source);
  } else if (isReactive(source)) {
    // 监听reactive,自动深度监听
    getter = () => source;
    deep = true;
  } else if (Array.isArray(source)) {
    // 监听多个数据源
    getter = () =>
      source.map(s => {
        if (isRef(s)) return s.value;
        if (isReactive(s)) return traverse(s);
        if (typeof s === 'function') return s();
      });
  } else if (typeof source === 'function') {
    // 监听getter函数
    getter = source;
  } else {
    getter = () => {};
  }

  // 2. 如果是深度监听,需要递归遍历
  if (deep && getter) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }

  // 3. 保存旧值
  let oldValue;

  // 4. 定义job函数
  const job = () => {
    if (cb) {
      // 获取新值
      const newValue = effect.run();
      
      // 深度监听或强制触发或新旧值不同时,执行回调
      if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
        // 执行清理函数
        if (cleanup) {
          cleanup();
        }
        
        // 执行回调
        cb(newValue, oldValue, onCleanup);
        
        // 更新旧值
        oldValue = newValue;
      }
    }
  };

  // 5. 创建effect
  const effect = new ReactiveEffect(getter, () => {
    // scheduler
    if (flush === 'sync') {
      job();
    } else if (flush === 'post') {
      queuePostFlushCb(job);
    } else {
      // 默认'pre'
      queueJob(job);
    }
  });

  // 6. 初始化
  if (cb) {
    if (immediate) {
      job();
    } else {
      oldValue = effect.run();
    }
  } else {
    effect.run();
  }

  // 7. 返回停止函数
  return () => {
    effect.stop();
  };
}

3.3 traverse深度遍历

traverse函数用于深度遍历对象,触发所有属性的getter:

function traverse(value, seen = new Set()) {
  // 不是对象或已经遍历过,直接返回
  if (!isObject(value) || seen.has(value)) {
    return value;
  }

  // 标记已遍历
  seen.add(value);

  // 处理ref
  if (isRef(value)) {
    traverse(value.value, seen);
  } else if (Array.isArray(value)) {
    // 遍历数组
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen);
    }
  } else if (isSet(value) || isMap(value)) {
    // 遍历Set和Map
    value.forEach((v) => {
      traverse(v, seen);
    });
  } else if (isPlainObject(value)) {
    // 遍历普通对象
    for (const key in value) {
      traverse(value[key], seen);
    }
  }

  return value;
}

3.4 cleanup清理函数

cleanup用于清理副作用:

function doWatch(source, cb, options) {
  let cleanup;
  
  const onCleanup = (fn) => {
    cleanup = fn;
  };

  const job = () => {
    const newValue = effect.run();
    
    // 执行上一次的清理函数
    if (cleanup) {
      cleanup();
    }
    
    cb(newValue, oldValue, onCleanup);
    oldValue = newValue;
  };
  
  // ...
}

// 使用示例
watch(id, async (newId, oldId, onCleanup) => {
  let expired = false;
  
  onCleanup(() => {
    expired = true;
  });
  
  const result = await fetch(`/api/${newId}`);
  
  if (!expired) {
    // 只有在未过期时才使用结果
    data.value = result;
  }
});

3.5 flush调度时机

flush参数控制回调的执行时机:

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

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

// 'sync':同步执行(不推荐)
watch(source, cb, { flush: 'sync' });

// 实现
function doWatch(source, cb, { flush = 'pre' } = {}) {
  const job = () => {
    // 执行回调
  };

  const effect = new ReactiveEffect(getter, () => {
    if (flush === 'sync') {
      job();
    } else if (flush === 'post') {
      queuePostFlushCb(job); // 推入后置队列
    } else {
      queueJob(job); // 推入前置队列
    }
  });
}

3.6 watchEffect简化版

watchEffect是watch的简化版,自动收集依赖:

function watchEffect(effect, options) {
  return doWatch(effect, null, options);
}

// 使用示例
const count = ref(0);

watchEffect(() => {
  console.log('count:', count.value);
});
// 立即执行,输出:count: 0

count.value++;
// 输出:count: 1

3.7 完整示例

const count = ref(0);
const state = reactive({ user: { name: 'Vue' } });

// 示例1:监听ref
watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变为${newVal}`);
});

count.value = 1;
// 输出:count从0变为1

// 示例2:深度监听对象
watch(
  () => state.user,
  (newVal, oldVal) => {
    console.log('user对象变化', newVal);
  },
  { deep: true }
);

state.user.name = 'Vue3';
// 输出:user对象变化 { name: 'Vue3' }

// 示例3:立即执行
watch(
  count,
  (newVal, oldVal) => {
    console.log('立即执行', newVal, oldVal);
  },
  { immediate: true }
);
// 输出:立即执行 1 undefined

// 示例4:cleanup清理
let id = ref(1);
watch(id, async (newId, oldId, onCleanup) => {
  let expired = false;
  
  onCleanup(() => {
    console.log('清理旧请求');
    expired = true;
  });
  
  console.log(`开始请求 id=${newId}`);
  
  setTimeout(() => {
    if (!expired) {
      console.log(`请求完成 id=${newId}`);
    }
  }, 1000);
});

id.value = 2;
// 输出:开始请求 id=2

setTimeout(() => {
  id.value = 3;
  // 输出:清理旧请求
  // 输出:开始请求 id=3
}, 500);

四、三者对比与使用场景

4.1 ref vs reactive

特性refreactive
数据类型任意类型对象类型
访问方式.value直接访问
模板中自动解包直接使用
替换整个对象✅ 可以❌ 会失去响应性
底层实现RefImpl类Proxy代理

4.2 computed vs watch

特性computedwatch
返回值✅ 有返回值❌ 无返回值
缓存✅ 有缓存❌ 无缓存
懒执行✅ 按需计算✅ 可配置
异步操作❌ 不支持✅ 支持
使用场景计算派生数据执行副作用

4.3 使用场景建议

使用ref:

  • 基本类型数据
  • 需要替换整个对象
  • 需要在模板中自动解包

使用reactive:

  • 复杂对象结构
  • 不需要替换整个对象
  • 更接近原生JavaScript对象

使用computed:

  • 基于响应式数据计算派生值
  • 需要缓存计算结果
  • 计算逻辑比较复杂

使用watch:

  • 执行异步操作
  • 执行开销较大的操作
  • 需要在数据变化时执行副作用

五、性能优化技巧

5.1 合理使用shallowRef

// 大型对象使用shallowRef避免深度代理
const bigData = shallowRef({
  list: new Array(10000).fill(0)
});

// 修改时替换整个对象
bigData.value = { list: [...] };

5.2 使用computed缓存计算结果

// ❌ 每次都重新计算
const total = () => list.value.reduce((sum, item) => sum + item.price, 0);

// ✅ 使用computed缓存
const total = computed(() => 
  list.value.reduce((sum, item) => sum + item.price, 0)
);

5.3 避免不必要的深度监听

// ❌ 深度监听整个对象
watch(state, callback, { deep: true });

// ✅ 只监听需要的属性
watch(() => state.user.name, callback);

5.4 使用watchEffect简化代码

// ❌ 手动指定依赖
watch([count, name], () => {
  console.log(count.value, name.value);
});

// ✅ 自动收集依赖
watchEffect(() => {
  console.log(count.value, name.value);
});

六、总结

本文深入剖析了Vue3三个核心响应式API的实现原理:

ref实现要点:

  • RefImpl类通过getter/setter实现响应式
  • 对象类型会转换为reactive
  • 独立的依赖收集和触发机制

computed实现要点:

  • ComputedRefImpl类实现缓存机制
  • 脏检查(dirty flag)控制是否重新计算
  • 调度器(scheduler)延迟触发更新

watch实现要点:

  • traverse函数实现深度遍历
  • cleanup清理函数处理副作用
  • flush参数控制执行时机

掌握这些原理后,我们就能更好地理解Vue3的响应式系统,写出更高效的代码。在下一篇文章中,我们将探讨编译器和虚拟DOM的实现原理。

相关资源

  • Vue3官方文档:https://cn.vuejs.org/
  • Vue3响应式API:https://cn.vuejs.org/api/reactivity-core.html
  • Vue3 GitHub仓库:https://github.com/vuejs/core

作者:前端技术探索者
本文为Vue3源码解析系列第二篇,专注于ref、computed、watch实现原理。下一篇将带来编译器与虚拟DOM的深度解析,敬请期待!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值