Skip to content

Vue3 Reactivity

线上DEMO

stackBlitz:https://stackblitz.com/edit/stackblitz-webcontainer-api-starter-pzbq1g?file=index.html

github地址: https://github.com/Wadehl/vue-easy-reactivity

Vue2

在Vue2里面,响应式主要是通过Object.definePropertyget()与set()来收集依赖与通知依赖更新,总的来说就是Object.defineProperty+发布订阅模式实现的,而因为Object.defineProperty无法深层监听,所以需要递归实现深度监听。并且,由于原生只支持getset,当我们向Object新增键或删除键的时候,这些都是不会被监听到的,需要调用Vue.$set才能实现新增的监听。

Vue3

到了Vue3,出现了新的响应式系统reactivity system,依赖于这个系统,Vue的响应式变得更加强大了,主要的APIreactiveref,下面实现一些简单点的版本。

Vue源码对应的位置为:core/packages/reactivity/src,我们主要聚焦baseHandlerseffectreactiveref这几个文件

reactive

reactive主要是针对引用类型进行处理的,Vue官方将合法类型分为commoncollectioncommonobjectarray,而collectionMap/Set/WeakMap/WeakSet这四类。

首先,reactive的实现离不开ES6新的API Proxy

js
const proxy = new Proxy(target, handler);
const proxy = new Proxy(target, handler);

通过handler里面配置类似的get,set即可实现原本的效果,并且Proxy原生支持深层监听,不仅如此,hasdeleteProperty更是支持了对查找与删除的监听,并且在对应的get/has收集依赖,在set/deleteProperty触发effect即可实现响应式系统。即reactive+effect+handler几个关键部分。

一个只针对obj类型的简单实现

1. 定义reactive函数

这里做的事情其实比较简单,就是定义了一个标识符,然后定义了一个缓存用的WeakSetcreateReactiveObject通过判断缓存是否存在。这样做优化了性能,避免重复代理,最后依赖proxy实现。

js
import { isObject } from './utils';
import { mutableHandlers, shallowMutableHandlers } from './baseHandlers.js'; // proxy handlers

export const ReactiveFlags = {
  isReactive: '__v_isReactive', // reactive的标识符
  RAW: '__v_isRaw'
}

export const reactiveMap = new WeakMap(); // weakmap缓存已经reactive的变量

export function reactive(target) {
  return createReactiveObject(
    target,
    mutableHandlers,
    reactiveMap,
  );
}

export function isReactive(target) {
  return !!target[ReactiveFlags.isReactive]; // 根据flag判断
}

function createReactiveObject(target, proxyHandlers, proxyMap) {
  if (!isObject(target)) {
    // 常规变量也直接返回
    return target;
  }
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    // 缓存里有直接返回
    return existingProxy;
  }

  const proxy = new Proxy(target, proxyHandlers);
  proxyMap.set(target, proxy); // 新增缓存

  return proxy;
}
import { isObject } from './utils';
import { mutableHandlers, shallowMutableHandlers } from './baseHandlers.js'; // proxy handlers

export const ReactiveFlags = {
  isReactive: '__v_isReactive', // reactive的标识符
  RAW: '__v_isRaw'
}

export const reactiveMap = new WeakMap(); // weakmap缓存已经reactive的变量

export function reactive(target) {
  return createReactiveObject(
    target,
    mutableHandlers,
    reactiveMap,
  );
}

export function isReactive(target) {
  return !!target[ReactiveFlags.isReactive]; // 根据flag判断
}

function createReactiveObject(target, proxyHandlers, proxyMap) {
  if (!isObject(target)) {
    // 常规变量也直接返回
    return target;
  }
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    // 缓存里有直接返回
    return existingProxy;
  }

  const proxy = new Proxy(target, proxyHandlers);
  proxyMap.set(target, proxy); // 新增缓存

  return proxy;
}

2. handlers的实现

handlers里主要的点在于依赖的收集,以及触发对应的effect,依赖收集主要是靠track函数,而触发effect则是依靠的trigger,那么只需要在访问的时候(get/has)进行收集,当修改的时候(set/deleteProperty)触发effect

js
import {
  reactive,
  ReactiveFlags,
  reactiveMap,
  shallowReactiveMap
} from './reactive.js';

import { isObject, hasOwn } from '../utils/index.js';
import { trigger, track } from './effect.js';

const createGetter = () => {
  return function get(target, key, receiver) {
    const isExistInMap = () => key === ReactiveFlags.RAW && (receiver === reactiveMap.get(target) || receiver === shallowReactiveMap.get(target));
    if (key === ReactiveFlags.isReactive) {
      return true;
    } else if (isExistInMap()) {
      return target;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);
    if (isObject(res)) {
      return reactive(res); // 返回reative后的对象,deep
    }

    return res;
  }
}


const createSetter = () => {
  return function (target, key, val, receiver) {
    const res = Reflect.set(target, key, val, receiver);
    trigger(target, 'set', key);
    return res;
  }
}

const get = createGetter();
const set = createSetter();

function has(target, key) {
  const res = Reflect.has(target, key);
  track(target, 'has', key);
  return res;
}

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key);
  const res = Reflect.deleteProperty(target, key);
  if (res && hadKey) {
    trigger(target, 'delete', key);
  }
  return res;
}

export const mutableHandlers = {
  get,
  set,
  has,
  deleteProperty,
}
import {
  reactive,
  ReactiveFlags,
  reactiveMap,
  shallowReactiveMap
} from './reactive.js';

import { isObject, hasOwn } from '../utils/index.js';
import { trigger, track } from './effect.js';

const createGetter = () => {
  return function get(target, key, receiver) {
    const isExistInMap = () => key === ReactiveFlags.RAW && (receiver === reactiveMap.get(target) || receiver === shallowReactiveMap.get(target));
    if (key === ReactiveFlags.isReactive) {
      return true;
    } else if (isExistInMap()) {
      return target;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);
    if (isObject(res)) {
      return reactive(res); // 返回reative后的对象,deep
    }

    return res;
  }
}


const createSetter = () => {
  return function (target, key, val, receiver) {
    const res = Reflect.set(target, key, val, receiver);
    trigger(target, 'set', key);
    return res;
  }
}

const get = createGetter();
const set = createSetter();

function has(target, key) {
  const res = Reflect.has(target, key);
  track(target, 'has', key);
  return res;
}

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key);
  const res = Reflect.deleteProperty(target, key);
  if (res && hadKey) {
    trigger(target, 'delete', key);
  }
  return res;
}

export const mutableHandlers = {
  get,
  set,
  has,
  deleteProperty,
}

3. effect与track/trigger

effect是这里非常关键的一部分,它用于创建副作用函数,使得内部可以自动追踪内部响应式数据的变化,当监听到变化的时候会自动重新执行副作用函数。Vue3中的多个关键API-watch/watchEffect/computed都与它相关联。

下面是关键代码的简单实现:

targetMap用于存放监听的targetObjectkey对应的依赖depsSetactiveEffect存储的是此时的effect

effect实际上依赖于类ReactiveEffectparent这个指针的存在主要是因为effect嵌套的问题,当我们嵌套后,this会指向最深的effect,此时我们对嵌套effect的依赖收集则会出现问题,通过parent改变activeEffect的指向,使得依赖能够被正确收集。

tracktrigger的实现相对就简单一些了,就是往targetMap内添加/递归访问收集的依赖副作用effect,最后调用run来执行effect.fn

targetMap数据结构图:

image-20230903132902378

js
const targetMap = new WeakMap();
let activeEffect = null;

export function effect(fn, options = {}) {
  if (fn.effect) {
    fn = fn.effect.fn; // 如果fn本来就是effect,就获取effect的fn 
  }

  const _effect = new ReactiveEffect(fn); // 构建新的effect

  if (!options || !options.lazy) {
    _effect.run();
  }

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}

class ReactiveEffect {
  deps = [];
  parent = activeEffect; // 指针,指向上一个effect
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    try {
      this.parent = activeEffect; // 保存上一个activeEffect
      activeEffect = this;
      return this.fn();
    } finally {
      //cleanUpEffect(this) // 这里清理一些本次未被访问依赖deps,优化性能
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
}

export function track(target, type, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
  }
  if (!deps.has(activeEffect) && activeEffect) {
    deps.add(activeEffect);
  }
  depsMap.set(key, deps);
}

export function trigger(target, type, key) {
  /**
   * depsMap {
   *  deps: {
   *    key: [effect1, effect2, ...]
   *  }
   * }
   */
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return; // 没有依赖
  }
  const deps = depsMap.get(key);
  if (!deps) {
    return; // 依赖没有effect
  }
  deps.forEach((effectFn) => {
    effectFn.run();
  })
}
const targetMap = new WeakMap();
let activeEffect = null;

export function effect(fn, options = {}) {
  if (fn.effect) {
    fn = fn.effect.fn; // 如果fn本来就是effect,就获取effect的fn 
  }

  const _effect = new ReactiveEffect(fn); // 构建新的effect

  if (!options || !options.lazy) {
    _effect.run();
  }

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}

class ReactiveEffect {
  deps = [];
  parent = activeEffect; // 指针,指向上一个effect
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    try {
      this.parent = activeEffect; // 保存上一个activeEffect
      activeEffect = this;
      return this.fn();
    } finally {
      //cleanUpEffect(this) // 这里清理一些本次未被访问依赖deps,优化性能
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
}

export function track(target, type, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
  }
  if (!deps.has(activeEffect) && activeEffect) {
    deps.add(activeEffect);
  }
  depsMap.set(key, deps);
}

export function trigger(target, type, key) {
  /**
   * depsMap {
   *  deps: {
   *    key: [effect1, effect2, ...]
   *  }
   * }
   */
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return; // 没有依赖
  }
  const deps = depsMap.get(key);
  if (!deps) {
    return; // 依赖没有effect
  }
  deps.forEach((effectFn) => {
    effectFn.run();
  })
}

Ref的实现

ref本质上类似于Object.defineProperty,只是用了get value()set value()进行track/trigger,这也是为什么我们使用ref变量的时候,获取值需要使用.value的原因

js
import { isObject } from "../utils/index.js";
import { track, trigger } from "./effect.js";
import { reactive } from "./reactive.js";

export function ref(val) {
  return createRef(val);
}

export function createRef(val) {
  if (isRef(val)) {
    return val;
  }
  return new RefImpl(val);
}

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

class RefImpl {
  constructor(val) {
    this.__v_isRef = true;
    this._val = convert(val);
  }
  get value() {
    track(this, 'get', 'value');
    return this._val;
  }
  set value(newVal) {
    trigger(this, 'set', 'value');
    this._val = convert(newVal);
  }
}

const convert = (val) => {
  return isObject(val) ? reactive(val) : val; 
}
import { isObject } from "../utils/index.js";
import { track, trigger } from "./effect.js";
import { reactive } from "./reactive.js";

export function ref(val) {
  return createRef(val);
}

export function createRef(val) {
  if (isRef(val)) {
    return val;
  }
  return new RefImpl(val);
}

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

class RefImpl {
  constructor(val) {
    this.__v_isRef = true;
    this._val = convert(val);
  }
  get value() {
    track(this, 'get', 'value');
    return this._val;
  }
  set value(newVal) {
    trigger(this, 'set', 'value');
    this._val = convert(newVal);
  }
}

const convert = (val) => {
  return isObject(val) ? reactive(val) : val; 
}

总结

Vue2相比,Reactivity改用了Proxy的方式实现了响应式,这样比起原本的get/set更多出了hasOwn/delete/has等多种API的响应式支持。不仅如此,通过effect+track/trigger实现的依赖收集替代了之前的Watcher的发布订阅者模式。另外,值得一提的是,Vue3中在响应式设计上考虑了层级嵌套依赖收集与清理不必要依赖的问题。

:::tips

Vue 3.2中使用二进制标记位方式选择性增加与清理依赖dep,代替了原本cleanup(全清空)的方案,进一步优化了性能。PR

:::

Released under the MIT License.