响应式是Vue的一个特性。简历里写Vue项目,基本都会问响应式实现原理。而且不仅仅是Vue,状态管理库Mobx也是基于响应式实现的。响应性是如何实现的?不说原理了,我们手写一个简单的版本。响应式首先,什么是响应式?响应式就是当观察到的数据发生变化时,做一系列的联动处理。就像一个社会热点事件,一旦有消息更新,所有媒体都会跟进并进行相关报道。这里的社会热点事件就是要观察的对象。那么在前端框架中,观察目标是什么?很明显,这是状态。一般有多个状态,按对象组织。因此,我们观察状态对象每个键的变化,并结合做一系列的处理。我们需要维护这样一个数据结构:state对象的每个key关联了一系列的effectsideeffects函数,也就是发生变化时联动执行的逻辑,通过Set来组织。每个key通过这种方式关联了一系列的效果函数,多个key可以维护在一个Map中。对象存在时这个Map就存在,对象销毁时它也会被销毁。(因为对象都没有了,也就不用再维护每个键关联的效果了)WeakMap正好有这样一个特性。WeakMap的键必须是对象,值可以是任意数据。当键的对象被销毁时,值也会被销毁。所以响应式Map会用WeakMap保存,key是原始对象。这个数据结构就是响应式的核心数据结构。比如这样一个状态对象:constobj={a:1,b:2}它的响应式数据结构是这样的:constdepsMap=newMap();constaDeps=newSet();depsMap.set('a',aDeps);constbDeps=newSet();depsMap.set('b',bDeps);constreactiveMap=newWeakMap()reactiveMap.set(obj,depsMap);创建的数据结构就是图中的那个:然后添加deps依赖,比如某个函数依赖a,就应该添加到a的deps集合中:effect(()=>{console.log(obj。A);});就是这样:constdepsMap=reactiveMap.get(obj);constaDeps=depsMap.get('a');aDeps.add(thisfunction);=这样维护deps函数没有问题,但是应该用户手动添加部门?那样不仅会侵入业务代码,也容易漏掉。所以,绝对不能让用户自己手动去维护dep,而是要做自动的依赖收集。那么如何自动收集依赖呢?当读取state值时,就建立了与state的依赖关系,所以很容易想到可以通过proxystateget来实现。通过Object.defineProperty或Proxy:constdata={a:1,b:2}letactiveEffectfunctioneffect(fn){activeEffect=fnfn()}constreactiveMap=newWeakMap()constobj=newProxy(data,{get(targetObj,key){letdepsMap=reactiveMap.get(targetObj);if(!depsMap){reactiveMap.set(targetObj,(depsMap=newMap()))}letdeps=depsMap.get(key)if(!deps){depsMap.set(key,(deps=newSet()))}deps.add(activeEffect)returntargetObj[key]}})effect会执行传入的回调函数fn,当你在fnobj的时候.a被读取,会触发get,获取对象的响应式Map,并从中取出a对应的deps集合,将当前效果函数添加到其中。这样就完成了一次依赖收集。修改obj.a时,需要通知所有deps,所以需要proxyset:set(targetObj,key,newVal){targetObj[key]=newValconstdepsMap=reactiveMap.get(targetObj)if(!depsMap)returnconsteffects=depsMap.get(key)effects&&effects.forEach(fn=>fn())}基本响应完成,我们测试一下:打印两次,第一次是1,第二次是3。effect会先执行一次传入的回调函数,然后触发get收集依赖。此时打印出来的obj.a为1,然后当obj.a赋值为3时,就会触发set去执行收集到的依赖。这时候会打印出obj。a是3个依赖也都收集正确了:结果是正确的,我们已经完成了基本的响应式风格!当然,响应式风格的代码不止这么一点。我们目前的实现还不完善,还存在一些问题。比如代码中有分支开关,上一次执行依赖obj.b,下一次执行不依赖。此时是否存在无效依赖?这样一段代码:constobj={a:1,b:2}effect(()=>{console.log(obj.a?obj.b:'nothing');});obj.a=undefined;对象.b=3;第一次执行效果函数,obj.a为1,此时会走到第一个分支,依赖obj.b。将obj.a改为undefined,触发set,执行所有依赖函数。这时候去分支2,不再依赖obj.b。把obj.b改成3,按理说此时没有依赖b的函数。我们试一下:第一次打印2是正确的,就是我们到了第一个分支,打印obj.b,第二次打印nothing也是对的,这时候去第二个分支。但是第三次??什么都不打印是错误的,因为此时obj.b已经没有依赖函数了,但是还是打印出来了。打印查看deps,会发现obj.b的deps并没有被清除,所以解决方法是每次添加依赖前先清除之前的deps。如何清除与函数关联的所有deps?记录一下吧。我们对已有的效果函数进行改造:letactiveEffectfunctioneffect(fn){activeEffect=fnfn()}记录这个效果函数放在了哪个depssets。即:letactiveEffectfunctioneffect(fn){consteffectFn=()=>{activeEffect=effectFnfn()}effectFn.deps=[]effectFn()}用前面的fn包裹一层,在函数中添加一个deps数组来记录添加了哪些依赖集合。get收集依赖的时候,也会在这里记录一份:这样下次执行effect函数的时候,可以把effect函数从上次添加的依赖集合中删除:cleanup实现如下:functioncleanup(effectFn){for(leti=0;i
