vue的响应式原理是利用副作用函数注册依赖,当副作用执行时,就会触发记录,在更新数据时,又重新执行副作用函数,实现响应式。
记录和更新是通过劫持对象的get/set操作,在get时记录依赖,在set时触发依赖,实现的。
本文参考Vue 3.4.38源码
简版源码
let activeEffect = null;
// packages/reactivity/src/dep.ts 源码中使用Map用于记录_trackId,这里不需要直接使用set
class Dep {
constructor() {
this.subscribers = new Set();
}
}
// 对应源码的effect,当副作用执行时,就会记录为activeEffect,然后被Dep记录为依赖
// 参考vue3.4.38 packages/reactivity/src/effect.ts
class ReactEffect {
constructor(effect) {
this.effect = effect;
this.run();
}
run() {
activeEffect = this;
this.effect();
activeEffect = null;
}
update() {
this.run();
}
}
// 参考 vue3.4.38 packages/reactivity/src/reactiveEffect.ts
let targetMap = new WeakMap();
// 参考 vue3.4.38 packages/reactivity/src/effect.ts
function effect(fn) {
return new ReactEffect(fn);
}
// 这个源码中没有,用于辅助获取 target 对应 key 的 dep 的
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
// depMap是用来给 代理对象的每个属性都添加依赖记录的
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
// 添加依赖
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// 参考 vue3.4.38 packages/reactivity/src/reactiveEffect.ts
function track(target, key) {
const dep = getDep(target, key);
if (activeEffect) {
dep.subscribers.add(activeEffect);
}
}
// 参考 vue3.4.38 packages/reactivity/src/reactiveEffect.ts
function trigger(target, key) {
const dep = getDep(target, key);
dep.subscribers.forEach((sub) => sub.update());
}
// 模拟 reactive API的实现
// 参考 vue3.4.38 packages/reactivity/src/reactive.ts
function reactive(target) {
const handler = {
get(target, key) {
// 记录依赖
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
Reflect.set(target, key, value);
// 触发依赖
trigger(target, key);
},
};
return new Proxy(target, handler);
}
// 数据
const count = reactive({ value: 0 });
// 事件
window.addEventListener("click", () => {
count.value++;
});
// 渲染
function render() {
document.body.innerHTML = count.value;
}
// vue内部处理render、computed等都使用了effect函数包裹
effect(() => {
render();
});
创建响应对象和数据劫持
创建响应对象
源码位置 packages/reactivity/src/reactive.ts
export const reactiveMap = new WeakMap();
function createReactiveObject(target, baseHandlers, proxyMap) {
if (typeof target !== 'object' || target === null) {
return target;
}
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
export function reactive(target) {
// ...
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
依赖订阅和更新
源码位置packages/reactivity/src/dep.ts
const targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
整体流程
- 创建响应式对象:通过
reactive函数将普通对象转换为响应式对象。 - 依赖收集:在读取对象属性时,收集依赖于该属性的副作用函数。
- 触发更新:在设置对象属性时,通知所有依赖于该属性的副作用函数执行。
为什么使用Proxy而不是defineProperty
vue2中使用Object.defineProperty来劫持数据,vue3使用Proxy来劫持数据
- 对象属性监听能力
- 只能监听已存在的属性,新增或删除属性需要手动使用
Vue.set和Vue.delete Proxy是直接代理整个对象,他可以自动监听属性的新增和删除
- 只能监听已存在的属性,新增或删除属性需要手动使用
- 数组监听
Object.defineProperty不能监听数组的变化,因此vue2需要重写数组方法来记录依赖Proxy可以直接监听数组的所有操作
- 性能问题
Object.defineProperty因为是对属性的代理,所以对象需要递归遍历,性能较弱Proxy直接代理整个对象
- 监听Map、Set数据结构
Object.defineProperty不能监听,Proxy可以监听
ref和reactive(普通类型的响应式处理)
reactive函数只能支持引用类型,所以vue3使用ref来处理普通类型,通过构建一个RefImpl对象,让普通类型也可以通过get set处理依赖
- ref对于value是引用类型的值,会直接使用reactive处理
- ref对于value是普通类型的值,会通过
get value方法记录依赖,通过set value触发依赖
源码位置 packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T = any> {
_value: T;
private _rawValue: T;
dep: Dep = new Dep();
public readonly [ReactiveFlags.IS_REF] = true;
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value);
this._value = isShallow ? value : toReactive(value);
}
get value() {
this.dep.track();
return this._value;
}
set value(newValue) {
const oldValue = this._rawValue;
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue;
this._value = newValue;
this.dep.trigger();
}
}
}
reactive和shallowReactive
reactive 和 shallowReactive 函数,用于创建深度和浅度响应式对象
reactive会递归遍历对象的所有引用类型属性,让整个对象都有响应式shallowReactive只会遍历对象的第一层的属性,只有第一层有响应式
ref和shallowRef同理
computed和scheduler
computed的get会被包装成一个副作用函数effect,不同于render,computed只在dirty为true时才重新计算
他是通过scheduler来控制dirty的
源码位置:packages/reactivity/src/computed.ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
constructor(
private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
) {
this.effect = new ReactiveEffect(
() => getter(this._value),
)
this.effect.computed = this
}
}