Vue响应式原理
响应性概念
响应性:当状态更新,系统会自动更新关联状态;在Web场景下,指的是不断变化的状态反映到DOM上的变化。
例如实现一个功能,使得变量b
的值总是变量a
的值的10倍。如果我们拥有一个magic function onAChanged()
,即当a
的值改变之后,自动调用该函数,则可以实现类似的功能。
// 当a的值改变, 调用该回调函数
onAChanged(() => {
document.querySelector('.cell.b1').textContent = a * 10;
// 上面的代码可以抽象为:
// view = render(state); // 当状态改变, 渲染对应的DOM元素
});
在React中,实现方式类似:
let update;
const onStateChanged = _update => {
update = _update;
};
// 必须使用setState更新状态
const setState = newState => {
state = newState;
update();
};
在Angular中,使用脏值检测实现,拦截例如点击等时间,检查数据是否被更新。
在Vue中,使用ES5的Object.defineProperty()
方法,重写对象所有属性的getter
和setter
方法。
getter和setter
通过ES5的Object.defineProperty()
方法,监听属性值的变更,注意:
- 下面的做法相当于,redefine对象
obj
的key
属性,所以要求configurable
不能为false
- 所以,当再次调用
convert(stu)
时,会报错
function convert(obj) {
// 通过forEach监听obj对象的每个属性
Object.keys(obj).forEach(key => {
let internalValue = obj[key]; // 通过闭包保存原来的值
Object.defineProperty(obj, key, {
configurable: false, // 该属性不能被redefine
get() {
console.log(`getting key "${key}": ${internalValue}`);
return internalValue;
},
set(newValue) {
console.log(`setting key "${key}" to: ${newValue}`);
internalValue = newValue;
}
});
});
}
let stu = { name: 'Lee', age: 20 };
convert(stu);
console.log(stu); // { name: [Getter/Setter], age: [Getter/Setter] }
let age = stu.age; // getting key "age": 20
stu.age = 50; // setting key "age" to: 50
// 由于上面已经将新的属性值设置为configurable: false, 所以不能进行redefine
// convert(stu); // TypeError: Cannot redefine property: name
可以看到,当使用convert()
方法转换stu
对象之后,每当读取/修改对象的属性时,都会收到提醒。
依赖追踪
我们期望实现一个Dep
类,它可以使用depend()
方法收集依赖项,当所依赖项发生改变时,使用notify()
方法触发依赖项的执行。
const dep = new Dep();
// 自动执行, 收集依赖项
autorun(() => {
dep.depend(); // 收集依赖项
console.log('updated');
})
dep.notify(); // 通知以上收集的依赖项: 所依赖的变量updated
实现一个真正的Dep
类:
let activeEffect; // 当前受依赖项影响的函数
class Dep {
subscribers = new Set(); // 所有受依赖项影响的函数
depend() { // 收集当前受依赖项影响的函数, 加入subscribers
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() { // 通知所有受依赖项影响的函数: 依赖项已经被改变, 需要执行函数
this.subscribers.forEach(effect => effect());
}
}
// 建立起effect函数与其依赖项的订阅关系: 当依赖项被改变, 执行effect()
function watchEffect(effect) {
activeEffect = effect;
effect();
}
如果我们要使用Dep
类,很明显,依赖项不能是普通的对象,而是需要设置过getter和setter的对象,该对象属性的getter和setter需要完成的额外功能是:
get()
:当值被读取时,使该值对应的dep
收集依赖set()
:当值被修改时,通知受该值依赖的函数执行
例如,我们实现上面提到的功能,变量b
的值总是变量a
的值的10倍:
/* use Dep */
const dep = new Dep();
let a = 0, b = 0;
const state = {}; // 以后需要使用state操作a, 从而实现对a的数据劫持
Object.defineProperty(state, 'a', {
get() {
dep.depend();
return a;
},
set(val) {
if (a !== val) {
a = val;
dep.notify();
}
}
});
// effect: () => {b = state.a * 10;}
// 首先在watchEffect函数中, 由于执行了effect(), 所以对state.a进行了读取, dep.depend()添加订阅
// 于是, 每当state.a的值改变, dep.notify()执行受state.a依赖的effect()
watchEffect(() => {
b = state.a * 10;
});
console.log(state.a); // 0
console.log(b); // 0
state.a = 10;
console.log(b); // 100
迷你观察者
我们将上面的convert()
函数和Dep
类进行结合,就得到了一个迷你观察者:
function observe(raw) {
// 1. 遍历对象的所有key
Object.keys(raw).forEach(key => {
// 2. 为每个key建立一个dep对象
const dep = new Dep();
// 3. 重写对象的key属性
let realVal = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend(); // 4. 读取key属性时, 建立依赖
return realVal;
},
set(newVal) {
realVal = newVal;
dep.notify(); // 4. key属性改变时, 通知被依赖的effect
}
});
});
return raw;
}
我们再实现上面的功能,就会更加简洁,不用手动重写getter和setter了:
let obj = {a: 1}; // 依赖项a
let b = 0;
observe(obj);
watchEffect(() => {
b = obj.a * 10;
});
console.log(b); // 10 (这是由于在watchEffect()中以及执行过effect()一次了)
obj.a = 20;
console.log(b); // 200
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!