这篇文章给大家分享的是有关vue-next响应式原理的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
预备知识
无论是阅读这篇文章,还是阅读 vue-next 响应式模块的源码,首先有两个知识点是必备的:
Proxy:es6 中新的代理内建工具类
Reflect:es6 中新的反射工具类
由于篇幅有限,这里也不详细赘述这两个类的用途与使用方法了,推荐三篇我认为不错的文章,仅供参考:
ES6 Proxies in Depth
ES6 Proxy Traps in Depth
ES6 Reflection in Depth
接口
对于 vue-next 响应式系统的 RFC,可以参考这里。虽然距离现在有一段时间了,但是通过阅读源码,可以发现一些影子。
我们大体要实现的效果如下面的代码所示:
// 实现两个方法 reactive 和 effect
const state = reactive({
count: 0
})
effect(() => {
console.log('count: ', state.count)
})
state.count++ // 输入 count: 1
可以发现我们熟悉的依赖收集阶段(同时也是观察者模式的订阅过程),是在 effect 中进行的,依赖收集的准备工作(即数据劫持逻辑),是在 reactive 中进行的,而数据变化的触发响应的逻辑在后面的 state.count++ 代码执行时进行(同时也是观察者模式的发布过程),之后便会执行之前传入 effect 内部的回调函数并输入 count: 1。
类型与公共变量
由于 vue-next 用 ts 进行了重写,这里我也使用 ts 来实现这个极简版本的响应式系统。主要涉及到的类型和公共变量如下:
type Effect = Function;
type EffectMap = Map<string, Effect[]>;
let currentEffect: Effect;
const effectMap: EffectMap = new Map();
currentEffect:用来储存当前正在收集依赖的 effect
effectMap:代表目标对象每个 key 所对应的依赖于它的 effect 数组,也可以把它理解为观察者模式中的订阅者字典
利用 Proxy 实现数据劫持
在之前的版本中,vue 利用 Object.defineProperty 中的 setter 和 getter 来对数据对象进行劫持,vue-next 则通过 Proxy。众所周知,Object.defineProperty 所实现的数据劫持是有一定限制的,而 Proxy 就会强大很多。
首先,我们在脑后中,设想一下如何使用 Proxy 来实现数据劫持呢?很简单,大体结构如下所示:
export function reactive(obj) {
const proxied = new Proxy(obj, handlers);
return proxied;
}
这里的 handlers 是声明如何处理各个 trap 的逻辑,比如:
const handlers = {
get: function(target, key, receiver) {
...
},
set: function(target, key, value, receiver) {
...
},
deleteProperty(target, key) {
...
}
// ...以及其他 trap
}
由于这里是极简版本的实现,那么我们就仅仅实现 get 和 set 两个 trap 就可以了,分别对应依赖收集和触发响应的逻辑。
依赖收集
对于依赖收集的实现,由于是极简版本,实现的前提如下:
不考虑对象的嵌套
不考虑集合类型
不考虑基础类型
不考虑对代理对象的处理
哈哈,基本这四点排除之后,这个依赖收集函数就会很轻很薄,如下:
function(target, key: string, receiver) {
// 仅仅在某个 effect 内部进行依赖收集
if (currentEffect) {
if (effectMap.has(key)) {
const effects = effectMap.get(key);
if (effects.indexOf(currentEffect) === -1) {
effects.push(currentEffect);
}
} else {
effectMap.set(key, [currentEffect]);
}
}
return Reflect.get(target, key, receiver);
}
实现的逻辑很简单,其实就是观察者模式中注册订阅者的实现逻辑,值得注意的是,这里对于 target 的赋值逻辑,我们委托给 Reflect 来完成,虽然 target[key] 也是可以工作的,但是使用 Reflect 是更提倡的方式。
触发响应
触发响应的逻辑就比较简单了,其实是对应观察者模式中,发布事件的逻辑,如下:
function(target, key: string, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
if (effectMap.has(key)) {
effectMap.get(key).forEach(effect => effect());
}
return result;
}
同样,这里使用 Reflect 来对 target 进行赋值操作,因为它会返回一个 boolean 值代表是否成功,而 set 这个 trap 也需要代表相同含义的值。
通过 reactive 方法来初始化代理对象
实现了数据劫持的代理逻辑之后,我们只需要在 reactive 这个方法中,返回一个代理对象的实例即可,还记的上文中我们在实现之前脑海中浮现的大致代码框架吗?
如下:
export function reactive(obj: any) {
const proxied = new Proxy(obj, {
get: function(target, key: string, receiver) {
if (currentEffect) {
if (effectMap.has(key)) {
const effects = effectMap.get(key);
if (effects.indexOf(currentEffect) === -1) {
effects.push(currentEffect);
}
} else {
effectMap.set(key, [currentEffect]);
}
}
return Reflect.get(target, key, receiver);
},
set: function(target, key: string, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
if (effectMap.has(key)) {
effectMap.get(key).forEach(effect => effect());
}
return result;
}
});
return proxied;
}
依赖收集的准备工作
上文中提到了,对于依赖收集的工作,我们是有条件地进行的,即在一个 effect 中,我们才会进行收集,其他情况下的取值逻辑,我们则不会进行依赖收集,因此,effect 方法正式为了实现这点而存在的,如下:
export function effect(fn: Function) {
const effected = function() {
fn();
};
currentEffect = effected;
effected();
currentEffect = undefined;
return effected;
}
之所以实现如此简单,是因为我们这里是极简版本,不需要考虑诸如 readOnly 、异常以及收集时机等因素。可以发现,就是将传入的回调函数包裹在另一个方法中,然后将这个方法用 currentEffect 这个变量暂存,之后尝试运行一下即可。当 effect 运行完毕之后,再将 currentEffect 置空,这样就可以达到只在 effect 下进行依赖收集的目的。
感谢各位的阅读!关于“vue-next响应式原理的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!