1 抛出问题
首先,看如下代码:
const obj = {
text: 'hello, siri',
}
function effect() {
document.body.innerHTML = obj.text;
};
effect();
当effect函数在执行时,页面上的内容变为hello,siri,但是当重新设置obj.text = 'hello,xiaoai'的时候,页面上的内容并不会发生改变,而实际上我们所希望的是当obj.text的值发生改变的时候,对应的effect函数被自动的执行,从而页面内容发生改变
2 实现数据最基本响应式
首先我们可以先分析一下,为什么数据没有发生改变,因为obj是一个普通的对象,所以当重新设置里面的属性发生变化的时候,并不会有任何其他的反应。因此需要做的就是将obj对象变为响应式的数据。
首先,可以发现:
在effect函数被执行的时候,触发了obj.text这个字段的读取操作。
在重新设置obj.text = 'hello,xiaoai'的时候,触发了obj.text这个字段的设置操作。
那么假设我们可以去拦截obj这个对象的读取操作和设置操作时,当重新去设置obj这个对象里面的某个字段时,进行一个拦截,在这里触发effect函数的执行操作,实现对象的最基本的响应式。
为了可以针对性的针对某个字段实现一些特定的拦截操作,则在针对这个字段在读取的时候,去讲这些特定操作给存储起来,这样在触发设置操作的时候,则可以将这些特定的操作给拿出来进行一个执行。
如下图所示:
在ES2015后,可以通过代理对象Proxy进行一个实现,实现对对象属性的读取和设置操作的拦截,代码如下所示:
// 创建一个桶 用于存储特定的某些函数
const bucket = new Set();
const data = {
text: 'hello, siri',
}
const obj = new Proxy(data, {
get(target, key) {
// 加入桶中
bucket.add(effect);
return target[key];
},
set(target, key, newVal) {
// 如果更新的值和原来的值相同 则不做任何操作
if(newVal === target[key]) {
return ;
}
// 不同 则重新进行赋值
target[key] = newVal;
// 同时将桶中存储的函数拿出来,进行执行。
bucket.forEach(fn =>fn());
return true;
}
});
function effect() {
document.body.innerText = obj.text;
};
effect();
setTimeout(() => {
obj.text = 'hello, xiaoai';
}, 1000);
代码解释如下:
首先创建了一个用于存储特定函数的桶bucket,定义了一个对象data,并给其设置了其代理对象,并分别设置get和set的拦截函数,当读取属性值的时候,则将特定函数给存储到桶中,返回属性值,重新设置属性值的时,进行更新的操作,并将桶中的函数逐一拿出来进行执行,实现了对象的响应式。
在执行代码时,首先执行了effect函数,在函数中进行了数据的读取操作: document.body.innerText = obj.text,从而触发get函数的执行,将effect函数给存储到bucket桶中。设置定时器,在定时器中重新设置了obj.text的值: obj.text = 'hello, xiaoai',则触发了set函数,更新属性值,并重新执行effect函数。
效果展示如下链接:
stackblitz.com/edit/js-whu…
3 明确特定属性和特定函数之间的联系
修复-特定属性值和特定函数之间的明确联系
但是在这里会存在一个问题,如下代码:
const effect = () => {
document.body.innerText = obj.text;
console.log('1');
};
setTimeout(() => {
obj.newText = 'say hi';
}, 1000);
没有针对特定的属性值进行一个拦截操作,也就是说设置obj的newText属性的时候,特定的函数也会被执行,导致effect函数被执行两次,输出两个1。
而实际上,在这里不应该执行effect函数,该函数和obj.text关联,不与其他属性关联。当重新设置一个新的属性值时,effect函数不应该被执行,
本质上: 没有将特定的字段和特定的函数之间建立一个强联系。
解决方法:重新设置桶的结构,将特定字段和特定函数之间建立一个明确的联系。
// 创建一个桶 用于存储特定的某些函数--修改代码
const bucket = new WeakMap();
const data = {
text: 'hello, siri',
}
// 修改代码
const obj = new Proxy(data, {
get(target, key) {
if(effect) {
let depsMap = bucket.get(target);
if(!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 取出所有和特定属性值相关的特定函数
let deps = depsMap.get(key);
if(!deps) {
depsMap.set(key, deps = new Set());
}
deps.add(effect);
}
return target[key];
},
set(target, key, newVal) {
// 如果更新的值和原来的值相同 则不做任何操作
if(newVal === target[key]) {
return ;
}
// 不同 则重新进行赋值
target[key] = newVal;
// 同时将桶中存储的函数拿出来,进行执行。
const depsMap = bucket.get(target);
if(!depsMap) {
return;
}
const deps = depsMap.get(key);
deps.forEach(fn => fn());
return true;
}
});
function effect() {
document.body.innerText = obj.text;
};
effect();
setTimeout(() => {
obj.text = 'hello, xiaoai';
}, 1000);
如下图示意图所示:
bucket中,由target---> Map构成,其中target是对象,Map是一个Map实例,Map中由key-->Set构成,key表示的是对象中的属性值,Set中包含了一些特定的函数。
4 实现深层次对象的响应式
在上面代码中,是针对对象中一层的属性进行数据的监听,如果对于深层次的对象,无法进行监听,如下代码所示:
// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
const data = {
text: 'hello siri',
a: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const data = {
a: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const effect = () => {
document.body.innerText = obj.b.c;
console.log('1');
};
effect();
setTimeout(() => {
obj.b.c = 3;
obj.fn = () => {};
}, 1000);
这时候当修改obj.b.c的值的时候,页面上数据不会发生改变。
解决方法: 递归嵌套属性
// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则直接返回
return target;
}
// 因为Proxy原生支持数组,所以这里不需要自己实现
// if (Array.isArray(target)) {
// target.__proto__ = newPrototype
// }
// 传给Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key);
if (effect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(effect);
}
// 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
return bindReactive(reflect);
},
set(target, key, val) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const success = Reflect.set(target, key, val);
update();
// 设置成功与否
const depsMap = bucket.get(target);
if (!depsMap) {
return;
}
const deps = depsMap.get(key);
deps && deps.forEach((fn) => fn());
return success;
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
// 删除成功与否
update();
// 不同 则重新进行赋值
return success;
},
};
// 生成proxy对象
const proxy = new Proxy(target, handler);
return proxy;
}
function update() {
console.log('999');
}
const effect = () => {
document.body.innerText = obj.b.c;
console.log('1');
};
const data = {
text: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const obj = bindReactive(data);
effect();
setTimeout(() => {
obj.b.c = 3;
}, 1000);
5 优化
5.1 代码优化
5.1.1 函数的优化:
在上面代码中,effect函数是一个具体名字的函数,可以进行一个优化,将该函数转化为一个无论是任何名字或是匿名函数,都可以被收集到桶中。
// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则直接返回
return target;
}
// 因为Proxy原生支持数组,所以这里不需要自己实现
// if (Array.isArray(target)) {
// target.__proto__ = newPrototype
// }
// 传给Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key);
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
// 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
return bindReactive(reflect);
},
set(target, key, val) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const success = Reflect.set(target, key, val);
update();
// 设置成功与否
const depsMap = bucket.get(target);
if (!depsMap) {
return;
}
const deps = depsMap.get(key);
deps && deps.forEach((fn) => fn());
return success;
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
// 删除成功与否
update();
// 不同 则重新进行赋值
return success;
},
};
// 生成proxy对象
const proxy = new Proxy(target, handler);
return proxy;
}
function update() {
console.log('999');
}
const data = {
a: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const obj = bindReactive(data);
setTimeout(() => {
obj.b.c = 3;
}, 1000);
// ---修改
let activeEffect;
function effect(fn) {
// 进行赋值
activeEffect = fn;
// 执行函数
fn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
document.body.innerText = obj.b.c;
console.log('1');
});
setTimeout(() => {
obj.b.c = 3;
}, 1000);
5.1.2 逻辑抽取
为了可以使得代码逻辑清晰,将特定函数给收集到桶中该逻辑从get函数中给抽取出来, 抽取为: collectFn函数 ,将重新设置属性值触发特定函数的执行逻辑从set函数中抽离出来,抽取为: trigger函数。
// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则直接返回
return target;
}
// 因为Proxy原生支持数组,所以这里不需要自己实现
// if (Array.isArray(target)) {
// target.__proto__ = newPrototype
// }
// 传给Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key);
collectFn(target, key);
// 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
return bindReactive(reflect);
},
set(target, key, val) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const success = Reflect.set(target, key, val);
update();
// 设置成功与否
trigger(target, key);
return success;
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
// 删除成功与否
update();
// 不同 则重新进行赋值
return success;
},
};
// 生成proxy对象
const proxy = new Proxy(target, handler);
return proxy;
}
// 抽取逻辑--修改
function collectFn(target, key) {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
}
// 抽取逻辑--修改
function trigger(target, key) {
// 同时将桶中存储的函数拿出来,进行执行。
const depsMap = bucket.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach((fn) => fn());
}
}
function update() {
console.log('999');
}
const data = {
text: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const obj = bindReactive(data);
setTimeout(() => {
obj.b.c = 3;
}, 1000);
let activeEffect;
function effect(fn) {
console.log(typeof fn);
// 进行赋值
activeEffect = fn;
// 执行函数
fn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
document.body.innerText = obj.b.c;
console.log('1');
});
setTimeout(() => {
obj.b.c = 3;
}, 1000);
5.2 性能优化
5.2.1 修复-去除不必要的更新
如下案例所示:
const data = {
isTrue: true,
text: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
effect(() => {
document.body.innerHTML = obj.isTrue ? obj.text : 'hello, zoom';
console.log(1);
})
setTimeout(() => {
obj.isTrue = false;
}, 1000);
setTimeout(() => {
obj.text = 'hello xiaoai';
}, 2000);
分析:
当第一次执行effect函数的时候,该函数会分别被isTrue、text字段都给收集,如下图所示:
当第一个定时器一秒后执行,将obj.isTrue 设置为 false,则会触发effect函数的执行。
当第二个定时器两秒后执行,将obj.text 设置为'hello xiaoai',也会触发effect函数执行,而这时因为obj.isTrue的值是false,所以实际上页面不会有任何变化,也就是说当obj.isTrue设置为false的时候,这时候obj.text的值的变化就不应该去触发副作用函数的执行,导致不必要的消耗。
解决思路:
每次effect函数执行的时候,由于它的执行一定会触发特定属性的一个读取操作,因此我们可以在该函数执行前进行一个清空操作,也就是先将其和所有有关联的属性集合中进行删除操作,当执行的时候由于进行读取操作,因此会重新收集。
因此我们必须要明确有哪些属性对应的集合中收集了特定的函数,代码如下:
// 抽取逻辑--修改
function collectFn(target, key) {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
// 修改 --- 将deps给存储到 activeEffect.deps数组中 这样就可以知道哪些属性和函数关联
activeEffect.deps.push(deps);
}
}
// 修改 重新定义 effect函数
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn();
};
effectFn.deps = [];
effectFn();
}
则通过这个数组,可以了解所有与这个特定函数相关的集合,在执行特定函数时可获取相关的所有关联的属性集合,则可以进行一个清除,代码如下:
function effect(fn) {
const effectFn = () => {
cleanUp(effectFn);
activeEffect = effectFn;
fn();
};
effectFn.deps = [];
effectFn();
}
function cleanUp(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const dep = effectFn.deps[i];
dep.delete(effectFn);
}
effectFn.deps.length = 0;
}
总代码
// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则直接返回
return target;
}
// 因为Proxy原生支持数组,所以这里不需要自己实现
// if (Array.isArray(target)) {
// target.__proto__ = newPrototype
// }
// 传给Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key);
collectFn(target, key);
// 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
return bindReactive(reflect);
},
set(target, key, val) {
// 重复的数据,不处理
if (val === target[key]) {
return true;
}
const success = Reflect.set(target, key, val);
update();
// 设置成功与否
trigger(target, key);
return success;
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
// 删除成功与否
update();
// 不同 则重新进行赋值
return success;
},
};
// 生成proxy对象
const proxy = new Proxy(target, handler);
return proxy;
}
// 抽取逻辑--修改
function collectFn(target, key) {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
// 将deps给存储到 activeEffect.deps数组中 这样就可以知道哪些数据和当前的函数关联
activeEffect.deps.push(deps);
}
}
// 抽取逻辑--修改
function trigger(target, key) {
// 同时将桶中存储的函数拿出来,进行执行。
const depsMap = bucket.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
const effectsFn = new Set(deps);
if (effectsFn) {
effectsFn.forEach((fn) => fn());
}
}
function update() {
console.log('999');
}
function cleanUp(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const dep = effectFn.deps[i];
dep.delete(effectFn);
}
effectFn.deps.length = 0;
}
const data = {
isTrue: true,
a: 'hello, siri',
b: {
c: 1,
d: {
e: '12',
},
},
};
const obj = bindReactive(data);
setTimeout(() => {
obj.b.c = 3;
}, 1000);
let activeEffect;
function effect(fn) {
const effectFn = () => {
cleanUp(effectFn);
activeEffect = effectFn;
fn();
};
effectFn.deps = [];
effectFn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
document.body.innerHTML = obj.isTrue ? obj.text : 'hello, zoom';
console.log('1');
});
setTimeout(() => {
obj.isTrue = false;
}, 1000);
setTimeout(() => {
obj.text = 'hello xiaoai';
}, 2000);