Vue.js
《Vue.js设计与实现》(霍春阳)
- 适合:从零手写Vue3响应式系统,大厂面试源码题直接覆盖。
- 重点章节:第4章(响应式)、第5章(渲染器)、第8章(编译器)
因为我第一周的任务就是响应式原理(Proxy
vs defineProperty
),手写简易reactive
,所以我是从第四章开始学习的。
4.1 响应式数据与副作用函数
1、副作用函数
函数的执行会直接或间接影响其他函数的执行
比如:
01 function effect() {
02 document.body.innerText = 'hello vue3'
03 }
它的执行会使整个body的值都为hello vue3
2、响应式数据
01 const obj = { text: 'hello world' }
02 function effect() {
03 // effect 函数的执行会读取 obj.text
04 document.body.innerText = obj.text
05 }
如上面的这一段代码,我们希望当text发生变化的时候,effect会自动执行,这就是所谓的响应式数据
下面我们将会讲到这是怎么实现的:
● 当副作用函数 effect 执行时,会触发字段obj.text 的读取操作;
● 当修改 obj.text 的值时,会触发字段 obj.text的设置操作。
实现一个响应式的数据:拦截读和写两个步骤,
读的时候把effect函数放在一个容器里面
修改的时候就把这个effect函数释放出来
拦截一个对象的读取和设置操作:Proxy
const data = { text: 'hello' }; // 定义数据对象
const bucket = new Set(); // 设置一个容器用于存储副作用函数
const obj = new Proxy(data, {
// 读的时候拦截
get(target, key) {
bucket.add(effect); // 将当前活跃的副作用函数添加到容器
return target[key]; // 返回属性值
},
// 拦截设置操作
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach(fn => fn()); // 执行所有存储的副作用函数
return true;
}
});
const effect = () => {
document.body.innerText = obj.text; // 副作用函数依赖于响应式数据
};
effect();
setTimeout(() => {
obj.text = "world"; // 修改数据,触发响应式更新
}, 1000); // 延迟1秒修改数据
继续强化->我们现在硬编码了effect,但是如果副作用函数的名字不叫effect的话,这段代码就无法继续工作了
所以我们要提供一个用来注册副作用函数的机制
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
function effect (fn) {
activeEffect = fn ;
fn();
}
const bucket = new Set(); //设置一个容器
const data = { text: 'world' }; // 初始化数据对象
const obj = new Proxy(data , {
//读的时候拦截放在容器里面
get( tartget, key ){
if(activeEffect){
bucket.add(activeEffect);
}
return tartget[key];//返回属性值
},
set(target , key , newVal){
// 拦截设置操作
target[key] = newVal;
bucket.forEach(fn => fn());
return true;
}
})
effect(() => {
console.log('run');
document.body.innerText = obj.text;
})
setTimeout(() => {
obj.text = "hello"
} , 1000)
当我们为obj添加新的属性的时候
setTimeout(() => {
obj.notExist = "hello vue3"
} , 1000)
匿名副作用函数内没有读取这个新的属性的值,那么在1s之后不会起到写操作(即放出桶里面的所有函数),但是我尝试了一下,发现它执行了的。
我们为了解决这个问题,就要重新设计“桶”这个数据结构:让它无论读取的哪一个属性都会将副作用函数收到桶里面,设置属性的时候,无论设置的是哪一个属性,也都会将副作用函数取出并执行。
let activeEffect;
function effect (fn) {
activeEffect = fn;
fn();
}
const bucket = new WeakMap();
const data = { text: 'world' }; // 确保所有属性都已定义
const obj = new Proxy(data, {
get(target, key){
if(!activeEffect){
return target[key];
}
// 根据tartget取来的depsMap,它是一个map类型
let depsMap = bucket.get(target);
// 如果不存在
if(!depsMap){
// 创建一个
bucket.set(target, (depsMap = new Map()));
}
// 根据key取来的deps,它是一个set类型
let deps = depsMap.get(key);
// 如果不存在
if(!deps){
// 创建一个
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect); // 添加当前活跃的副作用函数
return target[key];
},
set(target, key, newVal){
target[key] = newVal;
const depsMap = bucket.get(target);
if(!depsMap){
return;
}
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
}
});
effect(() => {
console.log('run');
document.body.innerText = obj.text;
});
setTimeout(() => {
obj.text = "hello vue3"; // 修改已定义的属性以触发依赖
}, 1000);
大家可以发现,我们引用了weakMap(它与map最大的不同就是它对key是弱引用,不影响垃圾回收器的工作,通常存储只有当key所引用的对象存在时,才有价值的信息);
我们要解决的是属性不存在时候的问题,那么
- 在读的时候判断是否有这个属性,没有就创建一个,有的话就把函数放在桶里面。
- 在修改的时候也是要判断
最后,我们将一些函数进行封装:
<script setup>
let activeEffect;
function effect (fn) {
activeEffect = fn;
fn();
}
const bucket = new WeakMap();
const data = { text: 'world' }; // 确保所有属性都已定义
const obj = new Proxy(data, {
get(target, key){
track(target , key);
return target[key];
},
set(target, key, newVal){
target[key] = newVal;
trigger(target , key , newVal);
}
});
// 追踪变化
function track(target , key){
if(!activeEffect){
return target[key];
}
// 根据tartget取来的depsMap,它是一个map类型
let depsMap = bucket.get(target);
// 如果不存在
if(!depsMap){
// 创建一个
bucket.set(target, (depsMap = new Map()));
}
// 根据key取来的deps,它是一个set类型
let deps = depsMap.get(key);
// 如果不存在
if(!deps){
// 创建一个
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect); // 添加当前活跃的副作用函数
}
// 触发变化
function trigger(target , key , newVal){
const depsMap = bucket.get(target);
if(!depsMap){
return;
}
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
}
effect(() => {
console.log('run');
document.body.innerText = obj.text;
});
setTimeout(() => {
obj.text = "hello vue3"; // 修改已定义的属性以触发依赖
}, 1000);
</script>