从零实现轻量化前端状态管理库:响应式原理与依赖追踪

引言

现代前端框架(如Vue、React)的核心之一是响应式数据流。但你是否好奇这些框架背后的状态管理库是如何工作的?本文将从零开始,手写一个轻量级的状态管理库,深入探讨响应式原理与依赖追踪。通过这一过程,你将理解Vue的reactivewatchEffect的底层机制,并掌握实现一个最小化状态管理库的关键技术。

Reactive system overview

核心概念:响应式与依赖追踪

要构建状态管理库,首先需要理解两个核心概念:

  • 响应式数据:当数据发生变化时,自动更新相关视图或副作用。
  • 依赖追踪:在读取数据时记录哪些依赖(如组件、函数)使用了该数据,并在数据变化时通知它们。

核心思想

我们将使用发布-订阅模式来实现依赖追踪。每个响应式属性都有一个“依赖列表”(订阅者列表)。当属性被读取时,将当前的“观察者”(例如一个正在执行的副作用函数)添加到该属性的依赖列表中;当属性被赋值时,通知所有订阅者执行更新。

第一步:实现一个简单的响应式系统

1.1 全局变量和辅助函数

首先,我们需要一个全局变量来存储当前正在执行的观察者(例如一个副作用函数)。同时,我们需要一个函数来运行副作用,并在运行期间将自身设置为当前观察者。

let activeEffect = null; // 当前激活的副作用函数

function effect(fn) {
  activeEffect = fn;
  fn(); // 执行fn,触发依赖收集
  activeEffect = null; // 执行完毕,清除
}

1.2 响应式函数 reactive

reactive函数接收一个普通对象,并返回一个代理(Proxy),拦截属性的读取和设置操作。

function reactive(target) {
  // 使用Map存储每个属性对应的依赖集合
  const depsMap = new Map();

  const handler = {
    get(target, key, receiver) {
      // 如果存在当前激活的副作用,则进行依赖收集
      if (activeEffect) {
        let deps = depsMap.get(key);
        if (!deps) {
          deps = new Set();
          depsMap.set(key, deps);
        }
        deps.add(activeEffect); // 将当前副作用加入依赖
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 通知依赖该属性的所有副作用
      const deps = depsMap.get(key);
      if (deps) {
        deps.forEach(effect => effect());
      }
      return result;
    }
  };

  return new Proxy(target, handler);
}

1.3 使用示例

const state = reactive({ count: 0, name: 'hello' });

effect(() => {
  console.log('Count changed:', state.count);
});

effect(() => {
  console.log('Name changed:', state.name);
});

state.count = 1; // 输出: Count changed: 1
state.name = 'world'; // 输出: Name changed: world

第二步:支持深层嵌套与数组

上述实现仅处理了单层对象。实际应用中,对象可能多层嵌套,或者包含数组。我们需要递归地将所有嵌套对象变为响应式。

Nested reactive object

2.1 递归代理

修改reactive函数,使其对嵌套对象也创建代理。使用一个proxyMap来缓存已代理的对象,避免重复代理。

const proxyMap = new WeakMap();

function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  // 如果已经代理过,直接返回
  if (proxyMap.has(target)) {
    return proxyMap.get(target);
  }
  const depsMap = new Map();

  const handler = {
    get(target, key, receiver) {
      if (activeEffect) {
        let deps = depsMap.get(key);
        if (!deps) {
          deps = new Set();
          depsMap.set(key, deps);
        }
        deps.add(activeEffect);
      }
      const value = Reflect.get(target, key, receiver);
      // 如果值是对象,递归使其响应式
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }
      return value;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 如果值变化才触发通知
      if (oldValue !== value) {
        const deps = depsMap.get(key);
        if (deps) {
          deps.forEach(effect => effect());
        }
      }
      return result;
    }
  };

  const proxy = new Proxy(target, handler);
  proxyMap.set(target, proxy);
  return proxy;
}

2.2 支持数组

数组的特殊性在于其索引访问、push、pop等方法。Proxy默认可以拦截数组索引,但像push这样的方法会触发多次get/set。我们使用一个技巧:在get时,如果key是数组方法(如'push'),则返回一个包装函数,自动追踪依赖。但为了简单,我们可以直接使用上述实现,数组的索引修改也能触发响应。

第三步:添加计算属性(computed)和监听(watch)

3.1 实现computed

计算属性是一个依赖其他响应式数据的“懒”值,只有在被访问时才重新计算,并且结果会被缓存。

function computed(getter) {
  let value;
  let dirty = true; // 标记是否需要重新计算

  const effectFn = () => {
    dirty = true;
    // 当依赖变化时,标记dirty,并通知依赖computed的副作用
    if (onDepend) onDepend();
  };

  let onDepend = null;

  const obj = {
    get value() {
      if (dirty) {
        activeEffect = effectFn;
        value = getter();
        activeEffect = null;
        dirty = false;
      }
      // 如果有当前激活的副作用,将当前computed也加入依赖
      if (activeEffect) {
        if (!onDepend) {
          onDepend = () => {
            // 通知依赖computed的副作用
            if (activeEffect) activeEffect();
          };
        }
        // 将当前副作用加入依赖(通过effectFn间接触发)
        effectFn(); // 实际上这里需要更精细的处理,简化版略
      }
      return value;
    }
  };

  return obj;
}

为了简化,我们暂不实现computed的依赖传播。完整工作留给读者思考。

3.2 实现watch

watch函数监听一个响应式数据,并在其变化时执行回调。

function watch(source, callback) {
  effect(() => {
    // 读取source,触发依赖收集
    const newValue = typeof source === 'function' ? source() : source;
    // 回调在新值变化时被调用(需要更复杂的设计来获取新旧值)
    callback(newValue);
  });
}

更完善的实现应该支持获取旧值和新值,这里仅作演示。

第四步:封装为状态管理库

将以上功能组织成一个轻量级的库。提供reactive, effect, computed, watch等API。同时增加一个简单的状态容器:

class Store {
  constructor(initialState) {
    this.state = reactive(initialState);
  }

  // 类似Vuex的commit或dispatch
  commit(mutation, payload) {
    // 简化,直接修改状态
    this.state[mutation] = payload;
  }
}

性能优化和注意事项

  • 避免不必要的通知:在set时比较新旧值,只有变化时才触发。
  • 批量更新:可以使用微任务队列合并多次更新,提高性能。
  • 内存泄漏:当组件卸载时,需要清理依赖(使用WeakMap或手动取消)。

Performance optimization

结语

通过本文,我们从零实现了一个轻量级的状态管理库,理解了响应式原理和依赖追踪的底层机制。这个简易库虽然不够完善,但已经揭示了Vue、MobX等库的核心思想。希望这能帮助你更好地使用现有的框架和库,甚至根据需求定制自己的状态管理方案。

如果你有任何问题,欢迎在评论区讨论!

觉得内容不错?我要

评论 暂无评论
暂无评论,快来抢沙发吧~