vue3源码解析(一) 响应式系统

388 阅读6分钟

划了一个礼拜的水,都不晓得干了点啥,写一写目前对vue3的响应式系统源码阅读一些心得吧。

目录: vue-next/reactivity/dist/reactivity.cjs.js

总体来看分为三部分 1)effect

讲道理没怎么看懂这effect,只是粗略的知道每次变动触发响应式系统的时候,就用一个effect来描述。每个effect其实是一个function

2)vue3著名的proxy

这部分写的是如何把传入的对象处理变成一个proxy

3)暴露的接口如ref()/computed()

这部分是利用上面的两部分完成的,写这篇博客的时候暂时只用到了ref和computed,其他不了解的接口先不写了

##effect部分 假如effect是一个类,那么大概是这样的 class effect{ isLazy : Boolean, // 代表生成后是否马上触发一次

computed : Boolean, // 代表是否computed属性

scheduler : Function,  // 触发effect

_isRef : Boolean, // 是否ref

fn : Function,  // 处理effect造成的影响,比如是计算属性的话会修改计算值取值

deps : Map // 用于存储effect会影响的所有响应式对象

}

源码的最前面是targetMap对象,该map用于存储所有的依赖关系。key是被监听的对象,value是该对象涉及的全部effect的一个set。(其实每个effect里面也保存了相关的target,确保effect触发的时候能知道需要修改哪些target)

然后是effectStack数组,用于存储目前为止的所有effect(有可能每次修改其实会触发的effect不止一种,可能一种effect里面会触发另一种effect,所以通过这栈来实现结算顺序的控制)

还有一个trackStack数组,这对应是effectStack数组,貌似有一些effect触发的时候是不希望对对象进行监听的,trackStack就是用来标记栈中每个effect是否希望对对象进行监听的。

重点讲一下track(target, type, key)函数,target和key都很好理解,type就是操作类型,包含get,has,iterate,每次调用track就说明对象多了一个依赖,于是对象会被纳入targetMap,并且activeEffect会被添加到target的set里面去

还有一个重点trigger(target, type, key, newValue, oldValue, oldTarget) type包括set,add,delete,clear。 首先会从targetmap看看target是否有被监听,如果没有就省事了,然后声明了两个set:effects和computedRunners。大概就是用来装载修改这属性会导致的UI修改,以及computed的修改。

接下来根据type的类型进行处理:

第一种情况,如果是clear,那么肯定全部相关的依赖都要修改,因此把对象关联的depsMap中的所有effect,根据是否computed分为两组,分别加入上面的effects和computedRunners

第二种情况,如果修改的key是length且target是数组的话,那么遍历数组中所有下标在newVal后面的值,因为修改length的话相当于把length后面的都抛弃了,所以新的length后面的值都需要进行处理,同样分成普通effect和computed加入上面说的两个set中。

最后一种情况,将depsMap中和key关联的effect拿出来,分成两种effect加入上面的两个set中。如果是增/删/map的set,同样需要修改target的遍历器对应的依赖。

最后就是遍历两个set中的每一个effect,如果effect带有scheduler方法的话,则调用scheduler方法,否则直接调用effect。

effect的部分到这里就结束了

##第二部分 接下来到重中之重的proxy部分

(先说一下一段我比较在意的代码builtInSymbols,这是用来返回当前在Symbol原型上目前已有的属性对应的值,并把其中是symbo的装起来,比如Symbol.iterator)

首先定义了一个数组的工具对象,用于封装对'includes', 'indexOf', 'lastIndexOf'这三个方法的访问。先拿到被代理的原数组,然后将方法参数传入,如果有结果则直接返回,没有的话,对参数进行toRaw之后再传入,再返回。(因为vue3对引用的调用都要通过obj.value这样去调用对象原生的值,所以这里先用obj去传入include等方法,如果不行再把obj.value传入)

是定义getter们和setter们,有正常的/浅响应的/只读的/浅响应只读的。总的来说,先判断是否数组。如果是数组且调用的是上面说的三个方法,则直接调用上面的方法并返回;接下来把key对应的value从target上拿出来。如果是浅响应,则track记录一下依赖,返回value;如果value是ref,且target是array,track一下,返回value;如果value是ref且target不是array,返回target[key].value(因为取ref的时候,其实希望返回的是ref.value,备注说ref unwrapping, only for Objects, not for Arrays.) 在getter的最后,如果target不是只读就要track,如果target[key]是一个对象,把对象做成proxy再返回出去,如果是普通类型则直接返回。

接着是setter,只有普通的和浅响应两个版本

先把target[key]现在的值取出保存,如果是浅响应且target不是数组,且目前的值已经是ref且新的value不是ref,则直接赋值返回true;进行set,set之后再判断,如果修改的是原型链上的东西,不做trigger直接返回;如果修改的target本身的属性,判断是新增属性还是修改属性,进行对应的trigger

然后,vue对于proxy的遍历/foreach也做了封装。具体看起来都差不多……就是各种toRaw和track

这里还有很多对get/set/delete等方法的封装,其实就是为了在调用的时候加入track和trigger,就不一一看了

##第三部分,

上来先声明四个map,这里的目的是能够根据任何一个对象找到其对应的proxy,或者根据proxy找到对应的对象

rawToReactive reactiveToRaw rawToReadonly readonlyToRaw

还有一个set,用于存储只读或者不需要响应的值

rawValues

讲一个重点的函数createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers)

简单的说,就是每创建一个proxy,就会在toProxy和toRaw里面添加一次关系,并且同一个对象的话只会添加一次。

然后就是vue.ref,先判断是不是浅响应,如果是深响应的话要对对象的每一层都做监听(其实就是再调用一次ref)。 处理好value之后,创建r,设置r._ref = true,所有get都调用track之后直接返回value;调用set的时候(这里相当于把ref整个换掉了),如果检查到前后的值不同,判断一下是否浅响应,然后触发trigger,结束。

vue.customRef传入工厂函数,用于自定义ref

vue.toRefs 用于创建__ref为true的普通对象……意义是什么? 文档说把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。(但是还是不能响应啊)

最后压轴的是computed

如果传入的是函数,则直接赋值为get,set用默认报错的函数;如果是对象,那么分别取其中的set和get.每次调用计算属性的时候,先检查依赖是否有变化,如果有变化先重新跑一次;然后track一下,返回。