手写 vue 源码 === watch 实现

1 阅读8分钟

手写 vue 源码 === watch 实现

目录

[TOC]

Vue 的响应式系统是其核心特性之一,而 watch 作为响应式系统的重要组成部分,允许我们监听数据变化并执行相应的回调函数。本文将深入探讨 Vue 中 watch 的实现原理,从源码角度逐步分析其工作机制。

1. watch 的基本使用

在深入实现原理之前,让我们先回顾一下 watch 的基本用法:

// 监听响应式对象
watch(state, (newVal, oldVal) => {
  console.log(newVal, oldVal);
});
 
// 监听 ref 对象
watch(name, (newVal, oldVal) => {
  console.log(newVal, oldVal);
});
 
// 监听 getter 函数
watch(() => state.name, (newVal, oldVal) => {
  console.log(newVal, oldVal);
}, {
  deep: true,      // 是否深度监听
  immediate: true  // 是否立即执行
});

2. watch 的整体架构

从源码中可以看出, watch 函数本质上是对 doWatch 函数的封装:

export function watch(source, cb, options = {} as any) {
  // watchEffect 也是基于这个实现
  return doWatch(source, cb, options);
}

watchEffect 也是基于同一个 doWatch 函数实现的,只是没有回调函数:

export function watchEffect(fn, options = {} as any) {
  return doWatch(fn, null, options);
}

3. doWatch 函数的实现

doWatch 函数是 watchwatchEffect 的核心实现,它处理不同类型的监听源,并设置相应的依赖收集和触发机制。

3.1 处理不同类型的监听源

doWatch 首先需要处理不同类型的监听源,将其转换为统一的 getter 函数:

function doWatch(source, cb, { deep, immediate }) {
  // source > getter
  const ReactiveGetter = (source) =>
    traverse(source, deep === false ? 1 : undefined);
 
  let getter;
  let oldValue;
  let cleanup;
  
  // 处理不同类型的监听源
  if (isReactive(source)) {
    getter = () => ReactiveGetter(source);
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (isFunction(source)) {
    getter = source;
  }
  // ...
}

这里根据监听源的类型设置不同的 getter 函数:

  • 对于响应式对象,使用   ReactiveGetter  函数遍历对象
  • 对于 ref 对象,直接获取其 value 属性
  • 对于函数,直接使用该函数作为 getter
3.2 清理副作用的机制

watch 提供了清理副作用的机制,通过 onCleanup 函数注册清理函数:

const onCleanup = (fn) => {
  cleanup = () => {
    fn();
    cleanup = null;
  };
};

这个机制允许我们在下一次回调执行前清理上一次回调产生的副作用,比如取消异步请求等。

3.3 创建响应式效果

 doWatch 的核心是创建一个 ReactiveEffect 实例,它负责依赖收集和触发更新:

const job = () => {
  if (cb) {
    const newValue = effect.run();
    if (cleanup) {
      cleanup(); // 在执行回调之前,先执行清理
    }
    // 执行cb
    cb(newValue, oldValue, onCleanup);
    oldValue = newValue;
  } else {
    effect.run();
  }
};
 
let effect = new ReactiveEffect(getter, job);

这里创建了一个 ReactiveEffect 实例,它接收两个参数:

  •  getter  函数:用于获取监听源的值,并在此过程中进行依赖收集

  •  job  函数:当依赖变化时执行的调度函数,它会获取新值,执行清理函数,然后调用用户提供的回调函数

3.4 初始化执行

根据配置决定是否立即执行回调函数:

if (cb) {
  // 如果immediate为true,则需要立即执行cb
  if (immediate) {
    job();
  } else {
    oldValue = effect.run(); // run 执行就是执行getter函数,getter函数会触发依赖收集
  }
} else {
  effect.run();
}
  • 如果有回调函数且   immediate 为 true,则立即执行  job 函数
  • 如果有回调函数但   immediate  不为 true,则执行 effect.run() 获取初始值,但不执行回调
  • 如果没有回调函数(即   watchEffect ),则直接执行 effect.run()
3.5 返回停止函数

最后, doWatch 返回一个函数,用于停止监听:

const unWatch = () => {
  effect.stop();
};
return unWatch;

4. watch 如何基于 ReactiveEffect 实现

现在我们已经了解了 ReactiveEffect 的核心机制,接下来看看 watch 如何基于它实现:

function doWatch(source, cb, { deep, immediate }) {
  // 处理不同类型的监听源,转换为 getter 函数
  const ReactiveGetter = (source) =>
    traverse(source, deep === false ? 1 : undefined);
 
  let getter;
  let oldValue;
  let cleanup;
  
  if (isReactive(source)) {
    getter = () => ReactiveGetter(source);
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (isFunction(source)) {
    getter = source;
  }
  
  // 清理函数
  const onCleanup = (fn) => {
    cleanup = () => {
      fn();
      cleanup = null;
    };
  };
  
  // 调度器函数
  const job = () => {
    if (cb) {
      const newValue = effect.run();
      if (cleanup) {
        cleanup();
      }
      cb(newValue, oldValue, onCleanup);
      oldValue = newValue;
    } else {
      effect.run();
    }
  };
 
  // 创建 ReactiveEffect 实例
  let effect = new ReactiveEffect(getter, job);
  
  // 初始化执行
  if (cb) {
    if (immediate) {
      job();
    } else {
      oldValue = effect.run();
    }
  } else {
    effect.run();
  }
  
  // 返回停止函数
  const unWatch = () => {
    effect.stop();
  };
  return unWatch;
}
4.1 依赖收集过程详解

watch 的依赖收集过程如下:

  1. 创建 getter 函数 :根据监听源类型(响应式对象、ref、函数)创建相应的 getter 函数。
  2. 创建 ReactiveEffect 实例 :使用 getter 函数和调度器函数 job 创建 ReactiveEffect 实例。
  3. 初始执行 :调用 effect.run() ,这会执行 getter 函数,触发依赖收集。
  • 对于响应式对象,会通过  traverse  函数递归访问所有属性,触发每个属性的 getter
  • 对于 ref 对象,会访问其 value 属性,触发 value 的 getter
  • 对于函数,会直接执行该函数,访问其中的响应式属性,触发相应的 getter
  1. 建立依赖关系 :在上述过程中,每当访问响应式属性时,都会触发   track
    函数,将当前的 effect(即我们创建的 ReactiveEffect 实例)添加到该属性的依赖集合中。
4.2 更新触发过程详解

当监听的响应式数据发生变化时,更新触发过程如下:

  1. 触发属性的 setter :修改响应式数据会触发其 setter。
  2. 调用 trigger 函数 :setter 中会调用   trigger  函数,找到该属性收集的所有依赖(effect)。
  3. 执行 triggerEffects :  trigger  函数会调用   triggerEffects  函数,遍历依赖集合。
  4. 执行调度器 :由于我们的 effect 配置了调度器函数 job,所以会执行 job 函数。
  5. 获取新值并执行回调 :job 函数会再次调用 effect.run() 获取新值,然后执行用户提供的回调函数,传入新值、旧值和清理函数。

5. 深度遍历的实现细节

对于深度监听,Vue 使用 traverse 函数递归遍历对象的所有属性:

function traverse(source, depth, currentDepth = 0, seen = new Set()) {
  if (!isObject(source)) {
    return source;
  }
 
  if (depth) {
    if (currentDepth >= depth) {
      return source;
    }
    currentDepth++;
  }
  
  if (seen.has(source)) {
    return source;
  }
  
  for (const key in source) {
    traverse(source[key], depth, currentDepth, seen);
  }
  
  seen.add(source);
  return source;
}

这个函数的工作原理:

  1. 递归访问对象的每个属性,触发每个属性的 getter
  2. 在 getter 中,会调用   track  函数,将当前的 effect 添加到该属性的依赖集合中
  3. 这样,当对象的任何属性发生变化时,都会触发 effect 的更新

6. 实际执行流程示例

让我们通过一个具体例子,详细追踪 watch 的执行流程:

const state = reactive({ name: "张三" });
 
watch(() => state.name, (newVal, oldVal) => {
  console.log(`名字从 ${oldVal} 变成了 ${newVal}`);
});
 
// 修改值触发更新
state.name = "李四";
6.1 初始化阶段
  1. 调用 watch 函数 :传入 getter 函数 () => state.name 和回调函数。
  2. 调用 doWatch 函数 :处理参数,创建 ReactiveEffect 实例。
  3. 执行 effect.run()
  • 将当前 effect 设置为全局的  activeEffect

  • 执行 getter 函数 () => state.name ,访问 state.name

  • 触发 state.name 的 getter

  • 在 getter 中调用 track(state, 'name')

  •  track  函数将当前 effect 添加到 state.name 的依赖集合中

  • 返回 state.name 的值 "张三",保存为   oldValue

  1. 建立依赖关系 :现在 state.name 的依赖集合中包含了我们创建的 effect
6.2 更新阶段
  1. 修改 state.namestate.name = "李四"
  2. 触发 setter :这会调用 trigger(state, 'name', "李四", "张三")
  3. 查找依赖 :  trigger  函数找到 state.name 的依赖集合
  4. 执行 triggerEffects :遍历依赖集合,对于我们的 effect,执行其调度器函数 job
  5. 执行 job 函数
  • 调用 effect.run() 再次执行 getter 函数,获取新值 "李四"
  • 执行回调函数,传入新值 "李四"、旧值 "张三"
  • 更新   oldValue  为 "李四"
  1. 输出结果 :控制台输出 "名字从 张三 变成了 李四"

7. watch 与 computed 的对比

watchcomputed 都是基于 ReactiveEffect 实现的,但它们的使用场景和行为有所不同:

export function computed(getterOrOptions) {
  let getter;
  let setter;
  let onlyGetter = isFunction(getterOrOptions);
  if (onlyGetter) {
    getter = getterOrOptions;
    setter = () => {};
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  return new ComputedRefImpl(getter, setter);
}
 
class ComputedRefImpl {
  public _value;
  public effect;
  public dep;
  
  constructor(public getter, public setter) {
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      () => {
        triggerRefValue(this);
      }
    );
  }
  
  get value() {
    if (this.effect.dirty) {
      this._value = this.effect.run();
      trackRefValue(this);
    }
    return this._value;
  }
  
  set value(newValue) {
    this.setter(newValue);
  }
}

主要区别:

  1. 计算时机
  •  computed  是惰性的,只在访问其 value 属性时才计算

  •  watch  是主动的,在依赖变化时立即执行回调

  1. 缓存机制
  •  computed  会缓存计算结果,只有依赖变化且再次访问时才重新计算

  •  watch  没有缓存机制,依赖变化就执行回调

  1. 返回值
  •  computed  返回一个带有 value 属性的对象

  •  watch  返回一个停止函数

8. 高级应用:清理副作用

watch 提供了清理副作用的机制,这在处理异步操作时特别有用:

watch(() => state.name, (newVal, oldVal, onCleanup) => {
  const timer = setTimeout(() => {
    console.log(`处理 ${newVal} 的数据`);
  }, 1000);
  
  onCleanup(() => {
    clearTimeout(timer);
    console.log("清理上一次的定时器");
  });
});

state.name 频繁变化时,每次变化都会先执行上一次注册的清理函数,然后再执行新的回调。这确保了我们只处理最新的数据,避免了竞态条件

总结

Vue 的 watch 实现是响应式系统的精彩应用,它巧妙地利用了 ReactiveEffect 类的依赖收集和触发更新机制,实现了对数据变化的监听和响应。

通过深入理解 watch 的实现原理,我们不仅能更好地使用这个 API,还能学习到Vue 响应式系统的设计思想和实现技巧。这些知识对于理解 Vue 的整体架构和开发高质量的 Vue 应用都有很大帮助。

watch 的实现展示了 Vue 响应式系统的强大和灵活,通过统一的底层机制( ReactiveEffect )支持了多种上层 API( watch watchEffect computed 等),这种分层设计使得 Vue 的响应式系统既强大又易用。