Как реализовать собственную реактивную систему с нуля, подобную Vue's reactivity?
Реализация собственной реактивной системы с нуля — это создание механизма, который отслеживает зависимости между состоянием и функциями, реагирующими на его изменения. В Vue 3 реактивность основана на Proxy, с автоматическим отслеживанием и повторным выполнением эффектов. Ниже — пошаговая реализация реактивной системы по принципам Vue's reactivity, включая трекинг, триггеринг, reactive(), effect(), ref(), computed().
1. Основные понятия
-
Реактивный объект — обёртка над обычным объектом, отслеживающая чтение/запись свойств.
-
Эффект (effect) — функция, которая зависит от реактивных данных.
-
Депенденси трекинг — процесс регистрации зависимостей при чтении.
-
Триггеринг (trigger) — вызов всех эффектов при изменении значения.
2. Глобальный контекст эффекта
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn(); // выполняем, чтобы зарегистрировать зависимости
activeEffect = null;
}
3. Хранилище зависимостей
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach(fn => fn());
}
}
4. Реализация reactive()
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
const value = Reflect.get(obj, key, receiver);
track(obj, key); // регистрируем зависимость
return typeof value === 'object' && value !== null ? reactive(value) : value;
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver);
trigger(obj, key); // уведомляем об изменении
return result;
}
});
}
5. Пример использования reactive
const state = reactive({ count: 0 });
effect(() => {
console.log('count changed:', state.count);
});
state.count++; // → count changed: 1
6. Реализация ref()
ref оборачивает примитив в объект с .value, чтобы можно было использовать реактивность.
function ref(initialValue) {
const wrapper = {
get value() {
track(wrapper, 'value');
return initialValue;
},
set value(newVal) {
initialValue = newVal;
trigger(wrapper, 'value');
}
};
return wrapper;
}
const count = ref(0);
effect(() => {
console.log('ref value:', count.value);
});
count.value++; // → ref value: 1
7. Реализация computed()
function computed(getter) {
let cachedValue;
let dirty = true;
const runner = () => {
if (dirty) {
cachedValue = getter();
dirty = false;
}
return cachedValue;
};
effect(() => {
getter();
dirty = true;
});
return {
get value() {
return runner();
}
};
}
8. Реализация watch()
function watch(getter, callback) {
let oldValue;
const onEffect = () => {
const newValue = getter();
if (newValue !== oldValue) {
callback(newValue, oldValue);
oldValue = newValue;
}
};
effect(() => {
oldValue = getter();
});
effect(onEffect);
}
const state = reactive({ count: 0 });
watch(() => state.count, (newVal, oldVal) => {
console.log(\`count changed from ${oldVal} to ${newVal}\`);
});
9. Поддержка вложенных объектов
В реализации reactive() выше уже есть рекурсивный вызов:
return typeof value === 'object' && value !== null ? reactive(value) : value;
Это позволяет автоматически оборачивать вложенные структуры:
const obj = reactive({ nested: { value: 42 } });
effect(() => {
console.log(obj.nested.value);
});
obj.nested.value = 100; // триггер сработает
10. Поддержка массивов (базовая)
Для массивов track и trigger будут работать так же, как для объектов. Проблемы возникают при перезаписи длины и использовании методов (push, pop, splice) — потребуется проксирование Array.prototype, аналогично Vue.
Упрощённая реализация:
const arr = reactive(\[1, 2, 3\]);
effect(() => {
console.log(arr\[0\]);
});
arr\[0\] = 42; // триггер вызовется
11. Утилиты: isRef, unref, toRefs, proxyRefs
function isRef(obj) {
return obj && obj.\__isRef === true;
}
function unref(ref) {
return isRef(ref) ? ref.value : ref;
}
function toRefs(obj) {
const result = {};
for (const key in obj) {
result\[key\] = {
\__isRef: true,
get value() {
return obj\[key\];
},
set value(val) {
obj\[key\] = val;
}
};
}
return result;
}
12. Очистка эффектов
Чтобы избежать утечек памяти, эффекты должны быть деактивируемыми.
Упрощённая форма:
function effect(fn) {
const runner = () => {
cleanup(runner);
activeEffect = runner;
fn();
activeEffect = null;
};
runner.deps = \[\];
runner();
return runner;
}
function cleanup(effect) {
for (const dep of effect.deps) {
dep.delete(effect);
}
effect.deps.length = 0;
}
13. Расширение: Scheduler
Позволяет управлять временем запуска эффектов (например, debounce, throttle).
function effect(fn, options = {}) {
const runner = () => {
cleanup(runner);
activeEffect = runner;
fn();
activeEffect = null;
};
runner.scheduler = options.scheduler;
runner();
return runner;
}
function trigger(target, key) {
const deps = targetMap.get(target)?.get(key);
if (deps) {
deps.forEach(effect => {
if (effect.scheduler) {
effect.scheduler(effect);
} else {
effect();
}
});
}
}
14. Минимальная сборка всех частей
const state = reactive({ count: 0 });
const double = computed(() => state.count \* 2);
effect(() => {
console.log('double:', double.value);
});
state.count++;
15. Сравнение с Vue 3
Возможность | Vue 3 (Proxy) | Наша реализация |
---|---|---|
Трекинг по ключу | ✅ | ✅ |
--- | --- | --- |
Nested reactive | ✅ | ✅ |
--- | --- | --- |
Ref для примитивов | ✅ | ✅ |
--- | --- | --- |
Computed | ✅ | ✅ |
--- | --- | --- |
Watch | ✅ | ✅ |
--- | --- | --- |
Scheduler в effect | ✅ | частично |
--- | --- | --- |
Cleanup | ✅ | ✅ |
--- | --- | --- |
Devtools | ✅ | ❌ |
--- | --- | --- |
Optimизации по зависимостям | ✅ | ❌ (всё вызывается всегда) |
--- | --- | --- |