15 分钟掌握vue-next响应式原理
写在前面
最新vue-next的源码发布了,虽然是pre-alpha版本,但这时候其实是阅读源码的比较好的时机。在vue中,比较重要的东西当然要数它的响应式系统,在之前的版本中,已经有若干篇文章对它的响应式原理和实现进行了介绍,这里就不赘述了。在vue-next中,其实现原理和之前还是相同的,即通过观察者模式和数据劫持,只不过对其实现方式进行了改变。
对于解析原理的文章,我个人是比较喜欢那种“小白”风格的文章,即不要摘录特别多的代码,也不要阐述一些很深奥的原理与概念。在我刚接触react的时候,还记得有一篇利用jquery来介绍react的文章,从简入繁,面面俱到,其背后阐述的知识点对我后来学习react起到很多的帮助。
因此,这篇文章我也打算按这种风格来写一下利用最近空闲时间阅读vue-next响应式模块的源码的一些心得与体会,算是抛砖引玉,同时实现一个极简的响应式系统。
如有错误,还望指正。
预备知识
无论是阅读这篇文章,还是阅读vue-next响应式模块的源码,首先有两个知识点是必备的:
- Proxy:es6中新的代理内建工具类
- Reflect:es6中新的反射工具类
由于篇幅有限,这里也不详细赘述这两个类的用途与使用方法了,推荐三篇我认为不错的文章,仅供参考:
- ES6ProxiesinDepth
- ES6ProxyTrapsinDepth
- ES6ReflectioninDepth
接口
对于vue-next响应式系统的RFC,可以参考这里。虽然距离现在有一段时间了,但是通过阅读源码,可以发现一些影子。
我们大体要实现的效果如下面的代码所示:
//实现两个方法reactive和effect conststate=reactive({ count:0 }) effect(()=>{ console.log('count:',state.count) }) state.count++//输入count:1
可以发现我们熟悉的依赖收集阶段(同时也是观察者模式的订阅过程),是在effect中进行的,依赖收集的准备工作(即数据劫持逻辑),是在reactive中进行的,而数据变化的触发响应的逻辑在后面的state.count++代码执行时进行(同时也是观察者模式的发布过程),之后便会执行之前传入effect内部的回调函数并输入count:1。
类型与公共变量
由于vue-next用ts进行了重写,这里我也使用ts来实现这个极简版本的响应式系统。主要涉及到的类型和公共变量如下:
typeEffect=Function; typeEffectMap=Map; letcurrentEffect:Effect; consteffectMap:EffectMap=newMap();
- currentEffect:用来储存当前正在收集依赖的effect
- effectMap:代表目标对象每个key所对应的依赖于它的effect数组,也可以把它理解为观察者模式中的订阅者字典
利用Proxy实现数据劫持
在之前的版本中,vue利用Object.defineProperty中的setter和getter来对数据对象进行劫持,vue-next则通过Proxy。众所周知,Object.defineProperty所实现的数据劫持是有一定限制的,而Proxy就会强大很多。
首先,我们在脑后中,设想一下如何使用Proxy来实现数据劫持呢?很简单,大体结构如下所示:
exportfunctionreactive(obj){ constproxied=newProxy(obj,handlers); returnproxied; }
这里的handlers是声明如何处理各个trap的逻辑,比如:
consthandlers={ 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)){ consteffects=effectMap.get(key); if(effects.indexOf(currentEffect)===-1){ effects.push(currentEffect); } }else{ effectMap.set(key,[currentEffect]); } } returnReflect.get(target,key,receiver); }
实现的逻辑很简单,其实就是观察者模式中注册订阅者的实现逻辑,值得注意的是,这里对于target的赋值逻辑,我们委托给Reflect来完成,虽然target[key]也是可以工作的,但是使用Reflect是更提倡的方式。
触发响应
触发响应的逻辑就比较简单了,其实是对应观察者模式中,发布事件的逻辑,如下:
function(target,key:string,value,receiver){ constresult=Reflect.set(target,key,value,receiver); if(effectMap.has(key)){ effectMap.get(key).forEach(effect=>effect()); } returnresult; }
同样,这里使用Reflect来对target进行赋值操作,因为它会返回一个boolean值代表是否成功,而set这个trap也需要代表相同含义的值。
通过reactive方法来初始化代理对象
实现了数据劫持的代理逻辑之后,我们只需要在reactive这个方法中,返回一个代理对象的实例即可,还记的上文中我们在实现之前脑海中浮现的大致代码框架吗?
如下:
exportfunctionreactive(obj:any){ constproxied=newProxy(obj,{ get:function(target,key:string,receiver){ if(currentEffect){ if(effectMap.has(key)){ consteffects=effectMap.get(key); if(effects.indexOf(currentEffect)===-1){ effects.push(currentEffect); } }else{ effectMap.set(key,[currentEffect]); } } returnReflect.get(target,key,receiver); }, set:function(target,key:string,value,receiver){ constresult=Reflect.set(target,key,value,receiver); if(effectMap.has(key)){ effectMap.get(key).forEach(effect=>effect()); } returnresult; } }); returnproxied; }
依赖收集的准备工作
上文中提到了,对于依赖收集的工作,我们是有条件地进行的,即在一个effect中,我们才会进行收集,其他情况下的取值逻辑,我们则不会进行依赖收集,因此,effect方法正式为了实现这点而存在的,如下:
exportfunctioneffect(fn:Function){ consteffected=function(){ fn(); }; currentEffect=effected; effected(); currentEffect=undefined; returneffected; }
之所以实现如此简单,是因为我们这里是极简版本,不需要考虑诸如readOnly、异常以及收集时机等因素。可以发现,就是将传入的回调函数包裹在另一个方法中,然后将这个方法用currentEffect这个变量暂存,之后尝试运行一下即可。当effect运行完毕之后,再将currentEffect置空,这样就可以达到只在effect下进行依赖收集的目的。
运行效果
我在codepen上简单写了一个计数器demo,链接如下:
https://codepen.io/littlelyon1/pen/mddVPgo
写在最后
这个极简的响应式系统虽然能用,但是有很多未考虑的因素,其实就是在上文中被我们忽略的那些前提条件,这里再列举一下,并给出源代码中的解法:
- 基础数据类型的处理:可以将基础数据类型封装为一个ref对象,其value指向基础数据类型的值
- 嵌套对象:递归进行执行代理过程即可
- 集合对象:编写专门的trap处理逻辑
- 代理实例:缓存这些代理实例,下次遇到直接返回即可
但我仍然推荐你直接去阅读一下源码,因为你会发现,源码会在这个极简版本基础上,利用了更加复杂数据结构以及流程,来控制依赖收集和触发响应的流程,同时各种特殊情况也有更加明细的考虑。
另外,这仅仅是vue-next响应式系统的简易实现,诸如其他功能模块,比如指令、模板解析、vdom等,我也准备利用最近的空闲时间再去看看,有时间的话,最近也整理出来,分享给大家。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。