Vue的响应式原理
文章基于Vue3,内容源于自己的学习Vue3源码的笔记与总结。
Vue是一个MVVM框架。在Vue中,当数据发生变化的时候,会去驱动视图的更新。总结得出一个公式: UI = render(data) 。 其中 UI 就是就是我们需要的输出,data数据就是我们需要开发的数据与业务模型。render()就是Vue的作用。
1、全损版数据响应式
基于逻辑进行分析: 当数据发生变化的时候,会去驱动视图的更新。
- 首先需要有一个数据data
- 其次有一个effect去处理渲染的问题
- 最后当数据data发生修改的时候,去触发effect然后对函数进行渲染。
// data数据
let data = {
text: "effect"
}
// effect函数,渲染的数据来自于data
function effect() {
document.body.innerHTML = `<div>${data.text}</div>`
}
// 初始化执行
effect();
setTimeout(() => {
data.text = "reactivity";
// 当数据发生变化的时候,执行渲染
effect();
}, 2000);
非常简陋的,需要手动去处理的数据响应式已经诞生了。
对于当前的数据响应式,虽然实现了,但是问题多多。
是否有办法可以直接在 data.text = "reactivity"时,自动就执行渲染呢?
可以使用Object.defineProperty。当属性发生变化的时候会触发set操作,set触发时进行渲染操作。
但是这是Vue2的使用方法。在Vue3中使用的是proxy。
2、使用Proxy去处理响应式问题
稍微对方法进行一下封装处理。(Proxy文档)
- createReactiveObject函数去创建一个响应式对象。
- 创建render函数,其中包含的是需要渲染的内容。
- effect副作用函数,接收一个渲染函数作为参数,然后执行该渲染函数。
- 当响应式对象修改的时候,去执行渲染。
// 响应式对象的创建
function createReactiveObject(raw) {
// 创建一个proxy对象
const proxy = new Proxy(raw, {
// get数据获取的时候
get(target, key) {
const res = Reflect.get(target, key);
// TODO
return res;
},
// 数据设置的时候
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO
effect(render);
return res;
}
});
// 返回一个响应式对象
return proxy;
}
let obj = createReactiveObject({
text: "effect2"
});
// 副作用函数,用于执行
function effect(fn) {
fn();
}
// render 需要渲染的内容
function render() {
document.body.innerHTML = `<div>${obj.text}</div>`
}
// 初始化调用
effect(render);
// 延时修改响应式对象的数据。
setTimeout(() => {
obj.text = "reactivity2";
}, 2000);
从上面的内容可以看出。
- 通过createReactiveObject 方法创建了一个proxy对象。
- render方法中渲染绑定的是obj (proxy创建的代理对象)。
- effect(render); 初始化调用时,渲染内容为 "effect2" ;
- setTimeout时,去修改obj.text 的内容时,会触发proxy对象的set操作。set操作中会执行 effect 副作用函数,去处理渲染的问题。
但是对于上面的内容,再进行优化处理。 处理的内容包括:
- 处理effect,因为effect在处理render的时候,会需要处理一些其他的操作
- 移除createReactiveObject中的手动effect。 配合改写后的effect,在get的时候,对当前的effect进行track收集。在set的时候,把effect中收集的内容进行调用。
首先是对effect进行改造
如果想要在渲染的时候进行特殊的处理。那么就把fn在执行前,调用所需要处理的函数。
这里采用的是抽离 一个effect 的类去处理, 在后续的扩展中,可以在执行渲染之前去处理想要处理的内容。具体代码如下:
// 声明一个全局变量,用来保存当前的effect,并且用于在依赖收集时进行收集
let activeEffect = '';
class ReactiveEffect {
constructor(fn) {
this._fn = fn;
}
run() {
// 在run的时候,保存当前active的副作用函数
activeEffect = this;
this._fn();
}
}
function effect(fn) {
// 创建一个_effect的实例。
let _effect = new ReactiveEffect(fn);
// 可以在执行run之前,执行其他渲染前的操作
_effect.run();
}
对createReactiveObject进行改造:
// 响应式对象的创建
function createReactiveObject(raw) {
// 创建一个proxy对象
const proxy = new Proxy(raw, {
// get数据获取的时候
get(target, key) {
const res = Reflect.get(target, key);
// TODO 依赖收集
track(target, key);
return res;
},
// 数据设置的时候
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO 触发依赖
trigger(target, key);
return res;
}
});
// 返回一个响应式对象
return proxy;
}
const targetMap = new WeakMap();
// targetMap: WeakMap -> 内容为 target => depsMap
// depsMap: Map -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
function track(target, key) {
if (!activeEffect) return;
// 从依赖收集Map中通过 target 提取 depsMap: Map - (key => deps)
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果depsMap不存在,表示首次收集,new Map 存入
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 从 depsMap中通过key获取 deps: Set - { effect... }
let deps = depsMap.get(key);
if (!deps) {
// 如果 deps不存在,则给 depsMap添加一个new Set
deps = new Set();
depsMap.set(key, deps);
}
if (!deps.has(activeEffect)) {
// 如果deps中不存在activeEffect, 则对依赖进行收集
deps.add(activeEffect)
}
}
// 触发依赖
function trigger(target, key) {
// 获取依赖
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = [];
deps.push(depsMap.get(key));
const effects = [];
deps.forEach((dep) => {
effects.push(...dep)
});
// 遍历执行依赖
effects?.forEach((fn) => fn.run())
}
将上述的代码进行整合。形成完整的数据响应式原理。
3、完整的响应式原理
完整的响应式原理如下所示
// 响应式对象的创建
function createReactiveObject(raw) {
// 创建一个proxy对象
const proxy = new Proxy(raw, {
// get数据获取的时候
get(target, key) {
const res = Reflect.get(target, key);
// TODO 依赖收集
track(target, key);
return res;
},
// 数据设置的时候
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO 依赖触发
trigger(target, key);
// effect(render);
return res;
}
});
// 返回一个响应式对象
return proxy;
}
// 当前的effect副作用函数
let activeEffect = '';
// 依赖收集的容器
const targetMap = new WeakMap();
// targetMap: WeakMap -> 内容为 target => depsMap
// depsMap: Map -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
function track(target, key) {
if (!activeEffect) return;
// 从依赖收集Map中通过 target 提取 depsMap: Map - (key => deps)
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果depsMap不存在,表示首次收集,new Map 存入
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 从 depsMap中通过key获取 deps: Set - { effect... }
let deps = depsMap.get(key);
if (!deps) {
// 如果 deps不存在,则给 depsMap添加一个new Set
deps = new Set();
depsMap.set(key, deps);
}
if (!deps.has(activeEffect)) {
// 如果deps中不存在activeEffect, 则对依赖进行收集
deps.add(activeEffect);
}
}
// 依赖触发。触发依赖主要是跟收集时是同样的步骤的。不同的是触发依赖最后一步是 执行依赖(收集起来的依赖)
function trigger(target, key) {
// 依赖的触发与收集形式是差不多的
const depsMap = targetMap.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
for (const effect of deps) {
effect.run();
}
//const deps = [];
// 这里留存的有包括后续扩展内容,暂时生搬硬套即可(在后续任务调度会添加额外的内容)
//deps.push(depsMap.get(key));
//const effects = [];
//deps.forEach((dep) => {
// effects.push(...dep)
//});
//effects?.forEach((fn) => fn.run())
}
let obj = createReactiveObject({
text: "effect3"
});
// 副作用函数,用于执行
class ReactiveEffect {
constructor(fn) {
// 将副作用函数需要处理的对调函数进行接受
this._fn = fn;
}
run() {
// 全局变量保存当前的副作用函数,用于依赖收集和调用
activeEffect = this;
this._fn();
}
}
// 副作用函数
function effect(fn) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
// render 需要渲染的内容
function render() {
document.body.innerHTML = `<div>${obj.text}</div>`
}
effect(render);
setTimeout(() => {
obj.text = "reactivity3";
}, 2000);