vue3.5.18源码:reactive和单例、代理和发布订阅者之间的关系

reactive是一个特别常用的API,在Vue3中,reactive函数是用来创建一个响应式对象。底层用到了哪些技术呢?我们一起来看一下。 学完本文,你将获得以下知识:

  • reactive函数底层实现原理
  • reactive函数与单例模式之间的关系
  • reactive函数与代理模式之间的关系
  • reactive函数中类的继承关系
  • reactive函数与发布订阅者模式之间的关系
ts 复制代码
<body>
    <div id="app"></div>
    <script>
        const App = {
            template:`
                {{ reactiveObj.count }}
                <button @click="changeCount">修改数据</button>
            `,
            setup() {
                debugger;
                // 定义响应式对象
                const reactiveObj = Vue.reactive({
                    count: 1
                })
                // 定义修改数据的方法
                const changeCount = function() {
                    reactiveObj.count += 1;
                }
                // 暴露给模板
                return {
                    reactiveObj,
                    changeCount
                };
            }
        };
        // 创建应用并挂载
        const app = Vue.createApp(App);
        app.mount("#app");
    </script>
</body>

以上例子中,我们定义了一个响应式对象reactiveobject,然后通过changeCount方法修改了reactiveObj中的count属性,此时,页面中的数据也会随之更新。下面我们来看一下reactive函数是如何实现的。

ts 复制代码
// reactive函数
function reactive(target) {
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}
// createReactiveObject函数
function createReactiveObject(
  target,
  isReadonly2,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  // 如果不是一个对象的话,直接返回。
  const targetType = getTargetType(target);
  if (targetType === 0) {
    return target;
  }
  // 如果在proxyMap中能找到缓存的话,就直接返回。
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 代理模式:在访问target之前先访问collectionHandlers
  const proxy = new Proxy(
    target,
    targetType === 2 ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}

一、单例模式

单例模式指的是全局对象中有且只有一个实例对象,当前例子:

ts 复制代码
const existingProxy = proxyMap.get(target);
if (existingProxy) {
  return existingProxy;
}

以上如果existingProxy存在,直接返回,否则才会执行后续逻辑,确保了target有且只有一个代理对象。

接下来看代理模式。

二、代理模式

以上例子中代码为:

ts 复制代码
// 这里targetType为1,执行的是baseHandlers
const proxy = new Proxy(
  target,
  targetType === 2 ? collectionHandlers : baseHandlers
);

我们知道代理模式指的是在访问目标对象之前先访问代理对象,那么在reactive函数中,target就是reactiveObj,那么在访问reactiveObj之前,会先访问baseHandlers函数,继续看baseHandlers函数中用到的继承关系。

三、类的继承

baseHandlers来源于开头例子中的mutableHandlers

ts 复制代码
// 处理函数实例
const mutableHandlers = new MutableReactiveHandler();
//  MutableReactiveHandler类
class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow2 = false) {
    super(false, isShallow2);
  }
  set(target, key, value, receiver) {
    let oldValue = target[key];
    // ...
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    const result = Reflect.set(
      target,
      key,
      value,
      isRef(target) ? target : receiver
    );
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add", key, value);
      } else if (hasChanged(value, oldValue)) {
        // 在修改数据时,触发set函数,触发试图更新
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
  deleteProperty(target, key) {}
  has(target, key) {}
  ownKeys(target) {}
}
// BaseReactiveHandler类
class BaseReactiveHandler {
  constructor(_isReadonly = false, _isShallow = false) {
    this._isReadonly = _isReadonly;
    this._isShallow = _isShallow;
  }
  get(target, key, receiver) {
    // 省略其他支线逻辑
    const isReadonly2 = this._isReadonly,
      isShallow2 = this._isShallow;
    if (!isReadonly2) {
      track(target, "get", key);
    }
    return res;
  }
}

从以上例子中,可以看出,mutableHandlersMutableReactiveHandler的实例,而MutableReactiveHandler继承了BaseReactiveHandler,在BaseReactiveHandler中,定义了get方法,该方法中调用了track方法,该方法用于收集依赖。在MutableReactiveHandler中,定义了set方法,该方法中调用了trigger方法,该方法用于触发更新。

接下来,我们看一下在track中发布者是如何收集订阅者的,在trigger中发布者又是如何触发订阅者更新视图的。

四、发布订阅者模式

1、Dep

再构建vnode时,会访问到reactiveObj中的count属性。先触发get函数,接着会触发track函数:

ts 复制代码
// 全局对象targetMap
const targetMap = /* @__PURE__ */ new WeakMap();
// track函数
function track(target, type, key) {
  if (shouldTrack && activeSub) {
    // 获取targetMap
    let depsMap = targetMap.get(target);
    // 如果depsMap不存在,以target为key,以创建一个Map对象为value,创建一个target和depsMap的映射关系
    if (!depsMap) {
      targetMap.set(target, (depsMap = /* @__PURE__ */ new Map()));
    }
    // 在depsMap中获取key对应的dep。
    let dep = depsMap.get(key);
    // 如果dep不存在,以当前key值为key,创建它与Dep实例之间的映射关系
    if (!dep) {
      depsMap.set(key, (dep = new Dep()));
      dep.map = depsMap;
      dep.key = key;
    }
    {
      dep.track({
        target,
        type,
        key,
      });
    }
  }
}

targetMap中可以通过target(即ReactiveObj)获取到depsMap,在depsMap中可以通过key(即count)获取到dep,接下来看 dep对应的Dep类的实现。

ts 复制代码
// 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) {
    // 通知更新,省略逻辑...
  }
}

至此,从reactiveObj开始,就找到了最终控制发布订阅者关系的Dep类。

这里我们先看看控制视图的订阅者对应的ReactiveEffect类。

2、ReactiveEffect

ReactiveEffect的实例化对象就是一个订阅者。渲染过程的调度过程就是由其实例化对象中的run方法进行的,核心代码如下:

ts 复制代码
// ReactiveEffect类
class ReactiveEffect {
  constructor(fn) {
    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() {
    // 省略逻辑...
  }
  resume() {
    // 省略逻辑...
  }
  notify() {
    // 省略逻辑...
  }
  run() {
    // 当前活跃的订阅者就是this,即effect
    activeSub = this;
    try {
      return this.fn();
    } finally {
      // 省略逻辑...
    }
  }
  stop() {
    // 省略逻辑...
  }
  trigger() {
    // 省略逻辑...
  }
  runIfDirty() {
    // 省略逻辑...
  }
  get dirty() {
    // 省略逻辑...
  }
}

在上述对象中,this.fn=fn,实例化时指向的是实例化的执行函数componentUpdateFn,获取vnodepatch的逻辑都在其中。const update = instance.update = effect.run.bind(effect),执行update时,实际执行的是ReactiveEffectrun方法。此时,就将当前实例赋值给了activeSub

说明白了DepReactiveEffect 的作用,再来看看订阅者是如何和发布者发生关联的。

3、构建关系(时机:首次渲染)

在首次渲染生成vnode时,会访问到reactiveObj中的count,进而执行到BaseReactiveHandler类中的get函数。该函数调用track(target, "get", key),即dep.track({ target, type, key });进行发布者和订阅者的关系构建,核心代码如下:

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之间建立了关系。实现了你中有我, 我中有你的双向依赖关系。从而数据变化时的找到对应的订阅者,进入触发更新的流程。

4、触发更新(时机:数据修改)

当执行reactiveObj.count += 1; 操作时,会触发trigger(target, "set", key, value, oldValue);函数,进而执行到this.dep.trigger函数,派发更新的核心代码如下:

依赖MutableReactiveHandler中的set逻辑,进而执行其中的trigger逻辑:

ts 复制代码
// MutableReactiveHandler的trigger函数
trigger(target, "set", key, value, oldValue);
// 以下函数中省略所有支线逻辑,如需要请查阅源码
function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target);
  const run = (dep) => {
    if (dep) {
      {
        // 执行dep中的trigger函数
        dep.trigger({
          target,
          type,
          key,
          newValue,
          oldValue,
          oldTarget,
        });
      }
    }
  };
  if (key !== void 0 || depsMap.has(void 0)) {
    // 开始执行run函数
    run(depsMap.get(key));
  }
}

其中通过depsMap = targetMap.get(target)获得了desMap,即实例化后的dep,我们接着看其中的trigger函数:

ts 复制代码
// Dep类中的trigger函数
trigger(debugInfo) {
  this.version++;
  globalVersion++;
  this.notify(debugInfo);
}
// Dep的notify函数
notify() {
  for (let link = this.subs; link; link = link.prevSub) {
    if (link.sub.notify()) {
      ;
      link.sub.dep.notify();
    }
  }
}

当执行到ink.sub.notify()时,即执行到了订阅者类ReactiveEffectnotify函数,核心代码如下:

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) {
    // 即effect.scheduler = () => queueJob(job);
    this.scheduler();
  } else {
    this.runIfDirty();
  }
}

再看 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);
    }
  }
// 因为job = instance.job = effect.runIfDirty.bind(effect);,所以,fn就是runIfDirty函数
runIfDirty() {
  if (isDirty(this)) {
    // 这里就是最终的渲染逻辑
    this.run();
  }
}

以上,就是从数据reactiveObj.count变化,到订阅者执行渲染逻辑的全过程。

总结:reactive函数会返回一个代理对象proxy,在生成vnode的过程中需要访问到reactiveObj.count,首先会访问到过起代理对象中的执行逻辑baseHandlersbaseHandlers中的基类和继承类分别包含setget函数,访问时数据是触发get中的track函数,进行依赖收集;在数据改变时,会让订阅者执行link.sub.notify()的操作,从而执行数据变化后的最终渲染函数。

相关推荐
孟祥_成都2 分钟前
AI 术语满天飞?90% 的人只懂名词,不懂为什么!
前端·人工智能
Lupino29 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘35 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo37 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山38 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点39 分钟前
手写promise
前端·promise
国思RDIF框架1 小时前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
颜酱1 小时前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
Mintopia1 小时前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名1 小时前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员