vue3.5.18源码:computed 在发布订阅者模式中的双重角色

computed在发布订阅者模式中的双重角色,是指它既是订阅者又是发布者。

响应式就是当数据变化时,会自动执行某些操作。

但是,computed的响应式又和refreactive有所不同,refreactive是发布订阅者模式中的发布者,而computed既是发布者,又是订阅者。

本文就以一个例子开头,详细介绍computed的双重角色。

ts 复制代码
// 以下整个所有的代码可以看做是渲染函数render
<div id="app"></div>
<script>
    const App = {
        template:`{{ computedCount }}<button @click="plus">plus</button>
        `,
        setup() {
            // 定义响应式数据,ref是RefImpl类的实例对象
            debugger;
            const count = Vue.ref(0);
            // 定义计算属性,computed中的核心类是ComputedRefImpl
            debugger
            const computedCount = Vue.computed(() => {
                return count.value + 1;
            })
            // 定义修改数据的方法
            const plus = () => {
                debugger;
                count.value++;
            }
            // 暴露给模板
            return {
                plus,
                computedCount
            };
        }
    };
    // 创建应用并挂载
    const app = Vue.createApp(App);
    app.mount("#app");
</script>

以上例子中,定义响应式数据count,初次渲染count的值为0。定义的计算属性computedCount的计算值依赖count,所以computedCountcount的订阅者。

全文渲染函数render在生成vnode时,会访问到computedCount,所以rendercomputedCount的订阅者。

当点击修改数据count的值会触发computedCount的更新,进而触发render的更新。

接下来首先介绍整个过程中的三个核心类RefImplComputedRefImplReactiveEffect

一、生成类(时机:setup)

接下来我们首先介绍这三个实例对象所对应的生成类。

1、RefImpl

const count = Vue.ref(1)执行,最终会返回RefImpl的实例。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(1)时,会执行响应式数据初始化函数,返回一个RefImpl对象,该对象包含了一个依赖管理器:this.dep = new Dep()。并且定义了取值和赋值两个方法:getset, 在首次渲染时,会访问到get函数,进而执行this.dep.track收集依赖。在数据变化时,会访问到set函数,执行this.dep.trigger派发更新。

至此,count就具备了依赖收集派发更新的功能。

2、ComputedRefImpl

Vue.computed(() => { return count.value + 1; })开始,会先获得gettersetter函数,然后作为ComputedRefImpl类的参数来创建实例。核心逻辑如下:

ts 复制代码
// computed函数
const computed = (getterOrOptions, debugOptions) => {
  const c = computed$1(getterOrOptions, debugOptions, isInSSRComponentSetup);
  {
    const i = getCurrentInstance();
    if (i && i.appContext.config.warnRecursiveComputed) {
      c._warnRecursive = true;
    }
  }
  return c;
};
// computed$1函数
function computed$1(getterOrOptions, debugOptions, isSSR = false) {
  let getter;
  let setter;
  if (isFunction(getterOrOptions)) {
    // 如果是函数,直接赋值为getter
    getter = getterOrOptions;
  } else {
    // 如果是对象,直接获取getterOrOptions中的get
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  // 实例化ComputedRefImpl类
  const cRef = new ComputedRefImpl(getter, setter, isSSR);
  if (debugOptions && !isSSR) {
    cRef.onTrack = debugOptions.onTrack;
    cRef.onTrigger = debugOptions.onTrigger;
  }
  return cRef;
}
// ComputedRefImpl类
class ComputedRefImpl {
  constructor(fn, setter, isSSR) {
    // 这里的fn就是传入的参数getter
    this.fn = fn;
    this.setter = setter;
    this._value = void 0;
    // 以自身实例为参数,作为Dep的参数
    this.dep = new Dep(this);
    this.__v_isRef = true;
    this.deps = void 0;
    this.depsTail = void 0;
    this.flags = 16;
    this.globalVersion = globalVersion - 1;
    this.next = void 0;
    this.effect = this;
    this["__v_isReadonly"] = !setter;
    this.isSSR = isSSR;
  }
  // notify函数
  notify() {
    this.flags |= 16;
    if (!(this.flags & 8) && activeSub !== this) {
      batch(this, true);
      return true;
    }
  }
  // get函数
  get value() {
    // 收集订阅者(渲染函数),建立`computed`和`render`之间的关系
    const link = this.dep.track({
      target: this,
      type: "get",
      key: "value",
    });
    // 将自己作为订阅者,让`count`订阅,建立`computed`和`count`之间的关系
    refreshComputed(this);
    if (link) {
      link.version = this.dep.version;
    }
    return this._value;
  }
  // set函数
  set value(newValue) {
    if (this.setter) {
      this.setter(newValue);
    } else {
      warn$2("Write operation failed: computed value is readonly");
    }
  }
}

这里可以看出ComputedRefImpl类中也包含getset方法,当访问computed.value时,会执行get方法,该方法会执行this.dep.track收集依赖,建立computedrender之间的关系。

同时,会执行refreshComputed函数,该函数的作用,就是将computedcount建立关系,即computed订阅count,当count发生变化时,会派发更新,通知computed重新计算。具体实现在构建三者关系时详细介绍,这里先有个印象。

接下来介绍最后和渲染相关的ReactiveEffect类。

3、ReactiveEffect

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

ts 复制代码
// ReactiveEffect类
class ReactiveEffect {
  constructor(fn) {
    // 这里的fn就是渲染函数componentUpdateFn
    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

介绍完RefImplComputedRefImplReactiveEffect类后,我们再介绍他们之间关系时如何建立的。

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

在首次渲染生成vnode时,会访问到computedCount,进而执行到ComputedRefImplget函数。get函数中的核心作用就是建立computedrender之间的关系,以及computedcount之间的关系。

ts 复制代码
// get函数
get value() {
  // 建立`computed`和`render`之间的关系
  const link = this.dep.track({
    target: this,
    type: "get",
    key: "value",
  });
  // 建立`computed`和`count`之间的关系
  refreshComputed(this);
  if (link) {
    link.version = this.dep.version;
  }
  return this._value;
}

1、computedrender之间的关系

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之间建立了关系。实现了你中有我, 我中有你的双向依赖关系。

2、computedcount之间的关系

ts 复制代码
function refreshComputed(computed) {
  // 省略其他逻辑
  const dep = computed.dep;
  const prevSub = activeSub;
  const prevShouldTrack = shouldTrack;
  // 这里将activeSub赋值为computed,以方便建立computed和count之间的关系
  activeSub = computed;
  shouldTrack = true;
  try {
    prepareDeps(computed);
    // 这里执行computed.fn,即count.value += 1
    const value = computed.fn(computed._value);
    if (dep.version === 0 || hasChanged(value, computed._value)) {
      computed.flags |= 128;
      computed._value = value;
      dep.version++;
    }
  } catch (err) {
    dep.version++;
    throw err;
  } finally {
    // 这里将activeSub赋值为prevSub,恢复activeSub的值
    activeSub = prevSub;
    shouldTrack = prevShouldTrack;
    cleanupDeps(computed);
    computed.flags &= -3;
  }
}

以上代码中通过activeSub = computed的方式,将computed作为订阅者,再通过const value = computed.fn(computed._value)的方式执行到代码count.value += 1。近而触发countget函数,即this.dep.track({ target: this, type: "get", key: "value", });。这里的逻辑和computed-render之间的关系的逻辑类似,请自行debugger断点调试。

此时,rendercomputedcount之间就建立了依赖关系。接下来看,当count的值发生变化时,是如何触发render重新渲染的。

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

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

1、依赖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();
  }
}

以上代码中,执行link.sub.notify(),即执行computednotify函数,核心逻辑如下:

2、订阅者ComputedRefImpl触发逻辑

ts 复制代码
// sub的notify函数
notify() {
  this.flags |= 16;
  if (!(this.flags & 8) &&
  activeSub !== this) {
    batch(this, true);
    return true;
  }
}

需要注意的是,这里的notify执行完之后,执行了return true,即:

ts 复制代码
if (link.sub.notify()) {
  link.sub.dep.notify();
}

中,link.sub.notify()返回了true,进而执行了link.sub.dep.notify()link.sub.notify()表示的是computednotify

那么,computed作为发布者,link.sub.dep.notify()表示的就是ReactiveEffectnotify。即rendernotify

当以上逻辑结束时,会执行到.finallyDependBatch函数,核心代码如下:

3、订阅者ReactiveEffect视图渲染

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();
  }
}

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

总结:计算属性computed作为订阅者,在访问count时实现了订阅;作为发布者,在生成vnode时会访问到computed对应的数据,渲染函数render订阅了它。当count发生变化时,会触发computedset函数,进而触发render重新渲染。

相关推荐
跟橙姐学代码2 分钟前
Python 集合:人生中最简单的真理,只有一次
前端·python·ipython
复苏季风3 分钟前
站在2025 年 来看,现在应该怎么入门CSS
前端·css
pepedd8644 分钟前
深度解剖 Vue3 架构:编译时 + 运行时的协作
前端·vue.js·trae
一枚前端小能手7 分钟前
🧪 改个代码就出Bug的恐惧,前端测试来帮忙
前端·测试
s3xysteak8 分钟前
我要成为vue高手02:数据传递
前端·javascript·vue.js
pepedd8648 分钟前
深入理解Vue响应式原理-源码解析
前端·vue.js·trae
Warren9817 分钟前
Spring Boot 整合网易163邮箱发送邮件实现找回密码功能
数据库·vue.js·spring boot·redis·后端·python·spring
Aphasia31119 分钟前
react常用hook
前端·react.js·面试
hfd199043 分钟前
Chrome 插件开发实战:从入门到进阶
前端·chrome
菠萝+冰1 小时前
CSS 定位的核心属性:position
前端·css