Vue3源码05 : Vue3响应式系统源码实现(2/2)

2022-09-27 14:24:18 浏览数 (1)

  • Vue3源码01 : 代码管理策略-monorepo
  • Vue3源码02: 项目构建流程和源码调试方法
  • Vue3源码03: Vue3响应式核心原理
  • Vue3源码04: Vue3响应式系统源码实现1/2

在前面的文章中,我们分析了reactiveeffectmutableHandlers之间的相互协作关系。本文会重点分析effect.ts中的每一个API及相关代码的实现细节,因为响应式原理的核心是Proxy代理机制,还有一个特别重要的就是对依赖关系的管理,而依赖关系的管理主要逻辑在effect.ts文件中完成,同时还会带着大家阅读computed的源码实现。鉴于涉及了响应式系统的很多实现细节,这是一篇比较长的文章,文字加代码超过2万个字符,请大家在耐心和时间上做好准备,阅读完本文相信会让大家对Vue3响应式系统有深刻的理解。

依赖收集及触发更新

在前面两篇文章中,我们知道了,响应式系统的核心,就是依赖收集和触发更新。在代理对象属性值被使用的时候,需要保存数据属性与依赖函数的关系;当代理对象的属性的只被修改的时候,需要将保存的该属性对应的依赖函数进行遍历并执行。可以用下面这张图来进一步理解这个基本原理:

图中的第一步,将普通对象处理成代理对象,是eactiveshallowReactive等函数来实现的,这些函数内部会创建Proxy实例,同时会为这些Proxy实例设置处理器,处理器中又会进行依赖收集和触发更新。而依赖收集和触发更新的具体实现是在effect.ts文件中完成。对于依赖的管理可以说是响应式系统的基石,下面我们就来看看effect.ts中对外暴露的那些函数的具体实现。

effect

代码语言:javascript复制
// 代码片段1
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 分析点1:参数处理
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 分析点2: ReactiveEffect
  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    // 分析点3: ReactiveScope
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    _effect.run()
  }
  // 分析点4: 作用域
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

上篇文章中只分析了effect函数的核心逻辑,这里我们来探索其中的实现细节。

分析点1: 参数处理

代码语言:javascript复制
// 代码片段2
if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
}

这里的处理很巧妙,为了防止传入的函数fn本身就是effect函数,这里规避了对函数重复包裹。

分析点2: ReactiveEffect

下面先分析类ReactiveEffect中几个属性值的含义,接着分析runstop等方法。

几个属性的含义和用途
代码语言:javascript复制
// 代码片段3
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  computed?: ComputedRefImpl<T>
  allowRecurse?: boolean
  onStop?: () => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  // 此处省略其他代码...
}

fn属性

基础不扎实的朋友可能会疑惑,因为没看见有一个属性叫做fn,只有一个构造函数的函数参数是fn,事实上这是一个TypeScript中和构造函数参数相关的语法,可以查阅相关文档理解。之所以先解释fn这个属性,是因为它太重要了。对于依赖收集而言,本质上收集的就是这个函数,虽然在实际代码中存储的依赖关系中,保存的是ReactiveEffect对象。ReactiveEffect实例存在的意义其实就是方便管理这个fn函数。事实上,依赖收集完成,触发更新的时候,就是触发这里的fn函数执行。

active属性

这里的active属性是用于标识该active属性对应的ReactiveEffect实例是否还应该参与正常的依赖收集活动。具体含义在本文run方法分析的部分进行解释。这里需要知道,正常情况下,active的值都是true,就目前响应式源码而言,只有人为干预才会将active的值变为false,比如用户手动调用了ReactiveEffect实例的stop方法,或者EffectScope实例调用了自己的stop方法,触发了自己所关联的ReactiveEffect实例的stop方法。至于EffectScope的相关内容在本文其他部分会有讲解。

deps属性

这个属性也特别重要,为什么呢?我们常常讲依赖收集,讲得大概是保存这样的对应关系:

代码语言:javascript复制
// 代码片段4
// 假设有对象target,target有属性,prop1、prop2;
// target有对应代理对象proxyTarget; 
// proxyTarget.prop1被函数fn1、fn2使用;
// proxyTarget.prop2被函数fn2、fn3使用
// 则对应关系可表示为如下形式:
{
    target:{
        prop1:[fn1, fn2],// 这里用数组表示,实际上是用Set集合保存
        prop2:[fn2, fn3]
    }
}

“注意:下面关于deps的描述和源码实现是不一致的,但是其本质思想确实是相似的 ”

这与上面的示意图的描述是一致的,但是,假如函数fn2在某种情况下不希望被依赖收集,不希望在proxyTarget.prop1proxyTarget.prop2值改变后自动执行fn2怎么办呢?这里的deps就发挥了作用,利用deps可以保存这样一组关系:

代码语言:javascript复制
//代码片段5
{
    fn2:[target.prop1, target.prop2]
}

当然,我们前面说过依赖函数实际上是包裹在了ReactiveEffect实例中,所以可以这样描述这组关系:

代码语言:javascript复制
// 代码片段6
{
    fn2所在的ReactiveEffect实例:[target.prop1, target.prop2]
}

有了这组关系,就可以讲fn2proxyTarget.prop1proxyTarget.prop2进行解绑,解绑方式很简单,就是找到代码片段4中的对应关系,从对应的属性对应的函数集合删除自己,最终讲代码片段4中的对应关系,修改成下面这种状态:

代码语言:javascript复制
// 代码片段7
{
    target:{
        prop1:[fn1],
        prop2:[fn3]
    }
}

这样,当proxyTarget.prop1proxyTarget.prop2发生变化的时候,就不会再触发fn2重新执行了。

我们要知道,所谓的依赖收集,是指保存这些函数与其所依赖的代理对象的属性之间的关系。也就是说函数是依赖方,代理对象的属性是被依赖方。代理对象属性的改变会触发依赖这些属性的函数重新执行。但是这些依赖函数自己,也保存了其所依赖的对象属性,在需要的时候,再根据保存的自己所依赖的对象属性,删除这组依赖关系。

上面关于deps的描述和源码实现是不一致的,但是有了这个基础,理解源码实现就很简单了,在代码片段6进行改造如下:

代码语言:javascript复制
{
    fn2所在的ReactiveEffect实例:[target.prop1所对应的依赖函数集合, target.prop2所对应的依赖函数的集合]
}

再进一步改造:

代码语言:javascript复制
ReactiveEffect实例.deps = [target.prop1所对应的依赖函数集合, target.prop2所对应的依赖函数的集合]

当然这里的"target.prop1所对应的依赖函数集合"、“target.prop2所对应的依赖函数的集合”都包含ReactiveEffect实例自身,这样和源码实现就是一致的了。

parent属性

个人觉得parent这个属性名在这里不太好,因为实际表达的是上一个处于活动中的ReactiveEffect实例。以理解为就像是一个,先处于活跃状态的实例在最底层,后处于活跃状态的实例处在上层,栈顶是当前活跃的ReactiveEffect实例。当然实际代码中只是通过变量维护了一个链式的关系。但理解为在程序运行流程上是没有太大差别的。事实上,在Vue3之前的版本,本身就是通过栈的形式来维护这种关系,为了性能上的提升,改为如今这种链式的方式维护,这也体现了Vue3框架作者们追求极致的精神。在下文run方法的部分,还会具体涉及这一块内容。

computed属性

和计算属性相关,此处暂不做解释,下文相关的地方会提及。

allowRecurse属性

允许递归调用,下文在分析函数triggerEffects时会提及。

onStop属性

一个回调函数,在调用ReactiveEffect实例的stop方法时,如果该实例onStop有对应的函数值,则调用该函数。相当于一个普通的回调函数,用户可以在该函数中处理一些个性化的需求。

onTrackonTrigger属性

这两个属性对应的是两个函数值,但只和开发阶段有关,在某些时机会调用这两个函数(如果传入的参数有对应的值的话),本文不对其进行分析。

scheduler属性

这个属性比较重要,如果用户传入参数给scheduler赋予了一个函数值,则不会执行该ReactiveEffect实例的run方法。

stop方法

请先阅读代码:

代码语言:javascript复制
// 代码片段8
export class ReactiveEffect<T = any> {
  // 此处省略很多代码...
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i  ) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

这里的逻辑比较简单,上文提到的active属性的值在该函数置为false,而onStop函数在上面已经解释过,至于cleanupEffect函数,实际上在对deps属性的解释中,已经回答了这里为什么要这么实现。

run方法

我们来先看看run方法中的代码实现:

代码语言:javascript复制
// 代码片段9
export class ReactiveEffect<T = any> {
  // 此处省略很多代码...
  run() {
    // 关键点1
    if (!this.active) {
      return this.fn()
    }
    // 关键点2
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    // 关键点3
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true

      trackOpBit = 1 <<   effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }
  // 此处省略很多代码...
}

关键点1

对应代码如下:

代码语言:javascript复制
// 代码片段10
if (!this.active) {
   return this.fn()
}

前面讲过,如果active属性值为false,那么就不会参与正常的依赖收集活动,但是我们发现如果为false仍然会执行该active属性对应的ReactiveEffect实例下的函数fn,然而我们知道在执行函数fn的时候,仍然会进行依赖收集。这里是矛盾的吗?答案是并不矛盾。至于具体原因,我们在代码片段9的关键点3部分的分析中进行解释。

关键点2

对应代码如下:

代码语言:javascript复制
// 代码片段11
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
    if (parent === this) {
        return
    }
    parent = parent.parent
}

这样如果一下看不出来,如果换回栈的描述就清楚了,上面的代码可以这样描述:

代码语言:javascript复制
// 代码片段12
// effectStack保存了所有的活跃过的依赖函数
if (effectStack.includes(this)) {
    return
}

上面代码片段11的while循环,目的就是看当前的ReactiveEffect实例,是不是已经处在这个活跃ReactiveEffect实例的链条中。这个时候可能有朋友问,为什么一定要保存这个链式的关系呢?在回答这个问题之前,我们先看看代码片段11中的变量parent被赋值为activeEffect,这个activeEffect又是什么呢?activeEffecteffect.ts文件中定义的一个全局变量。这个activeEffect又有什么用呢?还记得本文开始画的依赖收集的示意图吗,依赖收集就是保存对象属性和ReactiveEffect实例的关系。那这个ReactiveEffect实例从哪里获取呢,就是从这里的全局变量activeEffect。假如有下面示意的effect实例调用run方法的执行顺序:

代码语言:javascript复制
// 代码片段13
effect(()=>{}); // effect1
effect(()=>{}); // effect2
effect(()=>{}); // effect3

执行完代码片段13中的代码,那么effect3就是activeEffect。由于JavaScript程序是单线程,那么执行effect1的时候,此时effect1就是activeEffect,依赖收集的对应关系就关联到effect1,执行完effect1再执行effect2,依赖关系就关联到effect2,依此类推。按照这样看起来似乎没有问题,甚至不用维护一个链式关系也没问题。但是,如果是下面的执行逻辑呢?

代码语言:javascript复制
// 代码片段14
effect(()=>{ // effect1
    console.log(proxyTarget.name);
    setTimeout(()=>{
        console.log(proxyTarget.city);
    }, 3000);
    effect(()=>{ // effect2
        proxyTarget.age;
    });
})

如果activeEffect没有链式关系,在依赖收集的过程中,proxyTarget.city对应的ReactiveEffect实例就是effect2,而事实上,proxyTarget.city的依赖函数是effect1,出现了错乱的状况。这时候可能朋友们还会问,有activeEffect链式的关系又怎样呢,也不会自动解决这个问题吧,没错,需要做些相应的处理。我们接下来,看代码片段9中所示的关键点3处的代码:

关键点3

代码语言:javascript复制
// 代码片段15
try {
    this.parent = activeEffect
    activeEffect = this
    shouldTrack = true
    trackOpBit = 1 <<   effectTrackDepth
    if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
    } else {
        cleanupEffect(this)
    }
    return this.fn()
} finally {
    if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
    }
    trackOpBit = 1 << --effectTrackDepth
    activeEffect = this.parent
    shouldTrack = lastShouldTrack
    this.parent = undefined
}

为了解决代码片段14所示的问题,Vue3的作者们,设计了上文提到过的,维护activeEffect的链式关系。同时,如代码片段15所示,我们会发现,在执行fn函数执行有这样的代码:

代码语言:javascript复制
// 代码片段16
this.parent = activeEffect
activeEffect = this

而在执行完fn函数之后,有这样两行代码:

代码语言:javascript复制
// 代码片段17
activeEffect = this.parent
this.parent = undefined

也就是说,在fn函数执行完成后,activeEffect会恢复到上一级的状态。这就完美的解决了代码片段14所描述的问题。也回答了为什么要维护一个activeEffect的链式关系,总之,就是链式关系的存在就是为了解决嵌套的问题。

在代码片段15中还有几个点值得我们注意。这里涉及两个比较特殊的变量:effectTrackDepthtrackOpBiteffectTrackDepth表示当前effect函数的嵌套层数。但是,这个层数也不是无限的,而是常量maxMarkerBits所表示的数量30,至于为什么是这么一个数字,Vue作者们在源码中给出了注释:

代码语言:javascript复制
// 代码片段18
/**
 * The bitwise track markers support at most 30 levels of recursion.
 * This value is chosen to enable modern JS engines to use a SMI on all platforms.
 * When recursion depth is greater, fall back to using a full cleanup.
 */
const maxMarkerBits = 30

我们要重点了解下代码片段15中的变量trackOpBit。在函数fn执行前:

代码语言:javascript复制
// 代码片段19
trackOpBit = 1 <<   effectTrackDepth

在函数fn执行完成后:

代码语言:javascript复制
// 代码片段20
trackOpBit = 1 << --effectTrackDepth

从这里可以看出,trackOpBit某种意义上代表的是activeEffect嵌套的深度。那这个变量trackOpBit在哪里使用呢?抛开超过嵌套层数限制的逻辑,我们看在函数fn执行前后还执行了那些代码:

代码语言:javascript复制
// 代码片段21
// 函数fn执行前
initDepMarkers(this)
代码语言:javascript复制
// 代码片段22
// 函数fn执行后
finalizeDepMarkers(this)

那我们再来看看initDepMarkersfinalizeDepMarkers这两个函数的实现:

代码语言:javascript复制
// 代码片段23,所属文件:core/reactivity/src/dep.ts
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i  ) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}

export const finalizeDepMarkers = (effect: ReactiveEffect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i  ) {
      const dep = deps[i]
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      } else {
        deps[ptr  ] = dep
      }
      // clear bits
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

会发现,在代码片段23中,发现了上面提到的变量trackOpBit。从上文的内容我们知道,ReactiveEffect实例用变量deps保存了依赖收集中,该ReactiveEffect实例所在的所有依赖函数集合。initDepMarkers在这里给集合中所有的ReactiveEffect实例都通过一个属性w的值来做标记,表示该实例已经被收集过。而这个标记很特别,是一个二进制数据来表示,比如:

代码语言:javascript复制
// 代码片段24,因为最深嵌套为30层,所以下面二进制数据只显示30位
000000000000000000000000000001 // 表示第一层嵌套
000000000000000000000000001000 // 表示第四层嵌套,因为计算方式,是数字1左移对应嵌套层数对应的数字
deps[i].w |= trackOpBit // 如果deps[i].w默认值是0,trackOpBit为2,则deps[i].w的值为下面表示:
000000000000000000000000000010 // 如果deps[i].w默认值是2,trackOpBit为3,则deps[i].w的值为下面表示:
000000000000000000000000000110

我们再来看看finalizeDepMarkers中关于trackOpBit相关的代码:

代码语言:javascript复制
// 代码片段25
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit

因为trackOpBit的默认值是1,后续随着effect函数的嵌套深度增加或减少而递增或递减,无论怎样都不会比1小,也就是说这是一个正数。而~符号是非运算符,按位取反,对于有符号数来讲,正数会变为负数,而这里的&运算符,由于符号位不同,最终计算结果肯定依然是负值。到了这里,也就解释了函数finalizeDepMarkers完成的一项重要工作,就是清除该实例已经在收集依赖过程中被收集过的标记。至于属性dep.n是用来标记是不是刚刚被收集依赖。之所以要用二进制的方式进行标记,一方面可以提高性能,另一方面可以方便计算,具体为什么可以方便运算,上面的&|~就是很好的例子,其实还有更多体现,将来的文章在分析runtime-core相关内容的时候会详细解释。Vue3在不断发展变化,对性能的追求从未停止,而对位运算的精准应用在源码中不少地方都有体现。目前关于位运算标记暂时先了解这些,在分析trackEffects的时候还会有所涉及。

函数finalizeDepMarkers还完成了另一项重要的工作,由下面代码完成:

代码语言:javascript复制
// 代码片段26
if (wasTracked(dep) && !newTracked(dep)) {
    dep.delete(effect)
} else {
    deps[ptr  ] = dep
}}

如果该ReactiveEffect实例已经被依赖收集过,而且不是最近被收集的ReactiveEffect实例,那么就从依赖收集中删除ReactiveEffect实例。事实上正常情况下是不应该出现这种情况的,因为就如函数finalizeDepMarkers在代码片段25所处理的那样,执行完run方法,相应的状态都进行了重置。

分析点3: EffectScope

EfffectScopeVue3.2版本提供的一个高级特性,就日常开发来讲几乎用不到。但是对于一些库的作者就比较常用了。我们知道,我们在setup中调用reactiveref或者其他响应式API之后,依赖收集和解除这种依赖关系,是Vue组件内部自己完成的。但是如果在某些场景下,手动控制这种响应式的依赖关系呢?这时候EffectScope就派上用场了。我们先来看看类EffectScope的构造函数:

代码语言:javascript复制
// 代码片段27
export class EffectScope {
  active = true
  effects: ReactiveEffect[] = []
  cleanups: (() => void)[] = []

  parent: EffectScope | undefined
  scopes: EffectScope[] | undefined
  /**
   * track a child scope's index in its parent's scopes array for optimized
   * removal
   */
  private index: number | undefined

  constructor(detached = false) {
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

  run<T>(fn: () => T): T | undefined {
    if (this.active) {
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = this.parent
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }
  // 此处省略许多其他代码...
}

如果构造函数没有传递特别的参数来控制,那么默认情况下,就会建立一种链式的关系。这种链式关系也借助了一个全局变量activeEffectScope来实现,这和上文的ReactiveEffect实例的链式关系的建立有相似之处。这时这种链式关系的维护背后有共同的工程实践意义,那就是解决嵌套的问题,同时有了这种链式关系,也为EffectScope中的stop方法中的实现打下了基础。注意构造函数中还用到了一个index属性,这个index属性代表了该EffectScope实例在其父EffectScope实例维护的子EffectScope实例数组中所处的位置,在stop方法中会用到。

EffectScoperun方法逻辑很简单,维护链式关系,同时执行传入的fn函数。逻辑比较丰富比较巧妙的是其stop方法。

代码语言:javascript复制
// 代码片段28
stop(fromParent?: boolean) {
    if (this.active) {
      let i, l
      for (i = 0, l = this.effects.length; i < l; i  ) {
        this.effects[i].stop()
      }
      for (i = 0, l = this.cleanups.length; i < l; i  ) {
        this.cleanups[i]()
      }
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i  ) {
          this.scopes[i].stop(true)
        }
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.active = false
    }
  }

代码片段28所示的stop方法可以概括为做了4件事情:首先,将EffectScope实例所关联的ReactiveEffect实例数组进行遍历,依次执行ReactiveEffect实例的stop方法。其次,遍历this.cleanups数组,并依次调用相关函数,this.cleanups来源于下文将会讲解的onScopeDispose函数。再次,将关联的子EffectScope实例数组进行遍历,依次执行这些EffectScope实例的stop方法。最后,解除和父级EffectScope实例的关联,需要注意的是这里的解除关联的方式很巧妙:

代码语言:javascript复制
// 代码片段29
// nested scope, dereference from parent to avoid memory leaks
if (this.parent && !fromParent) {
    // optimized O(1) removal
    const last = this.parent.scopes!.pop()
    if (last && last !== this) {
        this.parent.scopes![this.index!] = last
        last.index = this.index!
    }
}

仔细观察这段代码,其最元素的需求是,子EffectScope实例从父级EffectScope实例维护的子EffectScope实例数组中删除自身。但是这里没有按照常规的先查找再删除。而是直接将数组末尾的元素放在了该子EffectScope实例原来所在的位置。将复杂度降到了最低,再次体现了Vue3作者们追求极致的精神。

下面对几个对外暴露的和EffectScope相关的函数。

effectScope
代码语言:javascript复制
// 代码片段30
export function effectScope(detached?: boolean) {
  return new EffectScope(detached)
}

逻辑很简单,只是创建了一个EffectScope实例,从功能上讲这个函数存在的意义不太大,但是这方便了创建对象,不需要手动进行new操作。

recordEffectScope
代码语言:javascript复制
// 代码片段31
export function recordEffectScope(
  effect: ReactiveEffect,
  scope: EffectScope | undefined = activeEffectScope
) {
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

将某个ReactiveEffect实例和该EffectScope实例进行关联,当然关联之后该EffectScope实例就可以调用该ReactiveEffect实例的stop等方法。

getCurrentScope
代码语言:javascript复制
// 代码片段32
export function getCurrentScope() {
  return activeEffectScope
}

返回当前处于活跃状态的EffectScope实例。

onScopeDispose
代码语言:javascript复制
// 代码片段33
export function onScopeDispose(fn: () => void) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__) {
    warn(
      `onScopeDispose() is called when there is no active effect scope`  
        ` to be associated with.`
    )
  }
}

将某些函数fnactiveEffectScope进行关联,当调用activeEffectScopestop方法的时候,会触发这些函数。

小结

上面对函数effect进行了介绍,先是引出了类ReactiveEffect并进入了ReactiveEffectrun方法中,分析了其主要逻辑,而后由引出了类EffectScope,并介绍了其实现细节。到目前为止,我们对effect函数背后的含义应该有了比较清晰的认知。接下来我们分析effect.ts文件中对外暴露的其他函数。

stop

代码语言:javascript复制
// 代码片段34
export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

这里的runner就是effect函数执行完成后的返回值。对应effect函数的这些代码:

代码语言:javascript复制
// 代码片段35
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner

不难发现,runner是一个函数,只不过该函数有一个effect属性,该属性的值就是一个ReactiveEffect实例。

pauseTracking、enableTracking、resetTracking

代码语言:javascript复制
// 代码片段36
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

通过维护一个全局变量shouldTrack和一个boolean数组,来控制当前是否需要进行依赖收集。最直接的体现,就是track函数中有下面的代码:

代码语言:javascript复制
// 代码片段37
if (shouldTrack && activeEffect) {
    // 省略其他代码...    这里在做依赖收集的具体工作
}

track、trackEffects

从前面的文章我们已经知道,依赖收集的具体触发点,在Proxy对象实例的get属性被访问的时候,具体触发的动作就是调用track函数,在track函数内部又调用了函数trackEffects。我们先看看函数track的内部实现:

代码语言:javascript复制
// 代码片段38
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

其实track函数的逻辑,在本文开头的依赖收集示意图中已经有直接的体现,其实就是在内存中保存了一组对象和对象属性及这些属性和对应依赖函数集合的对应关系。接下来我们看看trackEffects的内部实现:

代码语言:javascript复制
// 代码片段39
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

trackEffects函数中有几个我们值得注意的地方:

首先,前面提到过的变量trackOpBit,这里执行了dep.n |= trackOpBit的语句,相当于给dep所对应的ReactiveEffect实例集合初次收集activeEffect对应的ReactiveEffect实例。加上条件判断if (!newTracked(dep))最大的好处是提升性能,如果ReactiveEffect实例的fn函数中,多次使用了同一个代理对象的同一个属性,有了这个条件判断可以直接避免多次收集。

其次,关于执行语句shouldTrack = !wasTracked(dep),在触发更新的时候会执行ReactiveEffect实例的run方法,会将该实例对应的deps所有依赖集合做上该实例已经被依赖收集的标记。

最后,上面的逻辑之所以能够正常运转,最重要的原因是trackOpBit变量采用的是二进制方式来记录,可以轻松的确认是哪一级嵌套的依赖关系,因为依赖收集中的一个ReactiveEffect实例集合可能在不同嵌套层中重复出现,有了这种二进制机制的区分,解决了很多潜在的问题,不得不说这很巧妙,但同时代码的可读性下降了很多。所以我们在日常业务开发中需要进行取舍,代码可读性的优先级可能会更高一点,框架的性质决定了对性能的要求会更高,所以有必要追求极致的性能,哪怕是损失一些代码可读性也是值得的。

trigger、triggerEffects

从前面的文章我们已经知道,触发更新的具体触发点,在Proxy对象实例的set属性被访问的时候,具体触发的动作就是调用trigger函数,在trigger函数内部又调用了函数triggerEffects。我们先看看函数trigger的内部实现,整个trigger函数的逻辑被可以分成几部分,我们从上至下开始看代码:

代码语言:javascript复制
// 代码片段40
const depsMap = targetMap.get(target)
if (!depsMap) {
    // never been tracked
    return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
}

if(type === TriggerOpTypes.CLEAR)处理的事集合对象调用clear方法后的逻辑,这里需要所有用到了该集合对象的地方都触发更新。

代码语言:javascript复制
// 代码片段41
if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
}

代码片段41处理的是修改了数组的length属性值的逻辑,当keylength或者索引大于新赋予的值,则触发相应的依赖更新。

代码语言:javascript复制
// 代码片段42
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
    deps.push(depsMap.get(key))
}

// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
    case TriggerOpTypes.ADD:
        if (!isArray(target)) {
            deps.push(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
        } else if (isIntegerKey(key)) {
            // new index added to array -> length changes
            deps.push(depsMap.get('length'))
        }
        break
    case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
            deps.push(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
        }
        break
    case TriggerOpTypes.SET:
        if (isMap(target)) {
            deps.push(depsMap.get(ITERATE_KEY))
        }
        break
}

代码片段42,先通过if (key !== void 0)来保证depsMap.get(key)取值正常,进而确保给对象设置值、添加值、删除值的时候,会触发所有相关依赖更新。紧接着,通过类型来区分,进行相应条件判断,保证后续能触发相应的依赖更新。trigger函数的最后一部分逻辑如下:

代码语言:javascript复制
// 代码片段43
if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
} else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
}

这里逻辑不多,对deps.length === 1单独处理,做了一定程度的优化。最终是调用triigerEffects函数:

代码语言:javascript复制
// 代码片段44
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}

逻辑比较简单,触发对应的ReactiveEffect实例的run方法。需要注意的是,如果ReactiveEffect实例有scheduler属性,则执行该属性对应的函数值,而不再执行run方法。scheduler属性值是用户调用effect函数时候传递的参数,本文后面还会提及scheduler的作用。

reactive、ref相关API

我暂时不打算对reactive.tsref.ts中的每一个api进行介绍。有了本文上半部分的基础,加之前面的文章也对reactive.ts进行过主要逻辑的介绍,此时朋友们如果再回过头阅读reative.tsref.ts相关代码,会发现比较简单,如果读者朋友们发现还是很有必要对reative.tsref.ts的实现细节进行讲解,可以留言说明具体困难,如果有必要我再单独出一篇文章补充这部分内容,目前篇幅已经实在是太长了。

函数computed

我们先看看computed函数的具体实现:

代码语言:javascript复制
// 代码片段45
export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

从代码片段45中可以看出computed函数,最核心的逻辑就是新建了一个ComputedRefImpl实例,并将该实例作为结果返回。我们来看看类ComputedRefImpl的代码实现:

代码语言:javascript复制
// 代码片段46
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  public _dirty = true
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

对于这个类,我们可以从下面几个方面来分析:

首先,该类有value属性,底层相当于用了Object.defineProperty(所以如果说Vue3的响应式系统是建立在Proxy的基础上的,这句话是不严谨的),如果用户没有传入_setter参数,则整个计算属性相当于一个只读的属性。当用户使用value属性的时候,会进行依赖收集,从这里可以看出进行依赖收集之后马上执行来effect属性的run方法。

其次,我们知道effect属性的run方法内部会执行用户传入的fn函数,这里的getter函数就是那个fn,因为在构造函数中,初始化了一个ReactiveEffect实例,传入的正是这个用户传入的getter函数。内部执行了getter函数就会进行依赖收集,getter函数中的代理对象发生变化后就会进行触发更新操作。需要注意的是,由于这里给ReactiveEffect构造函数传入了第二个参数,也就是前面说的scheduler函数,那么触发更新的时候就不会再次执行getter,而是执行triggerRefValue(this)

最后,triggerRefValue(this)get value()中的trackRefVale(self)遥相呼应,当computed中涉及到的代理对象属性发生了变化,就会触发更新。

0 人点赞