vue的响应式实现过程:
- 通过proxy返回代理后的对象,在proxy中有get set可以拦截对数据的获取和修改
- 当在get中拦截,代表要执行与数据相关的操作。使用effect函数将副作用函数收集到一个桶里
- 当在set中拦截,代表要重新执行与数据对应的副作用函数(从桶中取)
通过effect函数又可以实现调度器,计算属性,watch
proxy的代理又会介绍
- 如何代理对象
- 如何代理数组
- 如何代理set map
- 如何代理简单数据类型(ref toRef toRefs) 举个简单的例子,你在页面上渲染了一个值,过了一秒修改这个值,你希望他做什么呢,也就是执行副作用 render将页面重新渲染一遍。你第一次进入页面,就是effect函数给你注册副作用函数的时机。这个时候副作用函数会自动执行一遍,这就是为什么第一次进页面没有做什么,只是在注册副作用函数,就能渲染出来的原因
proxy effect bucket
先解释一下这几个东西,proxy是用来监听对象的get set 操作,effect是注册副作用函数的函数,bucket是存放副作用的容器,即在副作用函数与被操作的目标字段之间建立明确的联系。这个副作用函数是用来做什么的?举个例子 :在页面上我渲染了一个值,之后我定时一秒后修改他的值,所以页面应该更新是吧。更新应该执行副作用 render(newVode,oldVode,container)即重新渲染页面。
这是一个比较基础的实现
const data = {
name: "July",
age: "22",
};
//开始代理
const proxyData = new Proxy(data, {
get(target, key) {
//收集依赖
if (!activeEffect) return target[key];
let deps = bucket.get(target);
if (!deps) bucket.set(target, (deps = new Map()));
let depsSet = deps.get(key);
if (!depsSet) deps.set(key, (depsSet = new Set()));
depsSet.add(activeEffect);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
//触发副作用
let effects = bucket?.get(target)?.get(key);
effects &&
effects.forEach((fn) => {
fn();
});
},
});
let activeEffect; //全局变量-存储被注册的副作用函数
let bucket = new WeakMap(); //副作用容器
function effect(fn) {
//注册副作用函数的函数
activeEffect = fn;
fn(); //执行了才会被收集
}
effect(() => {
console.log(proxyData.age); //修改的是代理对象的值,不是原始值
});
proxyData.age = 18;
去除不必要的副作用函数
在上面我们只能添加上对应的副作用函数,但是如果数据改变,可能之前的副作用函数就应该舍弃,例如:name修改后age对应的副作用函数集合就不应该包含这个副作用
effect(() => {
console.log(proxyData.name==="July"?proxyData.age:'not');
});
proxyData.name='Anna';
proxyData.age=100;//这一次不应该触发上面的副作用
思路:当触发副作用函数前,应该断开他在桶中所有对应的连接。因为可以消除不需要建立的连接,触发后又会重新建立连接,这个新建立的连接才是正确的。实现方法:给副作用添加一个属性deps,他是一个数组,数据项是一个个set集合(包含当前副作用函数的依赖集合)。
//修改effect函数
function effect(fn) {
//把fn在封装一层
const effectFn = () => {
activeEffect = effectFn;//在fn执行之前执行
fn()
}
effectFn.deps=[]//添加要记录的属性
effectFn();
}
//修改get
get(target, key) {
if (!activeEffect) return target[key];
let deps = bucket.get(target);
if (!deps) bucket.set(target, (deps = new Map()));
let depsSet = deps.get(key);
if (!depsSet) deps.set(key, (depsSet = new Set()));
activeEffect.deps.push(depsSet);//新增,将依赖存进去
depsSet.add(activeEffect);
return target[key];
},
//修改set
set(target, key, newVal) {
target[key] = newVal;
let effects = bucket?.get(target)?.get(key);
const effectsToRun = new Set(effects);//不这么写会一直死循环
effects &&
effectsToRun.forEach((effectFn) => {
let deps = effectFn.deps;
deps.forEach((item) => {
item.delete(effectFn);//新增,去除所有依赖
});
effectFn.deps = [];//新增,重置
effectFn();
});
},
嵌套effect
必须实现嵌套的副作用,比如组件里嵌套了一个子组件,在下面这种情况下,修改name,却会触发子副作用函数。因为执行到子副作用函数activeEffect的值改变了,执行完子副作用函数,activeEffect的值变不回去父函数,于是跟name绑定的是子函数
effect(() => {
effect(() => { console.log(proxyData.age) })
console.log(proxyData.name)
});
proxyData.name = "Anna";//打印年龄
解决方法:在effect函数将activeEffect入栈。收集依赖时不直接add activeEffect,而是add新加栈的栈顶,add后出栈。
// effect 栈
const effectStack = [] // 新增
get(target, key) {
if (!activeEffect) return target[key];
let deps = bucket.get(target);
if (!deps) bucket.set(target, (deps = new Map()));
let depsSet = deps.get(key);
if (!depsSet) deps.set(key, (depsSet = new Set()));
activeEffect.deps.push(depsSet);
if (effectStack.length !== 0) {//新增
depsSet.add(effectStack[effectStack.length - 1]);
effectStack.pop(activeEffect);
}
return target[key];
},
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
effectStack.push(activeEffect);//新增
fn();
};
effectFn.deps = [];
effectFn();
}
副作用函数内同时触发get set
在下面这种情况下 Maximum call stack size exceeded
首先读取 obj.foo 的值,这会触发 track 操作,将当前副 作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会 触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是 该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执 行。这样会导致无限递归地调用自己,于是就产生了栈溢出。
effect(() => {
console.log(proxyData.age++);
});
解决办法很简单
set(target, key, newVal) {
target[key] = newVal;
let effects = bucket?.get(target)?.get(key);
const effectsToRun = new Set(effects);
effects &&
effectsToRun.forEach((effectFn) => {
let deps = effectFn.deps;
deps.forEach((item) => {
item.delete(effectFn);
});
effectFn.deps.length = 0;
activeEffect!==effectFn&&effectFn(); //新增
});
},
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
effectStack.push(activeEffect);//新增
fn();
activeEffect=''//新增,否则触发与activeEffect相同的副作用会直接跳过
//例如
//最后执行proxyData.age++;就不会触发副作用了
};
effectFn.deps = [];
effectFn();
}