Vue3原理深究!手写简易响应式系统
难点处理
注意点1:如何识别访问某个对象的key时,应该收集的fn是谁?
方案:在effect
函数中,在执行fn
进行收集之前,先临时存储需要被收集的fn(设置 activeEffect=fn)
,然后在track
里add
这个activeEffect
。
js
++ let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
if (fn) {
++ activeEffect = fn; //临时记录需要收集的那个fn
console.warn("[current activeEffect is] ", activeEffect);
fn();
}
}
注意点2:触发依赖细致到具体某个key?
方案:修改deps
数据结构,如:
js
deps = <weakMap> {
data: <Map> {
key: <Set> [fn1, fn2]
}
}
js
/**
* 为对象的key收集副作用(依赖)
* deps格式简化如下:
* deps = <weakMap> {
* data: <Map> {
* key: <Set> [fn1, fn2]
* }
* }
* @param {*} target
* @param {*} key
*/
function track(target, key) {
console.log("[track key] ", key);
// 需要向 deps.get(target).get(key).add(fn)
if (!activeEffect) {
return;
}
// 获取对象
let depDataMap = deps.get(target);
if (!depDataMap) {
depDataMap = new Map();
deps.set(target, depDataMap);
}
// 获取对象某个key对应的依赖项
let depDataMapKeySet = depDataMap.get(key);
if (!depDataMapKeySet) {
depDataMapKeySet = new Set();
depDataMap.set(key, depDataMapKeySet);
}
depDataMapKeySet.add(activeEffect);
}
注意点3:避免收集age依赖时,会把name的依赖也收集起来的
方案:在effect
里的fn
执行(依赖收集)后,重置activeEffect = null
;
js
let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
if (fn) {
activeEffect = fn; //临时记录需要收集的那个fn
console.warn("[current activeEffect is] ", activeEffect);
fn();
++ activeEffect = null; //避免收集age时,会把name也收集起来的bug
} else {
activeEffect = null;
}
}
注意点4:如何实现深层次响应式?
方案:在get
里判断得到的结果res
是否为object
,如果是则递归调用reactive。(return reactive(res))
;
js
let deps = new WeakMap();
/**
* 引用类型响应式
* @param {*} obj
* @returns
*/
function reactive(obj) {
return new Proxy(obj, {
// 收集依赖
get: function (target, key, receiver) {
track(target, key);
let res = Reflect.get(target, key, receiver);
++ if (typeof res == "object" && res !== null) {
++ return reactive(res); // 这里记得return
++ }
return res;
},
// 触发依赖
set: function (target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
trigger(target, key);
},
});
}
注意点5:实现对原始值的响应式
方案:通过将原始值包装成一个对象(引用值),再调用reactive变成响应式,访问时通过 xxx.value 获取值。
js
/**
* 对原始值的响应式方式
* @param {原始值} val
* return ReactiveObj
*/
function ref(val) {
let wrapper = {
value: val,
};
/**_isRef用于标识这是一个通过包装的引用类型 */
Object.defineProperty(wrapper, "_isRef", { value: true });
return reactive(wrapper);
}
完整代码
js
let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
const doEffect = () => {
activeEffect = fn; //临时记录需要收集的那个fn
fn();
activeEffect = null; //避免收集age时,会把name也收集起来的bug
};
doEffect();
}
let deps = new WeakMap();
/**
* 引用类型响应式
* @param {*} obj
* @returns
*/
function reactive(obj) {
return new Proxy(obj, {
// 收集依赖
get: function (target, key, receiver) {
track(target, key);
let res = Reflect.get(target, key, receiver);
if (typeof res == "object" && res !== null) {
console.log("res:", res);
return reactive(res);
}
return res;
},
// 触发依赖
set: function (target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
trigger(target, key);
},
});
}
/**
* 对原始值的响应式方式
* @param {*} val
* return ReactiveObj
*/
function ref(val) {
let wrapper = {
value: val,
};
/**_isRef用于标识这是一个通过包装的引用类型 */
Object.defineProperty(wrapper, "_isRef", { value: true });
return reactive(wrapper);
}
/**
* 为对象的key收集副作用(依赖)
* deps格式简化如下:
* deps = <weakMap> {
* data: <Map> {
* key: <Set> [fn1, fn2]
* }
* }
* @param {*} target
* @param {*} key
*/
function track(target, key) {
console.log("[track key] ", key);
// 需要向 deps.get(target).get(key).add(fn)
if (!activeEffect) {
return;
}
// 获取对象
let depDataMap = deps.get(target);
if (!depDataMap) {
depDataMap = new Map();
deps.set(target, depDataMap);
}
// 获取对象某个key对应的依赖项
let depDataMapKeySet = depDataMap.get(key);
if (!depDataMapKeySet) {
depDataMapKeySet = new Set();
depDataMap.set(key, depDataMapKeySet);
}
depDataMapKeySet.add(activeEffect);
console.log("deps: ", deps);
}
/**
* 触发副作用(依赖)执行
* @param {*} target
* @param {*} key
*/
function trigger(target, key) {
console.log("[trigger key] ", key);
deps
.get(target)
.get(key)
.forEach((fn) => {
fn && fn();
});
}
DEMO
js
<!-- simpleReactive.js -->
let count = ref(0);
console.log("count ref:", count);
console.log("count.value:", count.value);
console.log("count._isRef:", count._isRef);
let data = reactive({
name: "believer",
age: 29,
skills: {
run: true,
jump: true,
drive: false,
},
});
effect(() => {
document.querySelector(
"#reactive_res"
).innerText += `update [data.age] = ${data.age} \n\n`;
});
effect(() => {
document.querySelector(
"#reactive_res"
).innerText += `update [data.name] = ${data.name} \n\n`;
});
effect(() => {
document.querySelector("#name").value = data.name;
});
effect(() => {
document.querySelector(
"#reactive_res"
).innerText += `update [data.skills.drive] = ${data.skills.drive} \n\n`;
});
effect(() => {
document.querySelector("#count").innerText = "click times: " + count.value;
});
setTimeout(() => {
data.age = 10;
data.skills.drive = true;
}, 2000);
setTimeout(() => {
data.name = "WC";
}, 3000);
// 用户操作
document.querySelector("#name").addEventListener("keyup", (e) => {
data.name = e.target.value;
});
document.querySelector("#add").addEventListener("click", () => {
data.age++;
count.value++;
});
document.querySelector("#reduce").addEventListener("click", () => {
data.age--;
count.value++;
});
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh">
<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>
<h3>简易响应式实现</h3>
<div id="count"></div>
<hr>
<input type="text" id="name">
<button id="add">age++</button>
<button id="reduce">age--</button>
<hr>
<div id="reactive_res"></div>
</body>
<script src="./simpleReactive.js"></script>
</html>