所谓watch,就是观测一个响应式数据或者监测一个副作用函数里面的响应式数据,当数据发生变化的时候通知并执行相应的回调函数。Vue3最新的watch实现是通过最底层的响应式类 ReactiveEffect 的实例化一个 reactive effect 对象来实现的。它的创建过程跟effect API的实现类似,所以在了解 watch API 之前,我们先要了解一下 effect 这个 API。

# effect函数

Vue3里面 effect 函数 API 是用于注册副作用函数的函数,也是Vue3响应式系统最重要的API之一。通过 effect 注册了一个副作用函数之后,当这个副作用函数当中的响应式数据发生了读取操作之后,通过Proxy的get、set拦截,从而在副作用函数与响应式数据之间建立了联系。具体就是当响应式数据的"读取"操作发生时,将当前执行的副作用函数存储起来;当响应式数据的"设置"操作发生时,在将储起来的副作用函数取出来就行

# 什么是副作用函数

副作用函数指的是会产生副作用的函数,如下面的代码所示

function effect() {
    document.body.innerText = 'hello coboy ~'
}

当 effect 函数执行时,它会设置 body 的文本内容,但除了effect函数之外的任何函数都可以读取或者设置body的文本内容。也就是说,effect函数的执行会直接或间接影响其他函数的执行,这时我们说effect函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用,如下面的代码所示

// 全局变量
let val = 1;
function effect() {
    val = 2; // 修改全局变量,产生副作用
}

我们在文章最开头说到Vue3最新的watch实现是通过最底层的响应式类 ReactiveEffect 的实例化一个 reactive effect 对象来实现的,那么我们先来了解一下 ReactiveEffect 类

# ReactiveEffect 类

相信很多关注Vue3源码的同学都知道,Vue3先前的响应式系统版本中是没有ReactiveEffect这个累的,最新版本用面向对象的编程方式把变量当成对象进行操作,让编程思路更加清晰简洁,而且减少了很多冗余变量的出现,使用ReactiveEffect这个类封装了 effect 相关的数据和方法,方便了函数、变量、数据的管理

下面我们来看看 ReactiveEffect 这个类的源码:

// 记录当前活跃的对象
let activeEffect;
// 标记是否追踪
let shouldTrack;
// 用于依赖收集
export class ReactiveEffect {
    private _fn: any;
    deps = []; // 所有依赖这个effect的响应式对象
    active = true; // 是否为激活状态
    onStop?: () => void;
    constructor(fn, public scheduler?) {
        // 用户传进来的副作用函数
        this._fn = fn;
    }
    run() {
        // 执行fn,但是不收集依赖
        if(!this.active) {
            return this._fn();
        }
        // 执行fn 收集依赖
        // 可以开始收集依赖了
        shouldTrack = true;
        // 执行的时候给全局的 activeEffect 赋值
        // 利用全局属性来获取当前的 effect
        activeEffect = this;
        // 执行用户传入的fn
        const result = this._fn();
        // 重置
        shouldTrack = false;
        return result;
    }
    stop() {
        if(this.active) {
            // 如果第一次执行stop后 active就false了
            // 这是为了防止重复的调用,执行stop逻辑
            clearupEffect(this);
            // 如果用户往effect实例对象设置了onStop函数,那么在清除effect对象的时候,也会执行用户设置的onStop方法
            if(this.onStop) {
                this.onStop();
            }
            this.active = false;
        }
    }
}

通过ReactiveEffect 这个类的实例化相当于实现了响应式系统里面的一个大管家,这个大管家管理者用户设置的副作用函数,调度函数 scheduler,所有依赖这个reactive effect 的响应式对象deps函数,还实现了两个方法,run和stop,在run方法里面执行副作用函数,触发依赖收集,并返回副作用函数执行的结果,stop方法则是清除当前的 reactive effect 实例对象。

通过ReactiveEffect 这个类我们可以清晰的看到大家管reactive effect 实例对象在干些什么工作

# effect 函数解析

接下来我们看看effect函数的具体代码

// package/reactivity/src/effect.ts
export function effect<T = any>(
  // 副作用函数
  fn: () => T,
  // 配置选项
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 如果当前 fn 已经是收集函数包装后的函数,则获取监听函数当做入参
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 创建effect对象
  const _effect = new ReactiveEffect(fn)
  // 把用户传过来的值合并到 _effect 对象上去
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  // 有些场景下,我们并不希望它立即执行,而是希望它在需要的时候才执行,例如计算属性。
  // 这个时候我们可以通过 optins 中添加 lazy 属性来达到目的,当 options.lazy 为 true 时,则不立即执行副作用函数
  if (!options || !options.lazy) {
    _effect.run() // 执行run,响应式数据将与副作用函数之间建立联系
  }
  // 把 _effect.run 这个方法返回,也就是等于将辅助函数作为返回值返回
  // 让用户可以自行选择调用的时机(调用 fn)
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

可以总结一下effect

  • 接收一个副作用函数和options参数
  • 判断传入的副作用函数是不是effect,如果是取出原始值
  • 调用createReactiveEffect创建reactive effect 实例对象
  • 把用户传过来的options参数中的lazy为false则立即执行effect包装之后的副作用函数
  • 最后返回 reactive effect 实例对象上的 run 方法让用户可以自行选择调用的时机

简单来说 effect API 的实现就是实例化 ReactiveEffect 类获得一个 reactive effect 的实例对象,在实例化的时候通过传参把副作用函数 和当前的 reactive effect 实例对象进行了绑定,当运行 reactive effect 实例对象上的run方法的时候就把响应式对象和 reactive effect 实例对象进行了绑定。在后续如果响应式对象发生了改变,就会把和响应式对象绑定的那些 reactive effect 实例对象取出来执行 reactive effect 实例对象上的 run 方法,run 方法里面就会执行最初传进来的副作用函数。

# 可调度执行

所谓可调度,指的是当trigger动作触发副作用函数重新执行的时候,有能力决定副作用函数执行的时机、次数以及方式

effect函数的第二个参数options,允许用户指定调度器。当用户在调用effect函数注册副作用函数时,可以传递第二个参数options。可以在options中指定scheduler调用函数

这里,我们顺便介绍一些effect的options参数

export interface ReactiveEffectOptions {
  lazy?: boolean //是否懒执行副作用函数
  scheduler?: (job: ReactiveEffect) => void //调度函数
  onTrack?: (event: DebuggerEvent) => void //追踪时触发
  onTrigger?: (event: DebuggerEvent) => void //触发回调时触发
  onStop?: () => void //停止监听时触发
  allowRecurse?: boolean //是否允许递归调用
}

# 组件更新函数的最新实现

我们来看看组件更新函数最新的实现是怎么实现的

const setupRenderEffect = () => {
    const componentUpdateFn = () => {
        // ...
    }
     // 通过 ReactiveEffect 类来创建渲染 reactive effect 实例对象
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope
    ))
    // 组件更新函数就是 reactive effect 实例对象上 run 方法
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
    update.id = instance.uid
    
    update()
}

我们可以看到最新的组件更新函数也是通过 ReactiveEffect 类来实现的,把组件更新的副作用函数和调度函数传到 ReactiveEffect 类,再实例化出一个 reactive effect 实例对象,再把 reactive effect 实例对象中的 run 方法赋值给组件更新函数属性。

通过上面前奏简单了解 effect 函数 API 之后,再进行了解 watch 的实现原理就好理解了。

# watch的实现原理

所谓watch,其实本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数

接下来我们简单实现如果下响应式数据的监测

watch(() => obj.name, () => {
    console.log('数据变化了')
})

当响应式数据 obj.name 发生更改的时候,就会执行回调函数。

在最新的 Vue3.2 版本中,watch API 是通过 ReactiveEffect 类来实现相关功能的。

# 最简单的watch实现

export function watch(
    source, 
    cb, 
    options
) {
    // 副作用函数
    const getter = source;
    // 调度函数
    const scheduler = () => cb();
    // 通过 ReactiveEffect 类实例化处一个effect实例对象
    const effect = new ReactiveEffect(getter, scheduler);
    // 立即执行实例对象上的run方法,执行副作用函数,触发依赖收集
    effect.run();
}

跟 effect API 的实现类似,通过 ReactiveEffect 类实例出一个Reactive effect 实例对象,然后执行实例对象上的run方法就会执行getter副作用函数,getter副作用函数里的响应式数据发生了读取的get操作之后触发依赖收集,通过依赖收集将 reactive effect 实例对象和响应式数据之间建立了联系,当响应式数据变化的时候,会触发副作用函数的重新执行,但有因为传入了 scheduler 调度函数,所以会执行调用函数,而调用函数里执行了回调函数cb,从而实现了监测。

# 副作用函数的封装

因为第一个参数source可以是一个

  • ref类型的变量
  • reactive 类型的变量
  • Array类型的变量,数组里面的元素可以是ref类型的变量、reactive类型的变量、Function函数
  • Function函数

所以需要对第一个参数处理封装成一个通用的副作用函数

let getter: () => any
  if (isRef(source)) {
    // 如果是 ref 类型
    getter = () => source.value
  } else if (isReactive(source)) {
    // 如果是 reactive 类型
    getter = () => source
    // 深度监听为 true
    deep = true
  } else if (isArray(source)) {
    // 如果是数组,进行循环处理
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return s()
        }
      })
  } else if (isFunction(source)) {
      // 如果是函数
      getter = () => source()
  }

  if (cb && deep) {
    // 如果有回调函数并且深度监听为 true,那么就通过 traverse 函数进行深度递归监听
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

# deep的实现原理

原文 (opens new window)