1. 响应式思想
要理解响应式原理,先理解一下什么是响应式。看下面的简单代码
let count = 10;
// 使用到count的一段代码,暂且封装成一个函数
function useCountFn() {
console.log(count * 2);
}
// 修改count的值
count = 20;
- count 有一个初始化的值,有一段代码使用到了这个值;
- 那么在count 有一个新的值时,这段使用到count 的一段代码可以自动重新执行;
这就是响应式最本质的思想。
类比我们在使用 Excel 电子表格 时的场景。
这里单元格 A3 中的值是通过公式 = A1 + A2 来定义的 (你可以在 A3 上点击来查看或编辑该公式),因此最终得到的值为 3。你可以试着更改 A1 或 A2,你会注意到 A3 也会随即 自动更新。
2. 对象的响应式
在vue中,响应式更多的体现在对象中。即使普通类型想要变成响应式的,也会封装在ref
对象中。
const obj = {
name: 'mono',
age: 18,
}
// 使用到对象中某个字段的一段代码
function useObjFn() {
console.log(obj.name);
}
// 在某处修改该字段
obj.name = 'haha'
即当我们有一段代码使用到某个响应式对象中的字段时,再其他地方修改了这个字段,使用到该字段的代码也会自动执行。
下面就来一步步实现这种思想!
3. 响应式函数的封装
// 封装一个响应式函数
const reactivesFns = [];
function watchFn(fn) {
reactiveFns.push(fn);
}
const obj = {
name: 'mono',
age: 18,
}
// 将需要执行的代码放入响应式函数中
watchFn(function() {
console.log(obj.name);
});
watchFn(() => {
console.log("另一个用到name字段的函数", obj.name);
})
obj.name = 'haha';
// 现在需要手动来执行收集的响应式函数
reactiveFns.forEach(fn => fn());
现在收集到的响应式函数全放在一个全局变量reactiveFns
里,而且真实开发中也不是只有一个对象,显然这样并不够合理。进行下一步的封装。
4. 封装依赖收集类
// 定义一个依赖管理的类
class Depend {
constructor() {
this.reactiveFns = [];
}
addDepend(fn) {
this.reactiveFns.push(fn)
}
notify() {
this.reactiveFns.forEach(fn => fn());
}
}
// 针对每一个对象的属性都新建一个对象来管理
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn)
}
const obj = {
name: 'mono',
age: 18,
}
watchFn(function() {
console.log(obj.name);
});
watchFn(() => {
console.log("另一个用到name字段的函数", obj.name);
})
obj.name = 'haha';
depend.notify()
这样封装之后,每一个对象的属性都对应一个Depend
对象来管理个字的依赖。离理想越来越近了。但现在总不能每次改变都要手动去notify
吧。是时候要自动监听对象的变化了。那么要怎么样才能监听对象属性的变化呢?有2种方式: Proxy
和Object.defineProperty
,Proxy
就是Vue3的响应式原理,Object.defineProperty
则是Vue2的响应式原理。
5. 自动监听对象变化
class Depend {
constructor() {
this.reactiveFns = [];
}
addDepend(fn) {
this.reactiveFns.push(fn)
}
notify() {
this.reactiveFns.forEach(fn => fn());
}
}
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn)
}
const obj = {
name: 'mono',
age: 18,
}
// 监听对象变化
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 设置完值之后通知响应式函数执行
depend.notify();
}
})
// 之后的操作,都应该是对代理对象 peoxyObj 的操作
watchFn(function() {
console.log(proxyObj.name);
});
watchFn(() => {
console.log("另一个用到name字段的函数", proxyObj.name);
})
// 只有对代理对象的操作才会被监听到
proxyObj.name = 'haha';
现在每次改变name属性,均能自动执行需要响应的代码。但是,却有一个问题就是,如果改变的是age属性,响应式函数也执行了,这并不是我们想要的。
那么到底该如何管理各个字段的依赖呢?
这里就涉及到Map
这个数据结构了。
每个对象的字段对应自己的depend
对象。而且每一个对象也需要保存在Map
里,这里推荐使用的是WeakMap
,大家可以思考一下为什么要使用WeakMap
呢?
最终的数据结构如下:
6. 依赖收集的管理
// 获取依赖的方法
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if(!map) {
map = new Map();
targetMap.set(target, map)
}
// 获取depend对象
let depend = map.get(key);
if(!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 在此处获取各自对象和key对应的depend对象
const depend = getDepend(target, key);
depend.notify();
}
})
此时针对每一个对象和每一个对象的字段都做到了各自的管理,但是还无法自动收集依赖项。于是我们想到了,要在get时来收集依赖,来吧,一步步实现它。
7. 正确的收集依赖
// 需要定义一个全局变量来保存当前依赖需要收集的响应式函数
let activeReactiveFn = null;
// 修改 watchFn 函数 如下
function watchFn(fn) {
activeReactiveFn = fn;
// 只有执行了fn,才能知道当前使用了哪个属性,才能保存对应的依赖。
fn();
activeReactiveFn = null;
}
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
// 在此处自动收集依赖
const depend = getDepend(target, key);
depend.addDepend(activeReactiveFn);
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 在此处获取各自对象和key对应的depend对象
const depend = getDepend(target, key);
depend.notify();
}
})
这时我们就能自动收集各个属性各自对应的依赖了。
但是,在监听对象变化时,我们却依赖了一个全局变量。这样不利于后面的封装,所以可以对Depend
类进行重构。
还有一个问题就是,如果收集依赖时,有相同属性的相同依赖时,会重复收集的问题。
下面来解决这些问题。
8. 重构Depend, 修复重复收集问题
// 需要定义一个全局变量来保存当前依赖需要的响应式函数
let activeReactiveFn = null; // 将此变量提升到最上方
class Depend {
constructor() {
// 重复依赖收集,采用set特性来去重
this.reactiveFns = new Set();
}
addDepend(fn) {
this.reactiveFns.add(fn)
}
depend() {
if(activeReactiveFn) {
// set对应的是add方法
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach(fn => fn());
}
}
9. 对响应式对象的操作封装
到这里还存在一个问题就是,我们如果再想要创建一个响应式对象,则还需要复制一遍监听对象的操作那部分代码,于是我们应该要把响应式对象的操作进行封装。
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 在此处自动收集依赖
const depend = getDepend(target, key);
depend.depend();
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 在此处获取各自对象和key对应的depend对象
const depend = getDepend(target, key);
depend.notify();
}
})
}
// 此时要创建响应式对象时,只需调用这个方法即可
const info = reactive({
height: 1.88
})
// 使用响应式对象的字段时会自动收集依赖
watchFn(() => {
console.log("r创建另一个响应式对象->", info.height);
})
// 当修改该字段时,自动触发响应式函数
info.height = 2.0
10.完整代码以及使用方法
// 需要定义一个全局变量来保存当前依赖需要的响应式函数
let activeReactiveFn = null;
class Depend {
constructor() {
// 重复依赖收集,采用set特性来去重
this.reactiveFns = new Set();
}
addDepend(fn) {
this.reactiveFns.add(fn)
}
depend() {
if(activeReactiveFn) {
// set对应的是add方法
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach(fn => fn());
}
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 在此处自动收集依赖
const depend = getDepend(target, key);
depend.depend();
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
// 在此处获取各自对象和key对应的depend对象
const depend = getDepend(target, key);
depend.notify();
}
})
}
// 获取依赖的方法
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if(!map) {
map = new Map();
targetMap.set(target, map)
}
// 获取depend对象
let depend = map.get(key);
if(!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
// 定义一个响应式函数
function watchFn(fn) {
activeReactiveFn = fn;
// 只有执行了fn,才能知道当前使用了哪个属性,才能保存对应的依赖。
fn();
activeReactiveFn = null;
}
const obj = {
name: 'mono',
age: 18,
}
const proxyObj = reactive(obj);
watchFn(function() {
console.log(proxyObj.name);
});
watchFn(() => {
console.log("另一个用到name字段的函数", proxyObj.name);
})
proxyObj.name = 'haha';
// 创建另一个响应式对象
const info = reactive({
height: 1.88
})
watchFn(() => {
console.log("r创建另一个响应式对象->", info.height);
})
info.height = 2.0
100行代码简单理解响应式原理。