vue3源码解析—响应式系统

240 阅读20分钟

前言

本小节我们开启响应式原理的篇章。 在 Composition API 中我们通过 reactive API 或者 ref API 来创建响应式数据,响应式的实现不再通过 Object.defineProperty,而是基于 Proxy;创建响应式数据时也会和 vue2 一样进行依赖收集,但不再通过 Dep 中间值,而是直接收集 activeEffect; 接下来我将逐一介绍 响应式依赖收集的实现,并附带实现响应式相关的API,例如 reactiverefeffecteffect 内部的 runnerschedulerstop,以及readonlyisReadonlyisReactiveshallowReadonlyisProxyisRefunReftoReftoRefsproxyRefs,最后还会实现 computedwatch 相关的 API。

reactive 相关 API

reactive

对于 reactive API 而言,核心是用来定义集合类型的响应式数据,比如:普通对象数组MapSet。 它的实现有以下几个特点:

  1. 本质是使用 Proxy 对数据进行代理;
  2. 数据类型必须是 object 类型
  3. 经过代理的数据不再重复代理
  4. 不重复代理同一对象
// reactivity/src/reactive.ts
import { isObject } from "@vue/shared";
import { mutableHandlers } from "./baseHandlers";
export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive",
}
const reactiveMap = new WeakMap(); // 使用 WeakMap 防止内存泄漏
export function reactive(target) {
    // 1. reactive 只代理对象
    if (!isObject(target)) {
        return target;
    }
    // 2. 如果已经被代理过,则直接返回
    // 取 target[ReactiveFlags.IS_REACTIVE] 时,如果target已经被代理过,则会走到get函数,返回true
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    // 3. 不重复代理同一对象
    const existProxy = reactiveMap.get(target);
    if (existProxy) {
        return existProxy;
    }
    // 4. 创建代理
    const proxy = new Proxy(target, mutableHandlers);
    reactiveMap.set(target, proxy); // target -> proxy 的映射表
    return proxy;
}

下面我们来实现 mutableHandlers,我们要实现的功能主要有:

  1. 自定义 gettersetter
  2. getter 中进行依赖收集
  3. setter 中触发更新
// reactivity/src/baseHandlers.ts
import { ReactiveFlags } from "./reactive";
export const mutableHandlers = {
    // receiver 是当前的代理对象
    get(target, key, receiver) {
        // 通过下列判断,解决不重复代理已经经过代理的对象
        if (ReactiveFlags.IS_REACTIVE === key) {
            return true;
        }
        track(target, key);
        // 使用 Reflect.get 处理了 target 内部的 this 指向问题
        let r = Reflect.get(target, key, receiver); 
        // 取值的时候,如果属性依然是对象,才对该属性递归使用reactive,相较于vue2性能更好
        if (isObject(r)) {
            return reactive(r);
        }
        return r;
    },
    set(target, key, value, receiver) {
        let oldValue = target[key];
        const r = Reflect.set(target, key, value, receiver); // Reflect.set 返回一个boolean值
        if (oldValue !== value) {
            trigger(target, key, value, oldValue);
        }
        return r;
    },
};

为什么在 getter 中不能直接通过 target[key] 取值,而要使用 Reflect.get()? 现有一个对象 person

let person = {
   name: 'jw',
   get aliasName() {
       return 'alias' + this.name
   }
}

当通过 person.aliasName 取值时,aliasName 内部的 this.namethis 指向 person)是通过 person.name 读取的,name 改变不会触发aliasName的响应式; 而改成 Reflect.get(target, key, receiver) 后,this 指向 receiver(即 personProxy),this.namepersonProxy.namename 改变会触发aliasName的响应式;

proxy 相较于 Object.defineproperty 的优势在哪里?

  1. 可以原生处理数组,而 vue2 中数组的响应式是通过重写数组原型方法实现的;
  2. 在创建 Proxy 数据时,没有对所有属性递归使用 Proxy,只是对第一层的属性进行了代理,只有当读取到深层属性时才递归使用 reactive,相较于 Object.defineproperty 性能更好

收集依赖 track(target, key) 和触发更新 trigger(target, key, value, oldValue) 的具体实现将在后文再详细解析。

isReactive

isReactive 用于检查对象是否是由 reactive 创建的响应式代理。判断一个数据是否是 reactive,只需要知道其是否触发了Proxy中的get方法。那么我们只需要在 isReactive 函数中访问target中的特定属性,然后修改proxyget函数即可:

// reactive.ts
export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
}
export function isReactive(value) {
  return !!value[ReactiveFlags.IS_REACTIVE];
}

// baseHandlers.ts
function createGetter(isReadonly=false) {
  return function get(target,key) {
    // 当key为__v_isReactive时,返回!isReadonly
    if(key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }
    let res = Reflect.get(target,key)
    // 依赖收集、深层递归...
  }
}

isReadOnly

isReadOnly 用于检查对象是否是由 readonly 创建的只读代理。isReadonly 的源码逻辑和isReactive 源码逻辑一致,通过触发get方法来实现isReadonly的判断。

// reactive.ts
export const enum ReactiveFlags {
  IS_READONLY = '__v_isReadonly'
}
export function isReadonly(value) {
  return !!value[ReactiveFlags.IS_READONLY];
}

// baseHandlers.ts
function createGetter(isReadonly=false) {
  return function get(target,key) {
    if(key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }else if(key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target,key)
    // 依赖收集、深层递归...
  }
}

isProxy

满足 isReactiveisReadonly 中的任何一个,即满足 isProxy

export function isProxy(value) {
  return isReactive(value) || isReadonly(value);
}

readOnly

readonly 方法的特点:

  • 会创建 Proxy 对象
  • 无法修改数据,强行修改会报错
  • 不会进行依赖收集和触发更新
  • 在读取深层属性时,如果该属性是 object 类型,会递归使用 readOnly 进行处理
export function readonly(raw) {
    return new Proxy(raw, {
        get(key, value) {
            let res = Reflect.get(target,key)
            if (isObject(res)) {
                return readonly(res);
            }
            return res
        }
        set(target,key,value) {
            console.warn(`key:${key} set 失败,因为 target是readonly`,target)
            return true
        }
    })
}

readOnlytrue 时不会进行依赖收集

shallowReadonly

shallowReadonly 的含义:创建一个 proxy,使其自身的 property (属性)为只读,但不执行嵌套对象的深度只读转换(直接暴露原始值,不递归处理)。

const shallowReadonlyGet = createGetter(true,true)
function createGetter(isReadonly=false,shallow = false) {
  return function get(target,key) {
    if(key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }else if(key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target,key)
    // 如果是shallowReadonly,直接返回值,不进行递归处理
    if(shallow) {
      return res
    }
    if(isObject(res)) {  
      return isReadonly ? readonly(res) : reactive(res)
    }
    //如果不是readonly,才进行依赖收集
    if(!isReadonly) {
      track(target,key)
    }
    return res
  }
}

shallowtrue 时,不会递归处理深层属性

effect 相关 API 及依赖收集、触发更新

我们知道,在 vue2 中依赖收集和触发更新是基于 watcher 实现的,一共有三类 watcher:渲染watchercomputed watcher、以及用户自定义watcher,并且借助中间量 Dep 实现双向收集的目的,可读性较差。在 vue3 中重写了这套逻辑,使用的是 effect

effect & cleanupEffect

  • ReactiveEffecteffect 实例的构造函数。
  • effect 是一个函数,在函数中会创建一个 ReactiveEffect 实例(接收一个回调函数),并执行实例的 run 方法;
  • activeEffect 是一个全局变量,用来记录当前活跃的 effect,用于后文的依赖收集;
  • cleanupEffect 用来清空 _effect 实例中所有的 deps,以及将该 _effect 实例从 deps 中的 dep 集合中删除;
// reactivity/src/effect.ts
function cleanupEffect(effect) {
  // 每次执行effect之前,都应该将该effect从deps所有属性的dep中清理出去,以及清空effect的deps数组
  let { deps } = effect;
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect);
  }
  effect.deps.length = 0;
}
export let activeEffect;
export class ReactiveEffect {
  public fn;
  public active = true;
  public deps = [];
  public parent = undefined;
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    // 通过设置parent属性,确保在嵌套effect中activeEffect的准确性
    try {
      this.parent = activeEffect;
      activeEffect = this;
      // 执行 fn 之前,清除 _effect 实例中所有 deps 相关的 effect 实例
      cleanupEffect(this);
      // 执行fn的时候就会取值,取值的时候收集当前的activeEffect
      return this.fn();
    } finally {
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
}
export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
}

【问】当 effect 函数内部再嵌套一个 effect 时,如何保证activeEffect的准确性,使得在依赖收集时不发生差错? 【问题描述】当创建内层effect实例时,activeEffect为内层的effect实例,当执行完内层effect函数,activeEffect依然为内层的effect实例,导致外层effect在进行依赖收集时,收集的是内层的effect实例。 【解决方案】先用一个变量parent存储当前的activeEffect(例如执行内层的_effect.run()时,存储的activeEffect就是外层_effect实例),然后将activeEffect设为当前的_effect实例,执行完effect函数后,将activeEffect设置回parent变量存储的activeEffect(即在执行完内层_effect.run()后,会将activeEffect设为外层的_effect实例),最后将parent变量置空。

【问】依赖变化时,意料之外的重新渲染 【问题描述】每次触发 trigger 重新执行 _effect.run(),此时 effect 的依赖项可能发生变化(可能会删除之前的依赖或新增新的依赖),如果之前的依赖被删掉了,那么就需要清除之前收集的依赖,不然当该依赖变化时会重新执行_effect.run(),与预期不符。 【解决方案】在 _effect.run() 中执行 fn 之前,先清除 effect.deps 中所有 dep 对应的_effect,然后执行fn(会读取响应式数据,触发Proxy中的getter,重新进行依赖收集)

在执行 _effect.run() 时,会将activeEffect 设为当前的 _effect 实例,然后执行回调函数 fn,会读取响应式数据,触发数据的 getter 函数,执行 track 进行依赖收集。

track

依赖收集是一个双向收集的过程,在数据中需要收集所有相关的 _effect 实例,在 _effect.deps 中也需要收集所有 _effect 实例相关的数据。 在收集 _effect 实例时,会先创建一个 WeakMap,收集所有 target;然后针对每个target创建一个Map,在Map中针对每个属性创建Set,用来存储与该属性相关的所有 _effect 实例。在属性收集 _effect 实例时,同时在_effect.deps中收集该属性对应的 Set 数据(里面存储的是该属性相关的所有 _effect 实例)。

// reactivity/src/effect.ts
// 双向依赖收集
const targetMap = new WeakMap();
export function track(target, key) {
  // 1. 如果取值操作没有发生在effect中,直接返回,不会进行依赖收集
  if (!activeEffect) {
    return;
  }
  // 2. 从映射表中读取属性对应的dep
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  // 3. 依赖收集
  trackEffects(dep);
}
export function trackEffects(dep) {
  let shouldTrack = !dep.has(activeEffect);
  // 双向收集:一个属性可能对应多个effect,一个effect可能对应多个属性
  if (shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

trigger

在响应式数据发生变化时,会触发proxy中的getter函数,进而执行trigger触发更新。 trigger 本质上就是执行依赖收集的所有 _effect 实例的 run() 方法。

// reactivity/src/effect.ts
// 触发更新
export function trigger(target, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  triggerEffects(dep);
}
export function triggerEffects(dep) {
  if (dep) {
    const effects = [...dep];
    // 执行dep中所有effect的run方法
    effects.forEach((effect) => {
      /**
       * 【问题描述】如果在effect内部修改依赖,会触发effect重新执行,造成死循环;
       * effect(() => {
       *  state.age = Math.random();  // 在effect内部修改state,如果此时重新执行当前的activeEffect,会造成死循环
       *  app.innerHTML = state.age
       * })
       * 所以重新执行effect时需要判断重新执行的effect是否是当前的activeEffect,如果是当前的activeEffect,则不重新执行
       */
      if (activeEffect !== effect) {
        effect.run();
      }
    });
  }
}

runner

effect 函数除了执行 _effect.run() 之外,还会将该方法返回;对 effect 函数做出相应修改如下:

export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
  const runner = _effect.run.bind(_effect); // 保证_effect执行的时候this是当前的effect
  runner.effect = _effect;
  return runner;
}

stop

_effect 实例还存在一个 stop 方法,该方法会清除依赖,然后将该 _effect 实例变成失活状态,使得在执行 _effect.run()仅执行回调函数,不设置activeEffect(则不会进行依赖收集

export class ReactiveEffect {
    public active = true;
    stop() {
        // 先将effect的依赖全部删除掉,然后将它变成失活态
        if (this.active) {
            cleanupEffect(this);
            this.active = false;
        }
    }
    run() {
        if (!this.active) {
            return this.fn();
        }
        // ...
    }
}

scheduler

scheduler 是我们调用 effect 函数时传入的第二个参数 — options对象中的一个scheduler属性,它是一个函数,在创建 ReactiveEffect 实例时,将scheduler作为第二个参数传入。它的特点是:当执行 trigger 触发响应式更新时,如果存在 _effect.scheduler,则不是执行 _effect.run(),而是执行 effect.scheduler(); 所以我们需要对 effect 函数的入参、reactiveEffect构造函数、以及trigger的逻辑进行一些修改:

export function triggerEffects(dep) {
  if (dep) {
    const effects = [...dep];
    effects.forEach((effect) => {
      if (activeEffect !== effect) {
        // 触发trigger时,有effect.scheduler时执行【scheduler】,没有scheduler时才执行run
        if (!effect.scheduler) {
          effect.run();
        } else {
          effect.scheduler();
        }
      }
    });
  }
}
export class ReactiveEffect {
  public fn;
  private scheduler;
  constructor(fn, scheduler) {
    this.fn = fn;
    this.scheduler = scheduler;
  }
  // ...
}
export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  // ...
}

ref 相关 API

ref

为什么需要 refreactive 是使用 Proxy 创建响应式数据的,而 Proxy 不可用于基本数据类型;所以使用 ref 创建基本数据类型的响应式收据(ref 也能接收 object 类型的数据,会先使用 reactive 对其进行处理)

ref 的实现步骤:

  1. ref 主要用于基础数据类型,也能接收 object 类型的数据;如果接收的是 object 类型的数据,会使用 reactive 处理
  2. ref 函数会创建一个 class 的实例,并利用 class 的访问器,对数据的 value 属性进行代理,在 get value() 中进行依赖收集,在 set value() 中触发更新
  3. 依赖收集是一个双向收集的过程:在 activeEffect.deps 中收集 _RefImpl.dep;并在 _RefImpl.dep 中收集 activeEffect; 也就是说:在执行_effect.run()时会设置activeEffect,此时如果触发到 _RefImpl.get value() 就会将该 activeEffect 收集到 _RefImpl.dep 中,也会在activeEffect.deps 中收集 _RefImpl.dep。(注意区分一下与 reactive 中双向收集的区别)
  4. 触发更新会遍历 _RefImpl.dep 中所有的 _effect,并执行它们的 run() 方法或者 scheduler() 方法。
  5. set value() 中,如果新数据是 object 类型,也会使用 reactive 进行处理。

具体实现如下:

import { isObject } from "@vue/shared";
import { reactive } from "./reactive";
import { activeEffect, trackEffects, triggerEffects } from "./effect";

export function ref(value) {
  return new RefImpl(value);
}
function toReactive(value) {
  return isObject(value) ? reactive(value) : value;
}
class RefImpl {
  dep = undefined;
  _value;
  __v_isRef = true;
  constructor(public rawValue) {
    // 如果 ref 传入的是对象,则用reactive将它变成响应式的
    this._value = toReactive(rawValue);
  }
  get value() {
    // 依赖收集
    if (activeEffect) {
        // 双向收集:在 activeEffect.deps 中收集 _RefImpl.dep;并在 _RefImpl.dep 中收集 activeEffect
      trackEffects(this.dep || (this.dep = new Set()));
    }
    return this._value;
  }
  set value(newValue) {
    if (newValue !== this.rawValue) {
      // 更新数据:newValue 也需要用 toReactive 进行处理
      this._value = toReactive(newValue);
      this.rawValue = newValue;
      // 触发effect更新
      triggerEffects(this.dep);
    }
  }
}

isRef

isRef 用于判断当前数据是否为 ref 类型。 在ref实例的构造函数中,我们将ref类型的数据做了标记,即 public __v_isRef = true;,所以只需要判断数据的 __v_isRef 属性是否为 true,即可判断它是否是 ref 类型。

export function isRef(ref) {
  return !!ref.__v_isRef;
}

unRef

unRef 的含义:如果参数为 ref 类型,则返回内部的 value 值,否则返回参数本身。

export function unRef(ref) {
  //如果数据是ref,我们返回ref.value,如果不是的话 ,直接返回value就好了
  return isRef(ref) ? ref.value : ref
}

toRef

为什么需要 toRef? 使用 reactive 创建的响应式对象,如果要使得其中某一个属性的改变触发响应式更新,需要通过data.xxx.xxx 这种链式取值的方式,如果在 setupreactive 数据解构后导出,它会失去响应式toRef 就是为了保证响应式对象的某一属性在不丢失响应式的前提下,对其进行解构,使其变成 ref 类型。

Ref 的特点:

  1. toRef 接收两个参数,第一个参数是一个响应式对象,第二个参数是该对象的某一个键值
  2. toRef 返回的数据与响应式对象的数据是引用关系,两者任意一个发生改变,另一个也会随着改变。
  3. toRef 本质上就是一个代理;以 let nameRef = toRef(person,'name') 为例,它是通过 classgetsetnameRef.value 代理到 person.name;获取或修改 nameRef.value 相当于获取或修改 person.name由于 person.name 是响应式的,所以 toRef 返回的数据也是响应式的)。
  4. 经过 toRef 处理的数据也是 ref 类型。
export function toRef(target, key) {
  return new ObjectRefImpl(target, key);
}
class ObjectRefImpl {
  __v_isRef = true; // 经过 toRef 处理的数据也是ref类型
  constructor(public _object, public _key) {}
  // 一定要代理到value属性,就是为了和ref保持一致,使得 toRef 返回的数据和 ref 一样在 template 中可以直接使用,不必添加 value 属性的后缀
  get value() {
    return this._object[this._key];
  }
  set value(newValue) {
    this._object[this._key] = newValue;
  }
}

toRefs

toRefs 的特点:

  • 将一个响应式对象转为普通对象
  • 对象的每一个属性都是对应的 ref 类型数据
  • toRefs 返回的数据与原响应式对象保持引用关系
  • toRefs 本质是遍历对象,对每一个属性使用 toRef 进行代理,使得解构之后的对象属性仍具有响应式。

示例:

setup() {
    const state1 = reactive(obj1);
    const state2 = reactive(obj2);
    const stateRefs = toRefs(state1);
    const ageRef = toRef(state2, 'age')
    return stateRefs.asign({state1, ageRef})
}

toRefs 产生的对象可以直接返回,也可以拼接其它的 reftoRefreactive 数据一起返回。

export function toRefs(object) {
  const ret = {};
  for (let key in object) {
    ret[key] = toRef(object, key);
  }
  return ret;
}

proxyRefs

我们在 template 中使用 ref 类型的数据(reftoReftoRefs产生的数据)不用手动添加 .value,其原因就是 vue3 内部会将数据用 proxyRefs 做处理。 proxyRefs 会用 Proxy 代理一个对象

  1. 如果对象属性是 ref 类型,则将 getset 都代理到该属性的 .value 属性上;
  2. 如果对象属性不是 ref 类型,则正常取值和设值.
export function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key, receiver) {
      let v = Reflect.get(target, key, receiver);
      // 如果对象内部属性是 ref 类型,则取其 .value 值,否则直接取值
      return v.__v_isRef ? v.value : v;
    },
    // 如果新值不是ref,且旧值是ref,则替换调旧的value属性;
    // 否则整体替换
    set(target, key, value) {
      if (isRef(target[key]) && !isRef(value)) {
        return (target[key].value = value);
      } else {
        return Reflect.set(target, key, value, receiver);
      }
    },
  });
}

computed

使用

computed 计算属性可以接受一个 getter 函数,返回一个只读的响应式 ref 对象。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象

实现

具体实现:

  1. 根据 computed 传入的gettersetter,创建 ComputedRefImpl 实例;
  2. ComputedRefImpl 是一个 class,会创建get value()set value() 存/取值函数,并将class实例设置为 ref 类型;
  3. constructor 中会创建一个 _effect 实例:在取值函数中当 _dirtytrue 时会执行 _effect.run(),此时会计算出最新值,以及收集 computed 内部的依赖;在创建 _effect 时通过scheduler实现当内部依赖变化时不直接计算最新值,而是将_dirty设为true,并触发与计算属性相关的effect更新。
  4. 在取值函数中,如果当前存在 activeEffect,需要进行依赖收集,收集与当前计算属性相关的_effect
  5. 利用 _dirty 实现缓存效果:在取值函数中,如果 _dirtytrue,会执行 this.effect.run()(执行完再将 _dirty 设为 false),执行getter计算出最新值,并在读取到computed内部的响应式数据时进行依赖收集;当 computed 内部的依赖发生变化时,执行 _effectscheduler,将 _dirty 设为 true,并触发与计算属性相关effect的更新。
import { isFunction } from "@vue/shared";
import {
  ReactiveEffect,
  activeEffect,
  trackEffects,
  triggerEffects,
} from "./effect";

const noop = () => {};
class ComputedRefImpl {
  dep = undefined;
  effect;
  __v_isRef = true; // 设置为 ref 类型
  _dirty = true; // 是否需要重新计算
  _value; // 计算属性的缓存结果
  constructor(getter, public setter) {
    this.effect = new ReactiveEffect(getter, () => {
      // 属性更新时,触发trigger重新执行effect,但是不执行run,而是执行scheduler,将dirty设为true,下次取值时重新计算;
      this._dirty = true;
      // 2. 并且触发 this.dep 中所有effect(即计算属性作为依赖的effect)执行
      triggerEffects(this.dep);
    });
  }
  // get 和 set 是类的属性访问器,等价于 Object.defineProperty() 中的get、set
  get value() {
    if (activeEffect) {
      // 1. 读取计算属性时,如果存在activeEffect,意味着这个计算属性在effect中使用,需要让这个计算属性收集这个effect;那么当计算属性发生变化时,收集的 _effect 会重新执行
      trackEffects(this.dep || (this.dep = new Set()));
    }

    if (this._dirty) {
      // 第一次取值和取新值时才执行effect,并且把取到的值缓存起来
      this._value = this.effect.run(); // 执行计算属性的 getter,会进行依赖收集(计算属性依赖的响应式数据会收集计算属性)
      this._dirty = false;
    }
    return this._value;
  }
  set value(newValue) {
    this.setter(newValue);
  }
}

export function computed(getterOrOptions) {
  let onlyGetter = isFunction(getterOrOptions);
  let getter;
  let setter;
  if (onlyGetter) {
    getter = getterOrOptions;
    setter = noop;
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set || noop;
  }

  return new ComputedRefImpl(getter, setter);
}

watch 相关 API

watch

watch 的本质就是监听一个响应式数据,当数据发生变化时候,去执行相应的回调函数。 实现步骤:

  1. watch 可以接受三个参数:sourcecboptions
  2. source 可以是一个响应式对象(此时必须开启深度监听),也可以是一个函数
  3. 深度监听的实现是通过递归取值实现的
  4. watch 的实现本质是创建一个 ReactiveEffect 实例,默认先执行一次 effect.run(),会收集监听的数据;当数据变化时,执行 scheduler;在 scheduler 中重新执行 effect.run() 计算出新值,然后执行回调函数 cb(newValue, oldValue)
  5. 如果 options.immediatetrue,会默认先执行一次 scheduler
import { isReactive } from "./reactive";
import { ReactiveEffect } from "./effect";
import { isFunction, isObject } from "@vue/shared";

// 深度监听:通过递归取值实现
function traverse(source, s = new Set()) {
  if (!isObject(source)) {
    return source;
  }
  // 通过 Set 数据类型去重
  if (s.has(source)) {
    return source;
  }
  s.add(source);
  for (let key in source) {
    // 递归取值,取值过程就是收集依赖的过程,即实现了深度监听
    traverse(source[key], s);
  }
  return source;
}
export function watch(source, cb, options) {
  let getter;
  // 【关键】watch的第一个参数是对象时,该对象必须是响应式的
  if (isReactive(source)) {
    // watch第一个参数是响应式对象时,默认需要开启深度监听(通过traverse递归取值实现)
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    getter = source;
  }
  let oldValue;
  // watcher effect 的依赖变化时,就会执行scheduler(即job)
  const job = () => {
    // watch监听的数据变化时,执行scheduler中的effect.run(),即再次执行getter,拿到新值
    let newValue = effect.run();
    // 执行 watch 中的回调函数
    cb(newValue, oldValue);
    oldValue = newValue;
  };
  const effect = new ReactiveEffect(getter, job);
  // immediate为true会默认先执行一次回调
  if (options?.immediate) {
    return job();
  }
  oldValue = effect.run(); // 执行getter,收集依赖
}

watchEffect

watchEffect 的含义:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。 watchwatchEffect 的区别:

  1. watch 在回调函数cb中执行副作用;watchEffect 直接传入一个副作用函数,并立即执行。
  2. watch 就是一个 effect + 自定义 scheduler(在scheduler中执行effect.run()和回调cb);
  3. watchEffect 的第一个参数就是副作用函数,它就是一个 effect,当依赖变化时重新执行effect.run(),没有回调函数cb
  4. watchEffect 的副作用函数会立即执行,相当于 immediatetrue
  5. watchwatchEffect 都可以使用 doWatch 方法实现,该方法会返回一个函数(本质是执行 effect.stop())用来停止监听
function doWatch(source, cb, { immediate } = {} as any) {
  let getter;
  if (isReactive(source)) {
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    getter = source;
  }
  let oldValue;
  // watcher effect 的依赖变化时,就会执行scheduler(即job)
  const job = () => {
    // 2.1. 如果是watch API,在 scheduler 中重新执行effect.run(),并调用 cb
    if (cb) {
      let newValue = effect.run();
      cb(newValue, oldValue);
      oldValue = newValue;
    } else {
      // 2.2. 如果是 watchEffect API,在 scheduler 中重新执行 effect.run() 即可
      effect.run();
    }
  };
  const effect = new ReactiveEffect(getter, job);
  if (immediate) {
    return job();
  }
  oldValue = effect.run(); // 1. 执行getter,收集依赖
  // 返回一个函数,用来停止监听。本质就是调用effect.stop()
  return () => {
    effect.stop()
  }
}
export function watch(source, cb, options) {
  doWatch(source, cb, options);
}
export function watchEffect(effect, options) {
  doWatch(effect, null, options.assign(immediate: true));
}

停止侦听器:

const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()

watch/watchEffect 中的 onCleanup

无论是 watch 还是 watchEffect,可以发现在副作用函数中都可以接受一个 onCleanupwatchonCleanup 是回调的第三个参数,watchEffect 的是回调的第一个参数。 清理回调的方法 onCleanup 会在该副作用下一次执行前被调用,可以用来清理无效的副作用。 onCleanup 的实现步骤:

  1. 声明一个变量 cleanup,再声明一个函数 onCleanup;在 onCleanup 函数中将 cleanup 设为 onCleanup 的入参(即用户传入的回调);
  2. 然后在 effect.scheduler 中执行cleanup;(即每次执行scheduler前都会执行onCleanup的回调函数
  3. 再将onCleanup传入副作用函数。
function doWatch(source, cb, { immediate } = {} as any) {
  let getter;
  if (isReactive(source)) {
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    getter = source;
  }
  let oldValue;

  let cleanup;
  function onCleanup(userCb) {
    cleanup = userCb;
  }
  const job = () => {
    // 2.1. 如果是watch API,在 scheduler 中调用 cb
    if (cb) {
      let newValue = effect.run();
      // watch的回调函数入参中如果存在cleanup,优先执行cleanup
      if (cleanup) {
        cleanup();
      }
      // 在 watch 中,将 onCleanup 是作为cb的第三个参数传入
      cb(newValue, oldValue, onCleanup);
      oldValue = newValue;
    } else {
      // 2.2. 如果是 watchEffect API,在 scheduler 中再次执行 effect.run
      effect.run();
    }
  };
  const effect = new ReactiveEffect(getter, job);
  if (immediate) {
    return job();
  }
  oldValue = effect.run();
}

使用场景:现重复请求某一接口,期望以最新一次的请求结果为准。如果第一次接口请求速度慢,第二次重复请求该接口速度比较快,那么第一次请求的结果会覆盖掉第二次的请求结果,与预期不符;可以使用 onCleanup 将上一次未完成的请求取消/不进行渲染。

watch(source, async (old, new, OnCleanup) => {
  // 是否过期是标志
  let expired = false
  // 注册过期回调
  OnCleanup(()=> {
    expired = true
  })
 const res = await fetch('something')
 // 如果未过期,那么可以取res为finalData
 if (!expired) {
   finalData = res
 }
})

使用 OnCleanup 关键在于创建闭包,在下一次请求时将上一次的 flag 设为 false,导致上一次的请求结果无法渲染。

总结

本小节我们阐述了 vue3 中响应式系统的基本原理,以及相关API的具体实现;它相较于 vue2 有着重大改变,vue3的响应式系统拥有更好的性能,更低的耦合性,更方便的逻辑复用。 在完成数据响应式之后,接下来的问题就是:组件是如何渲染和更新的?接下来,我们将解决这个问题。