vue3.5.18源码:深入watch api底层实现

侦听器的定义:当数据发生变化时,执行一些"副作用"。

侦听器的底层原理:发布订阅者模式,RefImpl类的实例是一个发布者,ReactiveEffect类的实例是一个订阅者。

学完本文,你将获得以下知识:

  • watch函数的底层实现
  • RefImpl类和ReactiveEffect类之间的关系
  • 发布订阅者模式在vue3中的使用

源码较长,只需抓住主要流程,发布者类RefImpl,订阅者类ReactiveEffect,两者关系的建立,数据改变到cb函数执行流程。

举例如下:

html 复制代码
<body>
  <div id="app"></div>
  <script>
    const App = {
      template: `<button @click="plus">plus</button>`,
      setup() {
        // 响应式数据
        debugger;
        const count = Vue.ref(0);
        // 监听变化
        debugger;
        Vue.watch(count, (val, oldVal) => {
          console.log(val, oldVal);
        });
        // 变化数据
        const plus = () => {
          debugger;
          count.value += 1;
        };
        // 暴露给模板
        return {
          count,
          plus,
        };
      },
    };
    // 创建应用并挂载
    const app = Vue.createApp(App);
    app.mount("#app");
  </script>
</body>

在以上例子中,

一、RefImpl

RefImpl的实例化对象就是一个发布者。从const count = Vue.ref(0)执行,最终会返回RefImpl的实例。核心逻辑如下:

ts 复制代码
// ref函数
function ref(value) {
  return createRef(value, false);
}
// createRef函数
function createRef(rawValue, shallow) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}
// RefImpl类
class RefImpl {
  constructor(value, isShallow2) {
    // 依赖管理
    this.dep = new Dep();
    this["__v_isRef"] = true;
    this["__v_isShallow"] = false;
    this._rawValue = isShallow2 ? value : toRaw(value);
    this._value = isShallow2 ? value : toReactive(value);
    this["__v_isShallow"] = isShallow2;
  }
  get value() {
    {
      // 在访问RefImpl实例化的对象的值的时候,会进行依赖收集
      this.dep.track({
        target: this,
        type: "get",
        key: "value",
      });
    }
    return this._value;
  }
  set value(newValue) {
    const oldValue = this._rawValue;
    const useDirectValue =
      this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue);
    newValue = useDirectValue ? newValue : toRaw(newValue);
    if (hasChanged(newValue, oldValue)) {
      this._rawValue = newValue;
      this._value = useDirectValue ? newValue : toReactive(newValue);
      {
        // 在修改RefImpl实例化的对象的值的时候,会执行派发更新
        this.dep.trigger({
          target: this,
          type: "set",
          key: "value",
          newValue,
          oldValue,
        });
      }
    }
  }
}
// Dep类,作为发布者和订阅者之间的关系管理类
class Dep {
  constructor(computed) {
    this.computed = computed;
    this.version = 0;
    // 核心: dep和当前活跃的sub之间的关系
    this.activeLink = void 0;
    this.subs = void 0;
    this.map = void 0;
    this.key = void 0;
    // 订阅者个数
    this.sc = 0;
    this.__v_skip = true;
    {
      this.subsHead = void 0;
    }
  }
  track(debugInfo) {
    // 依赖收集,省略逻辑...
  }
  trigger(debugInfo) {
    // 派发更新,省略逻辑...
  }
  notify(debugInfo) {
    // 通知更新,省略逻辑...
  }
}

当执行到const count = Vue.ref(0)时,会执行响应式数据初始化函数,返回一个RefImpl对象,该对象包含了一个依赖管理器:this.dep = new Dep()。并且定义了取值和赋值两个方法:getset, 在访问到count变量时,会访问到get函数,进而执行this.dep.track收集依赖。在数据变化时,会访问到set函数,执行this.dep.trigger派发更新。

RefImpl类的实例count就具备了依赖收集派发更新的功能,count是一个发布者。

接下来,我们看看watch函数是如何实现订阅者功能的。

二、watch函数

1、watch函数的定义

watch函数首先判断cb是否是函数,如果不是函数,则会发出警告,然后调用doWatch函数。

ts 复制代码
function watch(source, cb, options) {
  // cb必须是函数,否则会有警告提示
  if (!isFunction(cb)) {
    warn$1(
      `\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.`
    );
  }
  return doWatch(source, cb, options);
}

2、doWatch函数

doWatch函数首先判断传入的参数,根据不同的情况进行警告提示。然后构建watch参数,最后调用watch$1函数。

ts 复制代码
function doWatch(source, cb, options = EMPTY_OBJ) {
  // options中的参数
  const { immediate, deep, flush, once } = options;
  if (!cb) {
    if (immediate !== void 0) {
      warn$1(
        `watch() "immediate" option is only respected when using the watch(source, callback, options?) signature.`
      );
    }
    if (deep !== void 0) {
      warn$1(
        `watch() "deep" option is only respected when using the watch(source, callback, options?) signature.`
      );
    }
    if (once !== void 0) {
      warn$1(
        `watch() "once" option is only respected when using the watch(source, callback, options?) signature.`
      );
    }
  }
  // 构建watch参数
  const baseWatchOptions = extend({}, options);
  baseWatchOptions.onWarn = warn$1;
  const instance = currentInstance;
  baseWatchOptions.call = (fn, type, args) =>
    callWithAsyncErrorHandling(fn, instance, type, args);
  let isPre = false;
  if (flush === "post") {
    baseWatchOptions.scheduler = (job) => {
      queuePostRenderEffect(job, instance && instance.suspense);
    };
  } else if (flush !== "sync") {
    isPre = true;
    // scheduler函数
    baseWatchOptions.scheduler = (job, isFirstRun) => {
      if (isFirstRun) {
        job();
      } else {
        queueJob(job);
      }
    };
  }
  // augmentJob函数
  baseWatchOptions.augmentJob = (job) => {
    if (cb) {
      job.flags |= 4;
    }
    if (isPre) {
      job.flags |= 2;
      if (instance) {
        job.id = instance.uid;
        job.i = instance;
      }
    }
  };
  // 执行watch$1函数
  const watchHandle = watch$1(source, cb, baseWatchOptions);
  return watchHandle;
}

3、watch$1函数

watch$1函数中主要构建gettersetter函数,实例化ReactiveEffect得到effect,构建job函数。

ts 复制代码
function watch$1(source, cb, options = EMPTY_OBJ) {
  const { immediate, deep, once, scheduler, augmentJob, call } = options;
  const warnInvalidSource = (s) => {
    (options.onWarn || warn$2)(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`
    );
  };
  const reactiveGetter = (source2) => {
    if (deep) return source2;
    if (isShallow(source2) || deep === false || deep === 0)
      return traverse(source2, 1);
    return traverse(source2);
  };
  let effect;
  let getter;
  let cleanup;
  let boundCleanup;
  let forceTrigger = false;
  let isMultiSource = false;
  // 当前例子中的source数据源是ref,直接返回source.value,其他场景自行举例debugger尝试
  if (isRef(source)) {
    getter = () => source.value;
    forceTrigger = isShallow(source);
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source);
    forceTrigger = true;
  } else if (isArray(source)) {
    isMultiSource = true;
    forceTrigger = source.some((s) => isReactive(s) || isShallow(s));
    getter = () =>
      source.map((s) => {
        if (isRef(s)) {
          return s.value;
        } else if (isReactive(s)) {
          return reactiveGetter(s);
        } else if (isFunction(s)) {
          return call ? call(s, 2) : s();
        } else {
          warnInvalidSource(s);
        }
      });
  } else if (isFunction(source)) {
    if (cb) {
      getter = call ? () => call(source, 2) : source;
    } else {
      getter = () => {
        if (cleanup) {
          pauseTracking();
          try {
            cleanup();
          } finally {
            resetTracking();
          }
        }
        const currentEffect = activeWatcher;
        activeWatcher = effect;
        try {
          return call ? call(source, 3, [boundCleanup]) : source(boundCleanup);
        } finally {
          activeWatcher = currentEffect;
        }
      };
    }
  } else {
    getter = NOOP;
    warnInvalidSource(source);
  }
  // 如果配置参数中有deep,需要递归traverse处理
  if (cb && deep) {
    const baseGetter = getter;
    const depth = deep === true ? Infinity : deep;
    getter = () => traverse(baseGetter(), depth);
  }
  const scope = getCurrentScope();
  const watchHandle = () => {
    effect.stop();
    if (scope && scope.active) {
      remove(scope.effects, effect);
    }
  };
  // 如果配置参数中有once,执行一次后停止
  if (once && cb) {
    const _cb = cb;
    cb = (...args) => {
      _cb(...args);
      watchHandle();
    };
  }
  let oldValue = isMultiSource
    ? new Array(source.length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE;
  // 构建job函数
  const job = () => {
    // 省略逻辑,最后揭晓
  };
  if (augmentJob) {
    augmentJob(job);
  }
  // 这里是重点,实例化`ReactiveEffect`得到`effect`,一会儿细看
  effect = new ReactiveEffect(getter);
  effect.scheduler = scheduler ? () => scheduler(job, false) : job;
  boundCleanup = (fn) => onWatcherCleanup(fn, false, effect);
  cleanup = effect.onStop = () => {
    const cleanups = cleanupMap.get(effect);
    if (cleanups) {
      if (call) {
        call(cleanups, 4);
      } else {
        for (const cleanup2 of cleanups) cleanup2();
      }
      cleanupMap.delete(effect);
    }
  };
  {
    effect.onTrack = options.onTrack;
    effect.onTrigger = options.onTrigger;
  }
  if (cb) {
    // 如果参数中有immediate,立即执行一次job函数
    if (immediate) {
      job(true);
    } else {
      // 否则执行effect的run函数,将其作为旧值oldValue
      oldValue = effect.run();
    }
  } else if (scheduler) {
    scheduler(job.bind(null, true), true);
  } else {
    effect.run();
  }
  // 修改watchHandle的pause、resume、stop方法,让其指向effect,使其可以控制effect的暂停、恢复、停止
  watchHandle.pause = effect.pause.bind(effect);
  watchHandle.resume = effect.resume.bind(effect);
  watchHandle.stop = watchHandle;
  return watchHandle;
}

三、ReactiveEffect

以上源码中,oldValue = effect.run()会执行到ReactiveEffectrun方法,核心代码如下:

ts 复制代码
class ReactiveEffect {
  constructor(fn) {
    // 当前例子中是getter函数
    this.fn = fn;
    this.deps = void 0;
    this.depsTail = void 0;
    this.flags = 1 | 4;
    this.next = void 0;
    this.cleanup = void 0;
    this.scheduler = void 0;
    if (activeEffectScope && activeEffectScope.active) {
      activeEffectScope.effects.push(this);
    }
  }
  pause() {
    this.flags |= 64;
  }
  resume() {
    if (this.flags & 64) {
      this.flags &= -65;
      if (pausedQueueEffects.has(this)) {
        pausedQueueEffects.delete(this);
        this.trigger();
      }
    }
  }
  /**
   * @internal
   */
  notify() {
    if (this.flags & 2 && !(this.flags & 32)) {
      return;
    }
    if (!(this.flags & 8)) {
      batch(this);
    }
  }
  run() {
    if (!(this.flags & 1)) {
      return this.fn();
    }
    this.flags |= 2;
    cleanupEffect(this);
    prepareDeps(this);
    const prevEffect = activeSub;
    const prevShouldTrack = shouldTrack;
    // 将当前的订阅者赋值给activeSub,即effect
    activeSub = this;
    shouldTrack = true;
    try {
      // 执行getter函数,当前例子中是getter = () => source.value,即获取count的值
      return this.fn();
    } finally {
      if (activeSub !== this) {
        warn$2(
          "Active effect was not restored correctly - this is likely a Vue internal bug."
        );
      }
      cleanupDeps(this);
      activeSub = prevEffect;
      shouldTrack = prevShouldTrack;
      this.flags &= -3;
    }
  }
  stop() {}
  trigger() {
    if (this.flags & 64) {
      pausedQueueEffects.add(this);
    } else if (this.scheduler) {
      this.scheduler();
    } else {
      this.runIfDirty();
    }
  }
  runIfDirty() {}
  get dirty() {}
}

以上源码中,将实例化的ReactiveEffect对象赋值给activeSub,即effect。然后执行this.fn(),即getter = () => source.value,获取count的值,会触发RefImplget方法,核心代码如下:

ts 复制代码
this.dep.track({
  target: this,
  type: "get",
  key: "value",
});
ts 复制代码
// this.dep.track
track(debugInfo) {
  if (!activeSub || !shouldTrack || activeSub === this.computed) {
    return;
  }
  let link = this.activeLink;
  if (link === void 0 || link.sub !== activeSub) {
    // 建立activeSub和dep的关系
    link = this.activeLink = new Link(activeSub, this);
    if (!activeSub.deps) {
      // 订阅者的依赖deps指向link
      activeSub.deps = activeSub.depsTail = link;
    } else {
      // 链表的形式可以管理多个订阅者
      link.prevDep = activeSub.depsTail;
      activeSub.depsTail.nextDep = link;
      activeSub.depsTail = link;
    }
    addSub(link);
  }
  return link;
}
// Link类的实例将包含sub和dep两个属性,分别指向依赖和订阅者
class Link {
  constructor(sub, dep) {
    this.sub = sub;
    this.dep = dep;
    this.version = dep.version;
    // 链表的形式管理dep和sub
    this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0;
  }
}
// addSub函数
function addSub(link) {
  link.dep.sc++;
  if (link.sub.flags & 4) {
    if (link.dep.subsHead === void 0) {
      // link.dep.subsHead作为链表头,起初也指向link
      link.dep.subsHead = link;
    }
    // 依赖的订阅者subs也指向link
    link.dep.subs = link;
  }
}

这里需要重点关注的是activeSub.deps = activeSub.depsTail = linklink.dep.subs = link,两者都指向了同一个link对象,该对象包含depsub属性,这样activeSubdep之间建立了关系。实现了你中有我, 我中有你的双向依赖关系。从而数据变化时的找到对应的订阅者,进入触发更新的流程。

四、触发更新

当执行count.value += 1操作时,会触发countset函数,进而执行到this.dep.trigger函数,派发更新的核心代码如下:

依赖Dep触发逻辑:

ts 复制代码
// Dep的trigger函数
trigger(debugInfo) {
  this.version++;
  globalVersion++;
  this.notify(debugInfo);
}
// Dep的notify函数
notify(debugInfo) {
  startBatch();
  try {
    // 通过链表的形式,执行所有的订阅者
    for (let link = this.subs; link; link = link.prevSub) {
      // 执行订阅者link.sub的notify
      if (link.sub.notify()) {
        ;
        link.sub.dep.notify();
      }
    }
  } finally {
    endBatch();
  }
}

订阅者Sub设置当前notify逻辑:

ts 复制代码
// sub的notify函数
notify() {
  if (this.flags & 2 && !(this.flags & 32)) {
    return;
  }
  if (!(this.flags & 8)) {
    batch(this);
  }
}
// batch函数
function batch(sub, isComputed = false) {
  sub.flags |= 8;
  if (isComputed) {
    sub.next = batchedComputed;
    batchedComputed = sub;
    return;
  }
  sub.next = batchedSub;
  // batchedSub指向第一个sub
  batchedSub = sub;
}

当以上逻辑结束时,继续回到DependBatch方法:

ts 复制代码
// dep的endBatch方法
function endBatch() {
  if (--batchDepth > 0) {
    return;
  }
  // 省略侦听器相关的逻辑...
  while (batchedSub) {
    let e = batchedSub;
    batchedSub = void 0;
    while (e) {
      const next = e.next;
      e.next = void 0;
      e.flags &= -9;
      if (e.flags & 1) {
        try {
          // 执行e(batchedSub)的trigger()方法
          e.trigger();
        } catch (err) {
          if (!error) error = err;
        }
      }
      e = next;
    }
  }
  if (error) throw error;
}
// sub的trigger()方法
trigger() {
  if (this.flags & 64) {
    pausedQueueEffects.add(this);
  } else if (this.scheduler) {
    // 这里会执行scheduler函数
    this.scheduler();
  } else {
    this.runIfDirty();
  }
}

// `watch`中的`scheduler`函数逻辑如下:
baseWatchOptions.scheduler = (job, isFirstRun) => {
  if (isFirstRun) {
    job();
  } else {
    // 当前例子中执行以下逻辑
    queueJob(job);
  }
};

再看 queueJob 的核心逻辑

ts 复制代码
function queueJob(job) {
  if (!(job.flags & 1)) {
    const jobId = getId(job);
    const lastJob = queue[queue.length - 1];
    if (!lastJob || (!(job.flags & 2) && jobId >= getId(lastJob))) {
      // 将当前任务插入到数组尾部
      queue.push(job);
    } else {
      // 根据jobId,将其移动到合适的位置
      queue.splice(findInsertionIndex(jobId), 0, job);
    }
    job.flags |= 1;
    queueFlush();
  }
}
// queueFlush
function queueFlush() {
  if (!currentFlushPromise) {
    // flushJobs是异步任务,得等下个异步队列才执行
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

下一个异步队列,执行的任务:

ts 复制代码
function flushJobs(seen) {
  {
    seen = seen || /* @__PURE__ */ new Map();
  }
  const check = (job) => checkRecursiveUpdates(seen, job);
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex];
      if (job && !(job.flags & 8)) {
        if (check(job)) {
          continue;
        }
        if (job.flags & 4) {
          job.flags &= ~1;
        }
        // 在错误处理函数中执行job
        callWithErrorHandling(job, job.i, job.i ? 15 : 14);
        if (!(job.flags & 4)) {
          job.flags &= ~1;
        }
      }
    }
  } finally {
    for (; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex];
      if (job) {
        job.flags &= -2;
      }
    }
    flushIndex = -1;
    queue.length = 0;
    flushPostFlushCbs(seen);
    currentFlushPromise = null;
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen);
    }
  }
}
// 错误处理函数
function callWithErrorHandling(fn, instance, type, args) {
  try {
    return args ? fn(...args) : fn();
  } catch (err) {
    handleError(err, instance, type);
  }
}

watch中的job逻辑如下:

ts 复制代码
const job = (immediateFirstRun) => {
  if (!(effect.flags & 1) || (!effect.dirty && !immediateFirstRun)) {
    return;
  }
  if (cb) {
    // 执行effect的run函数,获取最新的值
    const newValue = effect.run();
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? newValue.some((v, i) => hasChanged(v, oldValue[i]))
        : hasChanged(newValue, oldValue))
    ) {
      if (cleanup) {
        cleanup();
      }
      const currentWatcher = activeWatcher;
      activeWatcher = effect;
      try {
        const args = [
          newValue,
          oldValue === INITIAL_WATCHER_VALUE
            ? void 0
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          boundCleanup,
        ];
        oldValue = newValue;
        // 当前例子中执行到这里
        call ? call(cb, 3, args) : cb(...args);
      } finally {
        activeWatcher = currentWatcher;
      }
    }
  } else {
    effect.run();
  }
};

以上源码中call指的是baseWatchOptions.call = (fn, type, args) => callWithAsyncErrorHandling(fn, instance, type, args),通过错误处理函数callWithAsyncErrorHandling来执行fn,也就是cb函数。

至此,从数据count变化到watchcb函数执行,整个流程就结束了。其他细节还需要自己举例来通过debugger的方式来验证,这里不再赘述。


这是一个vue3.5.18源码精读系列,欢迎大家持续关注,一起学习,共同进步。

相关推荐
OEC小胖胖3 小时前
掌握表单:React中的受控组件与表单处理
前端·javascript·react.js·前端框架·react·web
coding随想4 小时前
手机旋转也能触发代码?揭秘前端DeviceOrientation事件的神奇力量!
前端
Mintopia4 小时前
AIGC 训练数据的清洗与标注:技术痛点与自动化方案
前端·javascript·aigc
zzywxc7874 小时前
如何通过 AI IDE 集成开发工具快速生成简易留言板系统
javascript·ide·vue.js·人工智能·spring cloud·架构·dubbo
小喷友4 小时前
第9章 鸿蒙微内核与系统架构
前端·app·harmonyos
Hilaku4 小时前
我最近面试前端,发现一个很有意思的现象..
前端·javascript·面试
Js_cold4 小时前
Notepad++常用设置
前端·javascript·fpga开发·notepad++
Mintopia4 小时前
Next.js 新数据获取三剑客:fetch() + cache() + use —— 从引擎盖下聊到赛道上
前端·javascript·next.js
Juchecar4 小时前
Vite = 让 Vue 开发像写 HTML 一样快的现代工具
前端·vue.js