Skip to content

3.5版本响应式

在vue3.5中,响应式系统由三个类构成,Dep, Subscriber, Link, Subscriber就是之前讲的Effect, 在3.5中 Effect 继承实现了Subscriber, 所以EffectSubscriber是等价的。在3.5中新增了Link,主要是用来连接DepSubscriber的,3.5中DepSubscriber不再直接联系,而是通过中间层Link进行连接。常见的 Depref, reactive, computed,常见的 Subscribercomputed, watch, watchEffect, render函数

在3.5版本中,DepSubscriber依然是多对多的关系,只是这种关系通过Link来维护

接下来将结合3.5源码讲述相比3.5之前的版本,响应式系统有哪些变化。

首先,我们先看下源码中对 Dep, Subscriber, Link类中有哪些重要的属性,先有个印象就行

ts

export class Link {

  nextDep?: Link // 下一个 dep
  prevDep?: Link // 上一个 dep
  nextSub?: Link // 下一个 Subscriber
  prevSub?: Link // 上一个 Subscriber

  constructor(
    // Link节点对应的 Subscriber
    public sub: Subscriber,
    // Link节点对应的 Dep
    public dep: Dep,
    
  ) {
  }
}

export class Dep {

  // sub属性,指向队尾最后一个Subscriber
  subs?: Link = undefined

  // 收集依赖
  track()
}

export interface Subscriber extends DebuggerOptions {

  // Subscriber中订阅的第一个dep
  deps?: Link
  // Subscriber中订阅的最后一个dep
  depsTail?: Link
  // 执行自身的更新逻辑
  notify()
}

依赖收集

我们以下面这个例子举例, 看完之后你就会知道上面的每个属性都代表什么意思了

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { createApp, computed, ref, watchEffect } from './vue.runtime.esm-bundler.js'
    
    const count1 = ref(1)
    const count2 = ref(2)

    watchEffect(() => {
      console.log(count1.value + count2.value);
    })

    watchEffect(() => {
      console.log(count1.value + count2.value);
    })
    
    count1.value ++
  </script>
</body>
</html>

watchEffectSubscriber的一种,当脚本开始执行的时候,我们会先执行第一个 watchEffect 函数,自然就会执行 effect中的run函数(effect 是 Subscriber的具体实现,两者等价, 后面出现的effect都为 Subscriber的具体实现), 执行run函数的时候,我们会把当前effect的实例,即Subscriber放到全局上, 然后执行我们传给 watchEffect的函数, 而传给watchEffect的函数中,我们访问到了count1count2, 会对这两个ref实例进行依赖收集

ts
export class ReactiveEffect<T = any>
  implements Subscriber, ReactiveEffectOptions
{
    run(): T {
    // 将当前的 Subscriber 实例放到全局上
    activeSub = this
    shouldTrack = true

    // 执行传给 watchEffect 的函数,开始对 count1 和 count2进行依赖收集
    return this.fn()
  }
}

接下来我们开始依赖收集的过程, 我们先访问 count1

ts

class RefImpl<T = any> {
  // 每一个 ref上都有一个dep用来依赖收集
  dep: Dep = new Dep()
  // 当访问到 count1 会进来这里
  get value() {
    // 让 count1 收集当前的 Subscriber
    this.dep.track()
    return this._value
  }
}

export class Dep {
  track(): Link | undefined {
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return
    }
    // activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
    let link = this.activeLink
    // 成立
    if (link === undefined || link.sub !== activeSub) {
      // 初始化link节点,并把 dep和当前的 subscriber实例传进去
      link = this.activeLink = new Link(activeSub, this)

      // 此时第一个 watchEffect 还没有 订阅过 dep,所以是空的,条件成立
      if (!activeSub.deps) {
        // 由于是第一个收集的dep, 让当前的 subscriber的 deps队头和队尾都指向新建的link节点
        activeSub.deps = activeSub.depsTail = link
      } else {
        // 这个分支暂时还走不到
        link.prevDep = activeSub.depsTail
        activeSub.depsTail!.nextDep = link
        activeSub.depsTail = link
      }
      // 调用 addSub方法,维护 dep和 link的关系
      addSub(link)
    }
    return link
  }  
}

// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例 
function addSub(link: Link) {
  // count1对应的dep还没有subs属性
  const currentTail = link.dep.subs
  if (currentTail !== link) {
    link.prevSub = currentTail
    if (currentTail) currentTail.nextSub = link
  }

  // 主要执行这一句,让 count1的dep.subs属性指向新创建的link
  link.dep.subs = link
}

记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例

我们来画个图,看看此刻 Subscriber, Dep, Link之间的关系

此时的响应式系统角色有 第1个watcherEffect, count1, Link1

Link1 节点中保存了count1对应的dep以及watchEffect1, 分别用的是 subdep属性, 每个link节点都这样,后面不再讲

watchEffectSubscriber,它有两个属性 depsdepsTaildeps指向 watchEffect订阅的第一个dep对应的Link,这里就是Link1, depsTail指向watchEffect订阅的最后一个dep对应的Link,这里由于只有一个,因此也是 Link1

count1Dep,它有一个属性 subs,指向最后一个收集它的watchEffect对应的Link节点,这里是Link1

为什么要这样做? 本质上就是维护一个双向链表 往下看就知道了

接下来访问count2, 进行依赖收集

ts

class RefImpl<T = any> {
  // 每一个 ref上都有一个dep用来依赖收集
  dep: Dep = new Dep()
  // 当访问到 count2 会进来这里
  get value() {
    // 让 count2 收集当前的 Subscriber
    this.dep.track()
    return this._value
  }
}

export class Dep {
  track(): Link | undefined {
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return
    }
    // activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
    let link = this.activeLink
    // 成立
    if (link === undefined || link.sub !== activeSub) {
      // 初始化link节点,并把 dep和当前的 subscriber实例传进去
      link = this.activeLink = new Link(activeSub, this)

      // 结合上面的图,此时 watchEffect1 已经订阅了 count1, 不成立
      if (!activeSub.deps) {
        activeSub.deps = activeSub.depsTail = link
      } else {
        // 走这里
        // 让新创建的Link2的 prevDep属性指向activeSub的最后一个link,目前是Link1
        link.prevDep = activeSub.depsTail
        // 让 Link1 的 nextDep 指向 Link2
        activeSub.depsTail!.nextDep = link
        // 让 Link2 成为 activeSub 即 watchEffect1 的队尾节点
        activeSub.depsTail = link
      }
      // 调用 addSub方法,维护 dep和 link的关系
      addSub(link)
    }
    return link
  }  
}

// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例 
function addSub(link: Link) {
  // count2对应的dep还没有subs属性
  const currentTail = link.dep.subs
  if (currentTail !== link) {
    link.prevSub = currentTail
    if (currentTail) currentTail.nextSub = link
  }

  // 主要执行这一句,让 count2的dep.subs属性指向新创建的link2
  link.dep.subs = link
}

此时的图是这样的

接下来我们访问 watchEffect2, 同样会对 count1count2进行依赖收集

ts
// 第2个 watchEffect, watcheEffect2
watchEffect(() => {
  console.log(count1.value + count2.value);
})

count1依赖收集

ts

class RefImpl<T = any> {
  // 每一个 ref上都有一个dep用来依赖收集
  dep: Dep = new Dep()
  // 当访问到 count1 会进来这里
  get value() {
    // 让 count1 收集当前的 Subscriber
    this.dep.track()
    return this._value
  }
}

export class Dep {
  track(): Link | undefined {
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return
    }
    // activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
    let link = this.activeLink
    // 成立
    if (link === undefined || link.sub !== activeSub) {
      // 初始化link3节点,并把 dep和当前的 subscriber实例传进去
      link = this.activeLink = new Link(activeSub, this)

      // 此时 watchEffect2 刚刚执行,还没有订阅任何变量 成立
      if (!activeSub.deps) {
        // 走这里
        activeSub.deps = activeSub.depsTail = link
      } else {
        link.prevDep = activeSub.depsTail
        activeSub.depsTail!.nextDep = link
        activeSub.depsTail = link
      }
      // 调用 addSub方法,维护 dep和 link的关系
      addSub(link)
    }
    return link
  }  
}

// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例 
function addSub(link: Link) {
  // count1对应的dep已经有subs了,因为之前收集过 watchEffect1了,结合上面的图,此时currentTail就是 Link1
  const currentTail = link.dep.subs
  // Link1 不等于 Link3 成立
  if (currentTail !== link) {
    // 让 Link3 的 prevSub 指向 Link1 
    link.prevSub = currentTail
    // 让 Link1 的 nextSub指向 Link3
    if (currentTail) currentTail.nextSub = link
  }

  // 让 count1的dep.subs属性指向新创建的link3
  link.dep.subs = link
}

此时的图是这样子的

接下来 watchEffect2 访问 count2

ts

class RefImpl<T = any> {
  // 每一个 ref上都有一个dep用来依赖收集
  dep: Dep = new Dep()
  // 当访问到 count2 会进来这里
  get value() {
    // 让 count2 收集当前的 Subscriber
    this.dep.track()
    return this._value
  }
}

export class Dep {
  track(): Link | undefined {
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return
    }
    // activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
    let link = this.activeLink
    // 成立
    if (link === undefined || link.sub !== activeSub) {
      // 初始化link4节点,并把 dep和当前的 subscriber实例传进去
      link = this.activeLink = new Link(activeSub, this)

      // 此时 watchEffect2 已经订阅过 count1, 不成立
      if (!activeSub.deps) {
        activeSub.deps = activeSub.depsTail = link
      } else {
        // 让 link4.prevDep指向 activeSub.depsTail,结合上面的图,此时是 Link3
        // 因此是让 Link4.prevDep 指向 Link3
        link.prevDep = activeSub.depsTail
        // 让 Link3.nextDep指向 link4
        activeSub.depsTail!.nextDep = link
        // 让 watchEffect2的队尾 depsTail指向 Link4
        activeSub.depsTail = link
      }
      // 调用 addSub方法,维护 dep和 link的关系
      addSub(link)
    }
    return link
  }  
}

// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例 
function addSub(link: Link) {
  // count2对应的dep已经有subs了,因为之前收集过 watchEffect1了,结合上面的图,此时currentTail就是 Link2
  const currentTail = link.dep.subs
  // Link2 不等于 Link4 成立
  if (currentTail !== link) {
    // 让 Link4 的 prevSub 指向 Link2 
    link.prevSub = currentTail
    // 让 Link2 的 nextSub指向 Link4
    if (currentTail) currentTail.nextSub = link
  }

  // 让 count2的dep.subs属性指向新创建的link4
  link.dep.subs = link
}

此时的图是这样子的

至此,依赖收集的过程结束

为什么要维护这个结构呢? 因为当响应式变量发生变化的时候,可以通过响应式变量的dep属性,访问到dep.subs

举个例子,当count1发生变化的时候,我们就可以取出它对应的dep,进而通过dep.subs访问到 dep维护的最后一个Link节点,也就是Link3, 而Link3sub属性,恰好就是watchEffect2, 再通过 Link3prevSub就可以访问到 Link1, 再通过 Link1的sub属性访问到 watchEffect1,这个也是依赖触发更新的过程

ts
export class Dep {
  notify(): void {
    // 从队尾到队头依次取出对应的Subscriber执行
    for (let link = this.subs; link; link = link.prevSub) {
      link.sub.notify()
    }
  }
}

清除不用的依赖

换个例子

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
    
    const app = createApp({
      template: `
        <p>{{ doubleCount }}</p>
        <button @click="changeFlag">切换flag</button>
      `,
      setup() {
        const count1 = ref(1)
        const count2 = ref(10)
        const flag = ref(true)

        const doubleCount = computed(() => {
          console.log('computed');
          if(flag.value) {
            return count1.value * 2
          } else {
            return count2.value * 2
          }
        })

        const changeFlag = () => {
          flag.value = !flag.value
        }

        return {
          count1,
          count2,
          flag,
          doubleCount,
          changeFlag,
        }
      }
    })
    app.mount('#app')
  </script>
</body>
</html>

当依赖收集完毕后,我们的响应式系统是这样的