前言
vue
做为前端三大框架之一,在国内受欢迎程度远高于其他框架,特别是 vue3
版本的发布,把 vue
的发展又推上了一个台阶。下面我们就以《Vue的设计与实现》为教材来窥探 vue
的设计原理与实现细节吧。
不是大佬,文章如有错误欢迎下方留言,共同学习。
案例代码:github
副作用
最早是在 react hook
中听说过这个概念。其实也很好理解,映射在现实社会就是说 你做一件事影响到了别人。
那对于一个函数来说,只要它做的事和函数外部有交集(获取或者修改了外部的变量),那它就产生了副作用也就是变成了副作用函数。
var a = 1;
function fn(){
a = 2
}
所以如果按照这种方式给函数分类的话可以把函数分为 纯函数 和 副作用函数。
那副作用和我们进行要学习的响应式有什么关系呢?
响应式原型
思考一下,如果用白话来描述响应式你会怎么描述呢?我想大概是这样的:我修改了一个变量,页面自己重新渲染了。
我们也可以理解为,我修改了一个变量同时执行的它的渲染函数所以页面又重新渲染了,这个时候执行的渲染函数也就是我们说的 副作用函数,他和这个变量有着强依赖的关系。
let name = '掘金'
function render(){
document.body.innerHtml = name
}
// 改变变量
name = '你好,掘金';
// 执行渲染函数
render();
上面的代码不够自动,每次变量改变还得自己手动的去执行渲染函数,那有没有办法把这一过程变得自动化些呢,那就要提到 JavaScript 标准内置对象 Proxy
,而 vue3 响应式实现也是依赖这个 Proxy。下面我们使用 Proxy
来改造我们的代码。
let data = {
name: '掘金',
};
const obj = new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, val) {
target[key] = val;
render();
},
});
function render() {
document.body.innerHTML = obj.name;
}
// 首次渲染
render();
// 两秒后改变nam
setTimeout(() => {
obj.name = '你好,掘金';
}, 2000);
效果如下所示:
看上去我们实现了个简单的响应式原型,但我们需要思考,Vue 的副作用只能执行 render
函数渲染页面吗,显然不是。当一个函数内部依赖了响应式变量的时候,如果响应式变量发生了变化,那这个函数也需要重新执行。
所以上面对响应式的总结显然不完整,应该是:我修改了一个变量,依赖这个变量的函数又重新执行了 这个函数可能是渲染函数,也可能是其他函数。
所以我们的代码应该是这样的
// 定义一个副作用函数,如果obj.name发生变化,这个函数就会自动执行
effect(() => {
document.body.innerHTML = obj.name;
});
setTimeout(() => {
obj.name = '你好,掘金';
}, 2000);
从使用上来看 effect
入参接收的是一个函数 fn
,所以 effect 是个高阶函数。当 obj
触发 set
时需要执行这个 fn
,所以 fn 函数需要暴露出来以便使用。
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn();
}
当然 effect 副作用函数不止一个,所以我们需要有个数组去存放所有的副作用函数,先用 Set
来存放好了。
//存放所有的副作用
let effects = new Set();
现在我们需要思考一个问题,那就是什么时候需要收集这些副作用?什么时候要去执行这些副作用?当然是触发get
的时候去收集,触发set
的时候去执行,所以完整代码如下所示:
let activeEffect;
let effects = new Set();
let data = {
name: '掘金',
};
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
// 收集副作用
effects.add(activeEffect);
}
return target[key];
},
set(target, key, val) {
target[key] = val;
//执行所有的副作用
effects.forEach((effect) => effect());
},
});
function effect(fn) {
activeEffect = fn;
fn();
}
// 当 obj.name 改变的时候,希望 effect可以再次被执行
effect(() => {
document.body.innerHTML = obj.name;
});
// 可以存在多个effect
effect(() => {
console.log(obj.name);
});
setTimeout(() => {
obj.name = '你好,掘金';
}, 2000);
这种依赖收集和触发的模式也是我们常说的发布订阅模式
。
让响应式更加精准
我们来执行这段代码
let activeEffect;
let effects = new Set();
let data = {
name: '掘金',
age:10,
};
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
effects.add(activeEffect);
}
return target[key];
},
set(target, key, val) {
target[key] = val;
effects.forEach((effect) => effect());
},
});
function effect(fn) {
activeEffect = fn;
fn();
}
// 当 obj.name 改变的时候,希望 effect可以再次被执行
effect(() => {
document.body.innerHTML = obj.name;
console.log(obj.name);
});
setTimeout(() => {
// 注意改变的是age而不是name
obj.age = 18;
}, 2000);
我们发现虽然 effect
中没有依赖 obj.age
,但当我们改变 obj.age
时,effect
还是会重新执行。
这是因为我们的响应式系统的依赖收集和触发的颗粒度不够,我们现在的解决方案是只要 obj
里面的值发生变化都会触发副作用的更新,这显然是不对的。
所以收集依赖时,必须精确到 obj
的key,大致的数据结构设计如下所示:
在 vue3
中,使用的是 WeakMap
来描述这一关系
代码如下,关键地方已给出注释。
let activeEffect;
let effects = new WeakMap(); // 存放所有的对象及副作用的关系
let data = {
name: '掘金',
};
const obj = new Proxy(data, {
get(target, key) {
// 判断,有没有 target的关系树
let depsMap = effects.get(target);
//如果没有就创建,以当前 obj 为 key
if (!depsMap) {
effects.set(target, (depsMap = new Map()));
}
// 看 obj.xxx 具体的key有没有创建依赖关系
let deps = depsMap.get(key);
// 如果没有就创建
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 如果有依赖 就添加到对应的 key上
if (activeEffect) {
deps.add(activeEffect);
}
return target[key];
},
set(target, key, val) {
target[key] = val;
// 从WeakMap中取出对应的依赖关系
const depsMap = effects.get(target);
if (depsMap) {
// 取出obj对应的key
const effect = depsMap.get(key);
//如果有副作用函数就执行所有的副作用函数
effect && effect.forEach((fn) => fn());
}
},
});
function effect(fn) {
activeEffect = fn;
fn();
}
effect(() => {
document.body.innerHTML = obj.name;
console.log(obj.name);
});
setTimeout(() => {
obj.age = 18;
}, 2000);
对应的数据结构如下:
Vue3
响应式设计的巧妙之处就在于此,通过这样一种数据结构就把整个响应式的依赖收集以及对应关系描述的清清楚楚。当然我们今天实现的内容是 vue3
响应式的核心,但不是全部,一个完整的响应式系统会非常复杂,需要考虑到的情况也非常的多,但最终都会基于以上这种数据结构去缝缝补补。
最后
- 副作用嵌套了会不会进入死循环?
- 如果副作用函数中存在判断逻辑要怎么处理?
Vue3
的watch
函数又是如何实现的?- …
接下来我们一起探索!
如有帮助,请点赞关注😘