这是我们读《vuejs设计与实现》第四章响应式系统的作用与实现,原书第二章、第三章简单的给我们说了一些简单的原理,我们只需要了解即可(感兴趣的话也可以跟着书中例子写一写),从第四章开始到第六章就会进入响应式设计模块,这个内容我们有必要记录一下,并且跟着实现相应的代码,方便我们后续深入的学习。
1、副作用函数
副作用函数:执行函数会直接或间接对其他函数的执行或修改别的变量,那我们称这是个副作用函数;下面两个例子是典型的副作用函数:
function effect() {
document.getElementById('#app').innerHTML = 'hello world'
}
// 或者
var a = 1;
function change() {
a = 2
}
2、响应式数据概念
响应式数据:当某个变量的值发生了变化,相关的副作用函数自动执行,我们就说这个变量属于响应式数据;如下面的例子:
const obj = { text: 'hellow world' }
function effect() {
document.body.innerText = obj.text
}
// 如果obj.text值变化,能触发effect执行,那我们就认为obj是响应式数据
3、响应式系统的设计
针对与响应式数据,一般业内有两套方案,第一种自然是老版本的方式,通过Object.defineProperty属性拦截的方式来去实现(很经典的方式,当然也是vue面试老生常谈的八股文),第二种方式就是es6之后的Proxy代理的模式实现,针对上述案例,我们进行实现办法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
const bucket = new Set();
const data = { text : 'hello world' };
const obj = new Proxy(data, {
get(target, key) {
bucket.add(effect);
return target[key];
},
set(target, key, value) {
target[key] = value;
bucket.forEach(effect => effect());
return true;
}
});
function effect() {
console.log('此处被执行');
document.getElementById('app').innerHTML = obj.text;
}
effect();
setTimeout(() => {
obj.other = '我添加新属性'
}, 1000)
setTimeout(() => {
obj.text = '你变化啊~'
}, 2000);
</script>
</body>
</html>
对于上面的做的响应式数据系统,我们可以明显的发现,假如我们增加了一个other属性,也会触发proxy代理里面的副作用函数,事实上我们想要响应式系统是如下结构的:
同时我们设计一下存储副作用函数的bucket(关于数据类型 Map,WeakMap,Set,后续我会出相关介绍的文章,此处暂且不表)
就是对应的key值触发相应的副作用函数,而不是一股脑全部执行,下面是关于改进后的响应式数据设计:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
// 副作用函数
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn();
}
// 数据准备
const data = { text: 'hello world' }
// 响应式函数容器
const bucket = new WeakMap();
const obj = new Proxy(data, {
get(target, key) {
tack(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
}
});
// 向bucket里面注入副作用函数
function tack(target, key) {
// 没有acticeEffect
if (!activeEffect) {
return target[key];
}
// 判断下面有没有对应的对象相关的内容
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
// 对应key值的内容
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
// 副作用触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) {
return;
}
let deps = depsMap.get(key);
deps && deps.forEach(fn => fn());
}
effect(() => {
console.log('执行副作用函数 text')
document.querySelector('#app').innerHTML = obj.text;
});
effect(() => {
// 在获取obj.info时候,就会将副作用函数放入到对应的bucket中
console.log('执行副作用函数 info', obj.info);
});
setTimeout(() => {
obj.info = '开始变化';
}, 2000);
</script>
</body>
</html>
如果上述代码我们调整一下下面的内容:
const data = { text: 'hello world', isShow: true }
// 响应式数据设计这个一招上面例子所示即可
const obj = new Proxy(data , .....)
// 调整一下effect副作用函数
effect(() => {
console.log('执行副作用函数')
document.querySelector('#app').innerHTML = obj.isShow ? obj.text : 'not show text'
})
setTimeout(() => {
obj.isShow = false
}, 1000)
setTimeout(() => {
obj.text = 'can i show?'
}, 2000)
最后一个定时器,我们调整text值,我们发现他依然会触发副作用函数,事实上,这个时候obj.isShow是false,页面上面根本不会用到obj.text值,这是一次无效的副作用函数执行,具体原因就是,我们在创建副作用函数时候,在bucket里面在 text, isShow中放入了相同的副作用函数,所以我们对于响应式数据这个改造的方向就是能让他更智能一点,当我们用不到obj.text时候,修改obj.text值,去除无效的副作用函数;下面就是我们的第三个版本改造,具体如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
// 副作用函数
let activeEffect;
function effect(fn) {
const effectFn = () => {
clearEffectDeps(effectFn);
activeEffect = effectFn;
fn()
}
effectFn.deps = []
effectFn()
}
// 每次清除副作用函数列表里面的关联关系
function clearEffectDeps(effectFn) {
effectFn.deps.forEach(i => {
i.delete(effectFn)
})
effectFn.deps.length = 0
}
// 数据准备
const data = { text: 'hello world', ok: true };
// 响应式函数容器
const bucket = new WeakMap();
const obj = new Proxy(data, {
get(target, key) {
tack(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
}
});
// 向bucket里面注入副作用函数
function tack(target, key) {
// 没有acticeEffect
if (!activeEffect) {
return;
}
// 判断下面有没有对应的对象相关的内容
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
// 对应key值的内容
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
// 将对应key值的副作用函数相关信息放入副作用函数
activeEffect.deps.push(deps);
console.log(bucket)
}
// 副作用触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) {
return;
}
let deps = depsMap.get(key);
const effectsRun = new Set(deps);
effectsRun.forEach(fn => fn());
}
effect(() => {
console.log('触发副作用函数');
document.querySelector('#app').innerHTML = obj.ok ? obj.text : 'not';
});
setTimeout(() => {
obj.ok = false
}, 1000);
setTimeout(() => {
obj.text = 'hello world2'
}, 2000);
</script>
</body>
</html>
如此就能达到一个基础的响应式数据雏形