前言
vue3从发布开始已经有一年有余,近来开始撸源码,真是惭愧至极,啥也别说了,洗心革面 开干!直接上源码枯燥乏味
这里仅仅是我自己的理解响应式原理之后的简版代码
目标
我们今天的目标
- 1、通过从ref 入手,彻底的了解响应式的原理
- 2、理解effect 的副作用函数是怎么响应式执行的
ref 函数的原理
首先我们来看看ref官方文档是怎么解释ref 函数的
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property.value。
通俗的将其实就是当前的ref 函数返回的值就是一个对象,这个对象包含get 和set ,转换成es5 就是Object.defineProperty 监听的一个值
废话少说,看代码
// 判断是不是对象
const isObject = (val) => val !== null && typeof val === 'object';
// ref的函数
function ref(val) {
// 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
// 我们在对象情况下就不在使用value 访问
return isObject(val) ? reactive(val) : new refObj(val);
}
//创建响应式对象
class refObj {
constructor(val) {
this._value = val;
}
get value() {
// 在第一次执行之后触发get来收集依赖
track(this, 'value');
return this._value;
}
set value(newVal) {
console.log(newVal);
this._value = newVal;
trigger(this, 'value');
}
};
看了上述代码我们发现,其实当前的这个神奇的响应式的值,就是一个对象 ,当你改变这个值的时候,就会触发当前这个对象的get 和set 从而达到响应式的能力
接下来发现是个对象就好办了,我们就能在get 和set 的方法中去做一些事情,比如建立副作用和当前这个值的关系,也就是依赖收集
但是此时又会有个问题,如果在ref 中传入一个对象,new 当前这个对象的时候,就不好使了,因为里面的值就监听不到了
于是vue3中大名鼎鼎的Proxy登场了
Proxy
具体的使用方法,咱就不介绍,vue3出来这么长时间了, 相信大家都明白他的特性,直接上代码
// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
// 其实对象的响应式处理也就是重复执行reactive
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// Reflect用于执行对象默认操作,更规范、函数式
// Proxy和Object的方法Reflect都有对应
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key);
return res;
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
trigger(target, key);
return res;
}
});
}
上述代码中,将对象类型的也变成了响应式对象,接下来就是重点的地方了,要在这两个响应式的对象的get 和set 中去做依赖收集,和派发对应的副作用更新
既然需要副作用,那么怎么也要先收集一下吧,于是effect相当于桥梁函数
effect实现
总的来说这个effect 做了什么事情呢?他其实就是对当前的副作用函数进行包装,然后执行,触发副作用函数中的get,在get中在收集当前副作用,代码如下
// 保存临时依赖函数用于包装
const effectStack = [];
// 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
// 我们意在理解原理,只需要包装fn 即可
function effect(fn) {
// 包装当前依赖函数
const effect = function reactiveEffect() {
// 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
if (!effectStack.includes(effect)) {
try {
// 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
effectStack.push(fn);
// 执行当前函数,开始依赖收集了
return fn();
} finally {
// 执行成功了出栈
effectStack.pop();
}
};
};
effect();
}
他的原理比较巧妙,利用一个栈,将当前正在执行的副作用函数临时存储,在get中取出,存入依赖对象中。
那么如此一来,顺其自然的就需要有一个函数去收集依赖(这个依赖有可能是一个render 函数,也有可能是一个副作用,我们的例子中,由于没有涉及视图渲染相关,都是副作用),于是定义一个track 函数去收集依赖
track实现
废话少说上代码
// 依赖关系的map对象只能接受对象
let targetMap = new WeakMap();
// 在收集的依赖中建立关系
function track(target, key) {
// 取出最后一个数据内容
const effect = effectStack[effectStack.length - 1];
// 如果当前变量有依赖
if (effect) {
//判断当前的map中是否有target
let depsMap = targetMap.get(target);
// 如果没有
if (!depsMap) {
// new map存储当前weakmap
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取key对应的响应函数集
let deps = depsMap.get(key);
if (!deps) {
// 建立当前key 和依赖的关系,因为一个key 会有多个依赖
// 为了防止重复依赖,使用set
deps = new Set();
depsMap.set(key, deps);
}
// 存入当前依赖
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
track就有讲究了,他其实是建立了当前的响应式对象的每一个key 和 依赖对应关系,从而当key 发生变换的时候通知所有的依赖更新,怕大家不太理解,贴心的画了张图供大家理解
根据图中结构,我们就能看到,所有依赖的数据结构
接下来我们就需要派发更新,使用trigger函数来处理
trigger实现
代码如下
// 用于触发更新
function trigger(target, key) {
// 获取所有依赖内容
const depsMap = targetMap.get(target);
// 如果有依赖的话全部拉出来执行
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key);
if (deps) {
// 执行所有响应函数
const run = (effect) => {
// 源码中有异步调度任务,我们在这里省略
effect();
};
deps.forEach(run);
}
}
}
从以上代码看就非常简单取出当前修改的key 对应的依赖,全部执行一下也就是所谓的派发更新,到这里基本响应式原理基本都结束了。就是这么简单且有趣!之后附上自己画的响应式流程图,供大家理解,不对之处请指点
最后
我自己所理解的vue的响应式模块到此全部完毕,当然源码中有这很多兼容处理,高端写法。我们这里只为研究原理,暂不深究如有兴趣请移步 reactivity模块,详细研究。结尾附上可以跑的完整源码,亲自尝试一下吧!
// 保存临时依赖函数用于包装
const effectStack = [];
// 依赖关系的map对象只能接受对象
let targetMap = new WeakMap();
// 判断是不是对象
const isObject = (val) => val !== null && typeof val === 'object';
// ref的函数
function ref(val) {
// 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
// 我们在对象情况下就不在使用value 访问
return isObject(val) ? reactive(val) : new refObj(val);
}
//创建响应式对象
class refObj {
constructor(val) {
this._value = val;
}
get value() {
// 在第一次执行之后触发get来收集依赖
track(this, 'value');
return this._value;
}
set value(newVal) {
console.log(newVal);
this._value = newVal;
trigger(this, 'value');
}
};
// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
// 其实对象的响应式处理也就是重复执行reactive
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// Reflect用于执行对象默认操作,更规范、函数式
// Proxy和Object的方法Reflect都有对应
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key);
return res;
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
trigger(target, key);
return res;
}
});
}
// 到此处,当前的ref 对象就已经实现了对数据改变的监听
const newRef = ref(0);
// 但是还是没有响应式的能力,那么他是怎样实现响应式的呢----依赖收集,触发更新=
// 用来做依赖收集
// 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
// 我们意在理解原理,只需要包装fn 即可
function effect(fn) {
// 包装当前依赖函数
const effect = function reactiveEffect() {
// 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
if (!effectStack.includes(effect)) {
try {
// 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
effectStack.push(fn);
// 执行当前函数,开始依赖收集了
return fn();
} finally {
// 执行成功了出栈
effectStack.pop();
}
};
};
effect();
}
// 在收集的依赖中建立关系
function track(target, key) {
// 取出最后一个数据内容
const effect = effectStack[effectStack.length - 1];
// 如果当前变量有依赖
if (effect) {
//判断当前的map中是否有target
let depsMap = targetMap.get(target);
// 如果没有
if (!depsMap) {
// new map存储当前weakmap
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取key对应的响应函数集
let deps = depsMap.get(key);
if (!deps) {
// 建立当前key 和依赖的关系,因为一个key 会有多个依赖
// 为了防止重复依赖,使用set
deps = new Set();
depsMap.set(key, deps);
}
// 存入当前依赖
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
// 用于触发更新
function trigger(target, key) {
// 获取所有依赖内容
const depsMap = targetMap.get(target);
// 如果有依赖的话全部拉出来执行
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key);
if (deps) {
// 执行所有响应函数
const run = (effect) => {
// 源码中有异步调度任务,我们在这里省略
effect();
};
deps.forEach(run);
}
}
}
effect(() => {
console.log(11111);
// 在自己实现的effect中,由于为了演示原理,没有做兼容,不能来触发set,否则会死循环
// vue源码中触发对effect中的做了兼容处理只会执行一次
newRef.value;
});
newRef.value++;