Vue3响应式原理

193 阅读5分钟

Vue3响应式原理

什么是响应式?以及为什么需要响应式?

可以参考下面这个例子,我们希望总价total可以根据单价price和数量quantity自动计算,而不是我们每次手动计算。

let price = 10, quantity = 2;
let total = price * quantity;

console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20

total = total = price * quantity;

console.log(`total: ${total}`); // total: 40

简单的来说,我们希望能够监听某个值的变化,并且在这个值发生变化后,能够执行相应的操作

实现单个值的手动响应

要实现这个响应式,我们需要解决3个问题:

  1. 需要知道什么时候值发生了变化(when)
  2. 需要知道值变化了要执行什么操作(what)
  3. 在值变化的时候,执行对应的操作(do)

我们先来解决whatdo,我们新增三个方法effecttracktrigger

let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ① 
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) };  // ②
const trigger = () => { dep.forEach( effect => effect() )};  // ③

track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40
quantity = 3;
trigger();
console.log(`total: ${total}`); // total: 60

我们创建一个depSet,来保存数据变化时要执行的操作

effect函数我们用来定义要执行的操作,这里我们用来计算total总价,

track函数我们用来想dep中添加要执行的操作

trigger函数则会帮我们执行所有需要执行的函数

这样,我们在使用的时候,先使用track添加要执行的操作,然后在对应值变化后,再调用trigger来执行对应的逻辑。

但是这样和我们想要的响应式还是有一点距离,现在每次改变数据后都需要手动执行trigger函数,做不到自动执行,我们后面再来优化这个问题。

实现单个对象的多个属性的手动响应

现在我们先考虑另一个问题,如果数据是对象,并且有多个对象,我们应该怎么来保存要执行的操作,以及如何来执行操作呢?

这里用到了ES6新的数据类型Map,相对于直接使用对象来存储键值对,Map可以支持更多的数据类型来作为键,而不像对象仅支持字符串来作为键。

这里我们用depsMap来存储不用的属性对应的在数据变化时要做的操作

let product = {price: 10, quantity: 2}, total;
const depsMap = new Map();

function track(key) {
    let dep = depsMap.get(key);
    
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    
    dep.add(effect);
}

function trigger(key) {
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach((effect) => {
            effect();
        })
    }
}

let effect = () => {
    total = product.price * product.quantity;
}

effect();
console.log(total); // total: 20

track("quantity");
product.quantity = 3;
trigger("quantity");
console.log(total); // total: 30

track("price");
product.price = 20;
trigger("price");
console.log(total); // total: 60


对多个对象的多个属性实现手动响应

要实现对多个对象的多个属性进行响应,我们实现的思路和之前类似。之前我们用了一个Map来保存不同属性的要执行的操作。现在我们也可以再使用一个WeakMap来保存不同对象的要执行的操作。

let product = { price: 5 };
let sales = { quantity: 2 }
let total = 0;
const targetMap = new WeakMap();

function track(target, key) {
    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()));
    }
    
    dep.add(effect);
}

function trigger(target, key) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach((effect) => {
      		effect();
    	});
    }
}

let effect = () => {
  	total = product.price * sales.quantity;
};

effect();
console.log(total); // total: 10

track(product, "price");
track(salse, "quantity");

product.price  = 10;
trigger(product, "price");
console.log(total); // total: 20

sales.quantity = 3;
trigger(salse, "quantity");
console.log(total); // total: 30

到目前为止,我们不同对象上不同的键的关系如下图:

map-depsmap.jpg

变为自动响应

我们使用ProxyReflect来实现自动响应

const targetMap = new WeakMap();

function track(target, key) {
    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()));
    }
    dep.add(effect);
}

function trigger(target, key) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach((effect) => {
      		effect();
    	});
    }
}

let effect = () => {
  	total = product.price * sales.quantity;
};

function reactive(target) {
    const handlers = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            let oldValue = target[key];
            let result = Reflect.set(target, key, value, receiver);
            if (result && oldValue != value) {
                trigger(target, key);
            }
            return result;
        }
    }
    return new Proxy(target, handlers);
}

let product = reactive({ price: 5, quantity: 2 });
let total = 0;

effect();
console.log(total); // output: 10

product.quantity = 3;
console.log(total); // output: 15

优化自动响应过程

上述代码还有两个问题:

  1. effect是固定的,不能设置不同的effect
  2. 在我们设置quantity3的时候,trigger调用了对应的effect,这里的effect函数执行来计算的total时,会再走一遍proxy中的get流程。所以就会再次触发track的流程,但是我们并不需要触发track然后再保存一遍effect

可以优化这两个问题

我们增加一个全局变量activeEffect,默认为null,用来保存需要被track函数保存的操作,当activeEffectnull时,track函数不做任何操作。

function track(target, key) {
    if (activeEffect) {
     	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()));
    	}
    	dep.add(effect);   
    }
}

然后修改effect函数的实现,使得只有在调用effect函数时,activeEffect才可能有值,可以执行track函数对应的逻辑

function effect(eff) {
    activeEffect = eff;
  	activeEffect();
  	activeEffect = null;
}

完整的代码如下,其余部分没有做修改

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

function track(target, key) {
    if (activeEffect) {
     	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()));
    	}
    	dep.add(effect);   
    }
}

function trigger(target, key) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach((effect) => {
      		effect();
    	});
    }
}

function reactive(target) {
    const handlers = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            let oldValue = target[key];
            let result = Reflect.set(target, key, value, receiver);
            if (result && oldValue != value) {
                trigger(target, key);
            }
            return result;
        }
    }
    return new Proxy(target, handlers);
}

function effect(eff) {
    activeEffect = eff;
  	activeEffect();
  	activeEffect = null;
}

let product = reactive({ price: 5, quantity: 2 });
let salePrice = 0;
let total = 0;

effect(() => {
  	total = product.price * product.quantity;
});

effect(() => {
  	salePrice = product.price * 0.9;
});

console.log(total, salePrice); // output: 10, 4.5

// 设置quantity只会重新计算total
product.quantity = 3;
console.log(total, salePrice); // output: 15, 4.5

// 设置price后,total和salePrice对应的effect都会执行,都会被重新计算
product.price = 10;
console.log(total, salePrice); // output: 30, 9

Vue3 Composition APIreactive 方法的实现流程已经被我们手动大致实现了

实现ref

Vue3 Composition API 设计中,reactive 主要用于引用类型,另外专门提供了一个 ref 方法实现对原始类型的响应式。

function ref(raw) {
  const r = {
    get value() {
      // 在get之前,先保存到targetMap中
      track(r, 'value');
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      // set了之后,触发effect更新
      trigger(r, 'value');
    },
  };
  return r;
}

实现computed

function computed(getter) {
  	// 创建一个响应式的引用
  	let result = ref();
  	// 用effect封装着调用getter,将结果设给result.value的同时,也将eff保存在了targetMap中的对应位置
  	effect(() => (result.value = getter()));
  	// 最后把result返回
  	return result;
}

Vue3源码

Vue3 整体是用 Typescript 写的,reactivity 是一个独立的模块,源代码位于packages/reactivity/src目录下,几个不同的方法分别位于不同文件中