说明
【vue.js 设计与实现】 霍春阳 学习笔记
概念解释
-
响应式数据
数据发生变化后,依赖改数据的数据也会随之变化。
如果data是响应式数据,那么data.count发生变化后document.body.innerHTML也会更新。let data = { name: '舞狮牛牛', price: 10, count: 1, } function effect() { console.log('执行副作用函数'); let money = proxyData.count * proxyData.price; let text = `采购信息:总金额 = ${money}`; document.body.innerHTML = text; } -
副作用函数
会产生副作用的函数,它的执行会直接或间接的影响其他函数的执行。
比如effect,执行后会影响document.body.innerHTML,会影响其他读取document.body.innerHTML的函数,我们就说它产生了副作用。
实现
Version 0.1 — 基础版
如果data是响应式数据,当副作用函数执行后,money会更新。为了达到这样的效果,我们能怎么做?
我们注意到:
- 副作用的执行会触发data的读取
- data的读取会触发get
- data的修改会触发set 我们可以通过proxy拦截到对象的get和set操作,在get时将副作用函数暂存起来,set时重新执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="main"></div>
<script type="module" src="reactive-1.js"></script>
</body>
</html>
/*reactive-1.js*/
// 存储副作用函数,set可以去重
const bucket = new Set();
const proxyData = new Proxy(data, {
get(data, key) {
console.log(`触发get,key = ${key}`);
bucket.add(effect);
return data[key];
},
set(data, key, newVal) {
console.log(`触发set,key = ${key}`);
data[key] = newVal;
bucket.forEach(effect => {
effect();
});
return true;
}
});
function effect() {
console.log('执行副作用函数');
let money = proxyData.count * proxyData.price;
let text = `采购信息:总金额 = ${money}`;
document.body.innerHTML = text;
}
window.onload = function() {
effect();
setTimeout(() => {
console.log('-- update count before --');
proxyData.count = 10;
console.log('-- update count after --');
}, 1000);
}
但此时存在以下几个问题:
-
副作用函数是收集是硬编码的,现有的effect名称变更了,代码还需要修改才能按照预期运行
-
代码的复用性低
- 副作用函数还需要手动执行(
window.onload中),每增加一个副作用函数就要写一遍/*新增*/ function showDetail(){ let detail = `数量 = ${proxyData.count}, 单价 = ${proxyData.price}`; console.log(detail); } const proxyData = new Proxy(data, { get(data, key) { console.log(`触发get,key = ${key}`); bucket.add(effect); /*新增*/ bucket.add(showDetail); return data[key]; }, ... }); window.onload = function() { effect(); /*新增*/ showDetail(); }; - 如果希望再创建一个响应式对象,还需要再次写
Proxy读取、设置,再添加一个bucket,
- 副作用函数还需要手动执行(
-
对象每个属性的变更都会引起effect的重新执行,尽管这个属性和effect没什么关系
window.onload = function() { effect(); setTimeout(() => { console.log('-- update name before --'); proxyData.name = 10; console.log('-- update name after --'); }, 1000);
Version 0.2 — 进阶版
为了解决第1、2.1个问题,我们将effect的添加改成动态的——增加一个注册函数和一个全局变量,将创建proxy对象的过程包装一下
/* 新增 */
let activeEffect = null;
/* 新增 */
/**
* 注册副作用函数
* @param {Function} effectFn
*/
function effect(effectFn) {
activeEffect = effectFn;
effectFn();
}
// 存储副作用函数,set可以去重
const bucket = new Set();
/* 新增 */
/**
* 创建响应式对象
* @param {*} obj
* @returns
*/
function reactive(obj){
return new Proxy(obj, {
get(obj, key) {
console.log(`触发get,key = ${key}`);
/* 修改 */
if(activeEffect){
bucket.add(activeEffect);
}
return obj[key];
},
set(obj, key, newVal) {
console.log(`触发set,key = ${key}`);
obj[key] = newVal;
bucket.forEach(effect => {
effect();
});
return true;
}
});
}
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 1,
});
function calculateTotalCount() {
console.log('执行副作用函数');
let money = proxyData.count * proxyData.price;
let text = `采购信息:总金额 = ${money}`;
document.body.innerHTML = text;
}
window.onload = function() {
/** 修改 */
effect(calculateTotalCount);
setTimeout(() => {
console.log('-- update count before --');
proxyData.count = 10;
console.log('-- update count after --');
}, 1000);
}
这次修改提高了代码的复用性。
再来解决第3个问题,问题产生的原因是我们将副作用函数和对象进行了关联,但实际这个副作用函数只需要和对象的某几个属性做关联。希望的关系是:
这个树形的关系,可用以下结构来表示:
为什么这里使用WeackMap而不是Map?
WeakMap的key只能是对象,是弱引用。key所指的对象可以被垃圾回收,回收后对应的key和value就访问不到了。所以WeakMap适合存储那些只有当key所引用的对象存在时(没被回收)才有价值的信息。
修改后的代码:
/* 修改 存储副作用函数 */
const bucket = new WeakMap();
/**
* 创建响应式对象
* @param {*} obj
* @returns
*/
function reactive(obj){
return new Proxy(obj, {
get(target, key) {
console.log(`触发get,key = ${key}`);
/* 修改 存入副作用函数*/
if(activeEffect){
let targetMap = bucket.get(target);
if(!targetMap){
targetMap = new Map();
bucket.set(target, targetMap);
}
let effectSet = targetMap.get(key);
if(!effectSet){
effectSet = new Set();
targetMap.set(key, effectSet);
}
effectSet.add(activeEffect);
}
return target[key];
},
set(target, key, newVal) {
console.log(`触发set,key = ${key}`);
target[key] = newVal;
/* 修改 取出并执行副作用函数 */
const targetMap = bucket.get(target);
if(!targetMap) return true;
const effectSet = targetMap.get(key);
if(!effectSet) return true;
effectSet.forEach(effectFn => {
effectFn();
});
return true;
}
});
}
get和set中副作用的代码太多了,且此功能和get、set独立,我们将它抽取出来,让代码更整洁。完整版本:
let activeEffect = null;
/**
* 注册副作用函数
* @param {Function} effectFn
*/
function effect(effectFn) {
activeEffect = effectFn;
effectFn();
}
// 存储副作用函数
const bucket = new WeakMap();
/**
* 收集target对象key的副作用函数
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function track(target, key){
if(!activeEffect) return;
let targetMap = bucket.get(target);
if(!targetMap){
targetMap = new Map();
bucket.set(target, targetMap);
}
let effectSet = targetMap.get(key);
if(!effectSet){
effectSet = new Set();
targetMap.set(key, effectSet);
}
effectSet.add(activeEffect);
}
/**
* 触发target对象key的副作用函数执行
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function trigger(target, key) {
const targetMap = bucket.get(target);
if(!targetMap) return;
const effectSet = targetMap.get(key);
if(!effectSet) return;
effectSet.forEach(effectFn => {
effectFn();
});
}
/**
* 创建响应式对象
* @param {*} obj
* @returns
*/
function reactive(obj){
return new Proxy(obj, {
get(target, key) {
console.log(`触发get,key = ${key}`);
// 存入副作用函数
track(target, key);
return target[key];
},
set(target, key, newVal) {
console.log(`触发set,key = ${key}`);
target[key] = newVal;
// 取出并执行副作用函数
trigger(target, key);
return true;
}
});
}
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 1,
});
function calculateTotalCount() {
console.log('执行副作用函数');
let money = proxyData.count * proxyData.price;
let text = `采购信息:总金额 = ${money}`;
document.body.innerHTML = text;
}
window.onload = function() {
effect(calculateTotalCount);
setTimeout(() => {
console.log('-- update count before --');
proxyData.count = 10;
console.log('-- update count after --');
}, 1000);
setTimeout(() => {
console.log('-- update name before --');
proxyData.name = '皇冠牛牛';
console.log('-- update name after --');
}, 1500);
}