最新 vue-next
的源码发布了,虽然是 pre-alpha
版本,但这时候实际上是阅读源码的比较好的时机。在 vue
中,比较重要的东西固然要数它的响应式系统,在以前的版本中,已经有若干篇文章对它的响应式原理和实现进行了介绍,这里就不赘述了。在 vue-next
中,其实现原理和以前仍是相同的,即经过观察者模式和数据劫持,只不过对其实现方式进行了改变。vue
对于解析原理的文章,我我的是比较喜欢那种“小白”风格的文章,即不要摘录特别多的代码,也不要阐述一些很深奥的原理与概念。在我刚接触 react
的时候,还记得有一篇利用 jquery
来介绍 react
的文章,从简入繁,面面俱到,其背后阐述的知识点对我后来学习 react
起到不少的帮助。react
所以,这篇文章我也打算按这种风格来写一下利用最近空闲时间阅读 vue-next
响应式模块的源码的一些心得与体会,算是抛砖引玉,同时实现一个极简的响应式系统。jquery
若有错误,还望指正。git
不管是阅读这篇文章,仍是阅读 vue-next
响应式模块的源码,首先有两个知识点是必备的:es6
Proxy
:es6 中新的代理内建工具类Reflect
:es6 中新的反射工具类因为篇幅有限,这里也不详细赘述这两个类的用途与使用方法了,推荐三篇我认为不错的文章,仅供参考:github
对于 vue-next
响应式系统的 RFC
,能够参考[这里](github.com/vuejs/rfcs/…。虽然距离如今有一段时间了,可是经过阅读源码,能够发现一些影子。api
咱们大致要实现的效果以下面的代码所示:数组
// 实现两个方法 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
来实现这个极简版本的响应式系统。主要涉及到的类型和公共变量以下:bash
type Effect = Function;
type EffectMap = Map<string, Effect[]>;
let currentEffect: Effect;
const effectMap: EffectMap = new Map();
复制代码
currentEffect
:用来储存当前正在收集依赖的 effect
effectMap
:表明目标对象每一个 key
所对应的依赖于它的 effect
数组,也能够把它理解为观察者模式中的订阅者字典在以前的版本中,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
这个方法中,返回一个代理对象的实例便可,还记的上文中咱们在实现以前脑海中浮现的大体代码框架吗?
以下:
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
下进行依赖收集的目的。
我在 codepen
上简单写了一个计数器 demo
,连接以下: codepen.io/littlelyon1…
这个极简的响应式系统虽然能用,可是有不少未考虑的因素,其实就是在上文中被咱们忽略的那些前提条件,这里再列举一下,并给出源代码中的解法:
ref
对象,其 value
指向基础数据类型的值trap
处理逻辑但我仍然推荐你直接去阅读一下源码,由于你会发现,源码会在这个极简版本基础上,利用了更加复杂数据结构以及流程,来控制依赖收集和触发响应的流程,同时各类特殊状况也有更加明细的考虑。
另外,这仅仅是 vue-next
响应式系统的简易实现,诸如其余功能模块,好比指令、模板解析、vdom
等,我也准备利用最近的空闲时间再去看看,有时间的话,最近也整理出来,分享给你们。
关注公众号全栈_101,只谈技术,不谈人生。
长期兼职接各类规模的外包项目,有意者私聊。