前言
大部分使用过vue3的同学都知道,vue3的底层的响应式实现由Object.defineProperty更换成了Proxy。
为什么vue3要更换呢?proxy相对于前者又有何优势呢?
接下来让我们通过案例去一探究竟吧!
当响应式不存在
我们先看一个例子
let shoes = { num: 3, price: 10,}let total = shoes.num * shoes.priceconsole.log(total) // 30shoes.num = 5console.log(total) // 30
第二次打印依旧是30,虽然我们的num发生了变化,但是下一次获取total的值依旧是之前的值,因为total已经被运算过了。
那应该怎么做,才能实时的获取到当前最新的total呢?
也很简单,我们每次获取之间,手动重新计算一次就好了。
let shoes = { num: 3, price: 10,}let total = 0function effect() { total = shoes.num * shoes.price}effect() // 重新计算console.log(total) // 30shoes.num = 5effect() // 重新计算console.log(total) // 50
我们增加effect方法来手动触发依赖,这样我们实现了需求。
但是这样手动触发的方式,在真实业务中过于繁琐,难以维护,本质上依旧是命令式思维。
如何实现值的修改,后续逻辑的自动执行呢?
vue2的解决方案
通过Object.defineProperty来对字段进行代理,通过set,get方法,完成逻辑的自动触发。
let num = 3let shoes = { num: num, price: 10,}let total = 0function effect() { console.log('开始计算', shoes) total = shoes.num * shoes.price}// 被代理的值无法不可再get中使用了 因为会触发ett的死循环// 所以,必须增加一个变量来做被代理的值,所以我们监听shoes.num的get set内部实际修改和读取的都是numObject.defineProperty(shoes, 'num', { set(newVal) { num = newVal effect() }, get() { return num },})
我们再以上代码,再次修改shoes.num,将触发代理中的set,进而触发effect,实现依赖的自动触发,vue2的底层也正是如此实现的,这样看起来我们的需求已经解决了,那为何vue3有放弃了Object.defineProperty呢?
接下来我们就要聊聊他的缺陷。
Object.defineProperty的缺陷
该API确实满足了我们上面提到的案例,但是他在一些场景也存在很多问题。
比如大家一定都遇到过的问题
-
object中新增字段 没有响应性
-
array中指定下标的方式增加字段 没有响应性的
为什么会这样呢?vue的官方解释是
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。
尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
那JavaScript到底限制了什么呢?
object.defineProperty只能监听到指定对象的指定属性的get set,这些工作其实是vue初始化阶段完成,所以指定对象的指定元素发生变化的时候,我们可以监听到变化,vue中也确实是这么表现的;
但是如果,我们在指定对象上面新增属性,object.defineProPerty是无法监听到的,无法监听则无法处理被新增的字段,自然字段就不具备响应式;
在vue2中,如果想解决以上问题,需要使用Vue.$set进行手动增加响应式字段,解决无法监听到字段新增的问题。
vue3的解决方案
vue3中改用了proxy,为什么响应式核心api做了修改,proxy是什么?我们先实现一个类似vue2的案例
let shoes = { num: 3, price: 10,}let shoesProxy = new Proxy(shoes, { // target 被代理对象 key 本次修改的对象中的键 newValue 修改后的值 receiver 代理对象 set(target, key, newValue, receiver) { console.log('触发了写入事件') shoes[key] = newValue effect() return true }, // target 被代理对象 key 本次读取的值 receiver 代理对象 get(tartget, key, receiver) { console.log('触发了获取事件') return shoes[key] },})let total = 0function effect() { console.log('开始计算', shoes) // 如果使用被代理对象本身shoes,这不会触发 // 如果使用代理对象shoesProxy,则这里会触发proxy的get事件 total = shoes.num * shoes.price}
通过以上代码,我们可以看到一些差别
object.defineproperty
-
代理的并非对象本身,而是对象中的属性
-
只能监听到对象被代理的指定属性,无法监听到对象本身的修改
-
修改对象属性的时候,是对原对象进行修改的,原有属性,则需要第三方的值来充当代理对象
proxy
-
proxy针对对象本身进行代理
-
代理对象属性的变化都可以被代理到
-
修改对象属性的时候,我们针对代理对象进行修改
无论是逻辑的可读性,还是API能力上,proxy都比object.defineProPerty要强很多,这也是vue3选择proxy的原因。
proxy的好兄弟Reflect
在vue3的源码中的**@vue/reactivity**中,*
我们会经常看到在proxy的set、get中存在Reflect的身影*
,但是从我们上面对proxy的使用来看,赋值 读取都实现了,为什么vue3中使用了Reflect呢?
首先我们了解一下Reflect是干嘛的
官方解释:Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
似乎比较难理解,我们举个例子吧
let obj = { num:10 }obj.num // 10Reflect.get(obj,'num') // 10
这么来看,似乎这个api很普通啊,反而把简单的读取值写复杂了。
这时候我们就要提一下Reflect.get 的第三个参数了
Reflect.get(target, propertyKey, receiver]) // receiver 如果target对象中指定了propertyKey,receiver则为getter调用时的this值。
这次我们知道了,第三个参数receiver具有强制修改this指向的能力,接下来我们来看一个场景
let data = { name: '张三', age: '12岁', get useinfo() { return this.name + this.age },}let dataProxy = new Proxy(data, { get(target, key, receiver) { console.log('属性被读取') return target[key] },})console.log(dataProxy.useinfo)
打印情况如下
属性被读取张三12岁
dataProxy.useinfo的get输出的值是正常的,但是get只被触发了一次,这是不正常的;
因为useinfo里面还读取了被代理对象data的name、age,理想情况应当是get被触发三次。
为什么会出现这样的情况呢,这是因为调用userinfo的时候,this指向了data,实际执行的是data.userinfo,此时的this指向data,而不是dataProxy,此时get自然是监听不到name、age的get了。
这时候我们就用到了Reflect的第三个参数,来重置get set的this指向。
let dataProxy = new Proxy(data, { get(target, key, receiver) { console.log('属性被读取') return Reflect.get(target, key, receiver) // this强制指向了receiver // return target[key] },})
打印情况如下
属性被读取属性被读取属性被读取张三12岁
现在打印就正常了,get被执行的3次,此时的this指向了dataProxy,Reflect很好的解决了以上的this指向问题。
通过以上案例,我们可以看到使用target[key]*
有些情况下是不符预期的,比如案例中的被代理对象this指向问题,而使用*
Reflect则可以更加稳定的解决这些问题,在vue3源码中也确实是这么用的。
补充(WeakMap)
通过以上文章,我们了解到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,这是vue3最核心的api。
但是仅仅知道理解proxy+reflect,还不太够,为了尽量轻松的阅读Vue3源码,我们还要学习一个原生API,那就是WeakMap。
weakMap和map一样都是key value格式,但是他们还是存在一些差别。
-
weakMap的key必须是对象,并且是弱引用关系
-
Map的key可以是任何值(基础类型+对象),但是key所引用的对象是强引用关系
通过查阅MDN我们可以发现,weakMap可以实现的功能,Map也是可以实现的,那为什么Vue3内部使用了WeakMap呢,问题就在引用关系上
强引用:不会因为引用被清除而失效
弱引用:会因为引用被清除而自动被垃圾回收
概念似乎还无法体现其实际作用,我们通过以下案例即可明白
// Maplet obj = { name: '张三' }let map = new Map()map.set(obj, 'name')obj = null // obj的引用类型被垃圾回收console.log(map) // map中key obj依旧存在// WeakMaplet obj = { name: '张三' }let map = new WeakMap()map.set(obj, 'name')obj = null // obj的引用类型被垃圾回收console.log(map) // weakMap中key为obj的键值对已经不存在
通过以上案例我们可以了解到
-
弱引用在对象与key共存场景存在优势,作为key的对象被销毁的同时,WeakMap中的key value也自动销毁了。
-
弱引用也解释了为什么weakMap的key不能是基础类型,因为基础类型存在栈内存中,不存在弱引用关系;
在vue3的依赖收集阶段,源码中用到了WeakMap,具体什么作用?我们下一节进行解答。
小节
我们认识到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,还有一个Map的原生的API,WeakMap的作用。
Reactvie+effect源码解析
前言
reactive的含义如其名称,通过reactive创建的对象都是具备响应式的。即reactive对象的改变会造成副作用。
于是我们引出副作用API(effect),如果effect内部依赖了reactive,则reactive的改变会重新触发effect。
现在让我们走进案例与源码,看看究竟是如何实现响应式的。
案例
let { reactive, effect } = Vue const obj = reactive({ name: '卖鱼强', }) effect(() => { document.querySelector('#app').innerText = obj.name })setTimeout(() => { obj.name = '狂飙强'}, 2000)
以上测试案例,我们涉及到了三个重要的阶段
-
reactive初始化
-
effect初始化
-
reactive发生修改
最后形成了effect的自动触发,我们就从以上三个角度去切入源码实现。
reactive初始化
为了方便阅读与理解,以下仅贴出核心源码
packages/reactivity/src/reactive.tsexport function reactive(target) { return createReactiveObject( target, // reactive里面的值 false, mutableHandlers, mutableCollectionHandlers, reactiveMap )}function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { // 判断是否已经被代理过了,如果是,则获取缓存中的值,并直接返回 // 我们这里第一次指定,必然是不存在的,所以跳过这个 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 对reactive中的变量进行代理,我们这里的target类型是obejct,targetType为common,所以接下来进入baseHandlers逻辑 // 而baseHandlers从reactive被当做参数传递过来的,实际执行的是mutableHandlers const proxy = new Proxy( target, baseHandlers ) proxyMap.set(target, proxy) return proxy}// reactive中变量类型为object场景下,proxy的监听逻辑会走到这里export const mutableHandlers = { get, set,}
通过源码 我们可以看得出来,使用reactive,内部实际执行的是createReactiveObject,函数就是新建了proxy,并最终返回。
不过要注意一点的是,经过reactive处理过的对象,都会以target为WeakMap键,proxy为值,进行一次缓存,这样同一个值再次进行reactive的时候就会读取缓存中的值。
接下来,让我们进入初始化阶段的mutableHandlers,也就是proxy中核心的get set函数,看看内部做了些什么。
初始化读取(get)
当触发obj.name的读取行为的时候,就会触发代理对象的get函数
packages/reactivity/src/baseHandlers.tsconst get = createGetter()function createGetter() { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver) // 读取被代理对象 // 核心逻辑(track):依赖收集,后续单独看 // 如果当前值是reactive则递归proxy处理 if (isObject(res)) { return reactive(res) } return res }}
get内部的逻辑很简单,通过Reflect完成被代理对象的读取操作。
如果被读取对象的属性是object则会再次进入reactive逻辑中进行proxy处理,确保嵌套对象的响应式。
也许有的人会说了proxy不是自身就实现了对象的拦截了吗?为什么我们还是要递归处理嵌套obj呢?
这里我给大家解释一下,proxy确实会拦截到所有操作,但是他也只能拦截当前层级的。
如果没有递归处理, obj.name.abc = 123的时候,只会触发obj.name的get事件,但是不会触发obj.name.abc的set事件。
初始化修改(set)
当触发obj.name
的修改行为,将会触发代理对象的set函数
packages/reactivity/src/baseHandlers.tsconst set = createSetter()function createSetter(shallow = false) { return function set(target, key, value, receiver) { // 修改被代理数据,完成数据更新 const res = Reflect.set(target, key, value, receiver) // 核心逻辑(trigger):依赖触发,后续单独看 return res // true }}
通过Reflect完成被代理对象值的更新,最后返回本次Reflect.set的结果,完成逻辑。
总体就是对proxy的简单利用,还是很简单的嘛
小结
以上代码是去除所有边界判断,以及响应式逻辑后,reactive的核心代码;我们可以发现,其实就是proxy + Reflect的基础使用。
目前数据已经具备响应式,但是数据变化后,引用数据的effect如何实现自动执行呢?接下来我们就去看看effect初始化的时候究竟做了什么。
effect初始化
读取 - 依赖收集(track)
我们回到测试demo中,根据我们使用vue3的预期,在初始化完成后,effect会触发一次,若干时间后,setTimeout内set触发,依赖obj.name
的 effect的函数还会被触发一次,这又是如何实现的呢?
这里我要提到Vue3中第一个非常非常非常重要的概念,依赖收集(track),整个reactivity都利用到了这个概念。
接下来,我们就要通过源码去了解,effect的初始化的时候,到底发生了什么,Vue3在此阶段是如何完成依赖收集的。
packages/reactivity/src/effect.ts/** * 当前被执行的effect */export let activeEffect: ReactiveEffect | undefinedexport function effect(fn) { const _effect = new ReactiveEffect(fn) // 首先执行new ReactiveEffect,所以我们跳转到ReactiveEffect中 _effect.run() // 并立刻执行了run方法,run方法内实际执行的就是effect内部函数}export class ReactiveEffect { parent: ReactiveEffect | undefined = undefined constructor( public fn: () => T, // 这里的fn就是effect内部的匿名函数 ) {} run() { try { activeEffect = this // 将effect对象,也就是new ReactiveEffect的结果,保存到activeEffect shouldTrack = true // 表示开始依赖收集 return this.fn() // 这里的fn,实际上就是effect内部的匿名函数 } }}
vue3的依赖收集几乎都是通过ReactiveEffect进行完成的,简单来说就是ReactiveEffect.run一旦运行后,就会将当前正在运行的匿名函数保存到内存中,以便于proxy get事件触发的时候,收集保存在内存中的匿名函数,进而完成依赖收集。
effect方法内部,首先new ReactiveEffect 最终执行了一次fn,但是在执行之前,将activeEffect赋值为this,将自身保存到了公共变量activeEffect之中。
让我们来看看此时运行的fn是什么
() => { document.querySelector('#app').innerText = obj.name}
匿名函数的内部读取了obj.name,触发了被代理对象obj的get方法.
所以接下来我们回到get方法中,查看之前忽略的依赖收集逻辑。
packages/reactivity/src/baseHandlers.tsfunction createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { const res = Reflect.get(target, key, receiver) // 读取被代理对象 if (!isReadonly) { // obj为可读代码 所以isReadony一定为false 进入if中 track(target, TrackOpTypes.GET, key) } return res }}// 依赖收集function track(target, type, key) { if (shouldTrack && activeEffect) { // 在effect中执行run方法的时候,我们确保了shouldTrack为true activeEffect 存在值,所以进入判断 let depsMap = targetMap.get(target) // targetMap是一个全局变量,实际上是一个new WeakMap 首次depsMap肯定是不存在的 if (!depsMap) { // 这里的target为被代理对象,{name: '张三'},该值做为key,Map作为value targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) // 当前key为name 首次也是不存在的 if (!dep) { // depsMap是一个Map结构,key是name value是createDep()的返回值,我们进入createDep depsMap.set(key, (dep = createDep())) } // 将dep作为参数传递到trackEffects中,此时的dep为Set trackEffects(dep, undefined) }}export const createDep = (effects?) => { const dep = new Set(effects) // 实际上就是生成了Set结构(Set我们简单理解为元素不可重复的数组) dep.w = 0 dep.n = 0 return dep}export function trackEffects( dep: Dep,) { // 一系列边界判断,合法的情况下shouldTrack为true if (shouldTrack) { dep.add(activeEffect!) // 将全局变量activeEffect(包含effect的匿名函数)加入到dep(Set)中 // 到这里 我们将响应式数据与effect函数建立起了联系 标志着我们完成了依赖收集 }}
effect内部的fn被触发,fn执行中触发了obj的get,get内部触发了依赖收集(track),track内部通过构建targetMap,来维护变量与effect之间的关系,进而实现所谓的依赖收集。
我们来梳理一下他的数据结构
-
WeakMap
-
key:被代理对象({name:'张三'})
-
value:Map对象
-
key:响应式对象的指定属性(name)
-
value:指定对象的指定属性的使用函数(effect的匿名函数)
-
-
在WeakMap中,我们不仅仅收集了effect的匿名函数,还将effect与effect中具体读取的变量建立起了联系。
在未来的依赖触发逻辑中,weakMap将会发挥巨大作用。
到此为止,effect内的匿名函数执行完毕,同时我们也完成了重要的依赖收集。
修改 - 依赖触发(trigger)
继续回到demo中,2s后,obj.name赋值为狂飙强,此时的现象是effect中的函数自动执行了,这又是如何实现的呢?
此处首先一定是触发了代理对象obj.name的set,所以我们由此处开始分析。
packages/reactivity/src/baseHandlers.tsfunction createSetter(shallow = false) { return function set(target, key, value, receiver): boolean { const result = Reflect.set(target, key, value, receiver) // 完成被代理对象的赋值操作 trigger(target, TriggerOpTypes.SET, key, value, oldValue) return result } export function trigger(target, type, key?, newValue?, oldValue?, oldTarget?) { // 通过全局变量targetMap(weakMap)获取value // 在依赖收集阶段我们收集到了当前target,所以这时候 depsMap存在值 值为Map Map的key为name 值为Set Set内部是effect的fn const depsMap = targetMap.get(target) triggerEffects(depsMap.get(key))} export function triggerEffects(dep, debuggerEventExtraInfo?) { const effects = isArray(dep) ? dep : [...dep] // 将set处理为数组 for (const effect of effects) { triggerEffect(effect, debuggerEventExtraInfo) }} function triggerEffect( effect: ReactiveEffect, // 每一个effect都是ReactiveEffect,内部的fn都是effect的fn) { // 此时的activeEffect为undefined,一定进入if中 if (effect !== activeEffect || effect.allowRecurse) { effect.run() // effect的run方法就是effect的fn,完成执行 }}
经过以上代码,我们可以了解到,obj.name的改变在触发了proxy的set方法的同时,也触发了依赖触发(trigger)。
trigger中,我们首先通过**{name: '狂飙强'},找到了Map**,再通过name找到Set,最终找到对应的effect的fn,并进行匿名函数的执行,于是我们便看到了effect函数自动触发。
到此为止完成了整个响应式过程。
reactive源码总结
我们简单总结一下,reactive中依赖收集与依赖触发的过程
-
通过proxy处理reactive包裹的对象,被返回proxy代理对象
-
effect初始化,生成了类ReactiveEffect,并执行了其run方法
-
run方法执行后,当前effect的fn函数本身被保存到了activeEffect(公共变量),随后执行了effect的fn
-
effect的fn触发,函数内使用到了obj.name,触发了代理对象的get
-
get方法内部触发了依赖收集(track),配合保存到局部的activeEffect,最终通过WeakMap,建立了effect的fn与当前get的属性的联系,完成了依赖收集。
-
若干时间后,obj.name = '狂飙强',触发proxy的set,同时触发了依赖触发(trigger)
-
trigger内部通过当前代理对象以及具体修改的属性,在依赖收集阶段保存的WeakMap中,找到所有需要触发的effect的fn。
-
触发effect的fn函数,完成响应式。
最后反映在我们眼前,就是obj.name改变的同时,所有使用到obj.name的effet都被自动触发其匿名函数,完成响应式。
关于vue3 reactive的面试题
为什么Vue3的响应式使用WeakMap实现?
还记得我们前一篇文章谈到的WeakMap吗,一旦被代理对象被置为null,weakMap中该key将会被垃圾回收,达到性能最大化的目的
简述Vue3的响应式的核心实现逻辑?
通过proxy递归代理对象,然后在get中完成依赖收集,在set中完成依赖触发
Vue3的reactive为什么不能代理简单类型?
reactive底层依赖proxy,但是proxy只能代理对象,无法代理基础类型。
为什么reactive解构会失去响应式?
这里要明确一点,只有解构出来的变量是基础类型的时候,才会失去响应式,失去响应式的主要原因是基础类型无法被proxy代理。
小结
到此为止,我们的vue3中的响应式模块的第一个API,reactive源码解读就完成了;
总的来说逻辑还是比较复杂的,尽管我已经很努力的去反复修改与简化,但是还是能可以感觉到,有些东西很难用文字讲清楚。
也不知道是否可以帮助到正在阅读文章的你,如果你觉得还不错的话,还麻烦你动动小手点个赞,关注专栏,这是我输出优质文章最大的动力。
如果有小伙伴存在视频教程诉求的话,请评论区告诉我,我会评估出几期视频的必要性~
下一站,我们将前往ref。
ref源码解析
逻辑图
因为ref既可以传入基础类型,也可以传入复杂类型,所以其总体实现逻辑要比reactive更加复杂,并且依赖reactive。
前置知识
如果关于class get set已经很了解,请跳过前置知识
为了降低大家理解ref源码的难度,我们在正式阅读源码之前,先学习一下JavaScript的 class以及修饰符get set相关知识点
class Obj { _value = '张三' get value() { console.log('value的get行为触发') return this._value } set value(val) { console.log('value的set行为触发', val) this._value = val }}let obj = new Obj()
get: 被get修饰的方法,允许通过属性读取的方式,触发方法
set: 被set修饰的方法,允许通过属性赋值的方式,触发方法
当访问obj.value
的时候,会执行被get修饰的value(),打印log,并得到返回值**‘张三’**
当我们执行obj.value = ’李四‘
,进行赋值的时候,将会执行被set修饰的**value()**方法,打印log,并完成变量_value的赋值
看到这里,大家是否有点似曾相识的感觉,访问与赋值触发get set,和proxy代理的对象的get set很相似,大家能理解到这一点就足够了。
因为ref可以代理简单类型,同时也可以代理复杂类型,并且这两种情况下的响应式实现逻辑是完全不同的。
所以接下来,我们从这两个角度分别解读ref的源码实现,以及其核心逻辑。
首先我们看相对简单的基础类型场景,从源码的角度去了解ref是如何实现响应式的。
基础类型场景
案例
let { ref, effect } = Vueconst name = ref('卖鱼强')effect(() => { document.querySelector('#app').innerText = name.value})setTimeout(() => { name.value = '狂飙强'}, 2000)
上述代码现象:
-
页面初始化的时候显示“卖鱼强”
-
2s之后,name发生改变,变成了“狂飙强”。
通过现象与我们之前分析reactive的经验,这个我们可以将ref的实现分为三大模块
-
初始化
-
读取(依赖收集)
-
赋值(依赖触发)
初始化
packages/reactivity/src/ref.tsexport function ref(value?: unknown) { // ref 实际上就是createRef return createRef(value, false)}function createRef(rawValue: unknown, shallow: boolean) { // 如果已经是ref,则直接返回 if (isRef(rawValue)) { return rawValue } // ref API 参数shallow 为 false 含义是 代理是否是浅层的,浅层则只会代理第一层数据 // ref 就是RefImpl的实例 return new RefImpl(rawValue, shallow)}class RefImpl<T> { private _value: T // 被代理对象 private _rawValue: T // 原始对象 public dep?: Dep = undefined // Dep是reative阶段声明的Set, 内部存放的是ReactiveEffect public readonly __v_isRef = true // 将RefImpl实例默认为true, 未来的isRef判断就一定为true constructor(value: T, public readonly __v_isShallow: boolean) { // 寻找原始类型,如果是基础类型不会做任何处理 this._rawValue = toRaw(value) // 如果value是基础类型,toReactive内部不会做任何处理 this._value = toReactive(value) } get value() { return this._value } set value(newVal) { newVal = toRaw(newVal) // 判断新旧值是否一致,不一致进入if if (hasChanged(newVal, this._rawValue)) { // 每次value的值发生修改的时候,都保存一下原始对象 this._rawValue = newVal // 如果value是基础类型 toReactive不会做任何处理 // 如果value是复杂类型,则重新进行proxy处理 this._value = toReactive(newVal) // 依赖触发,后面单独说 } }}
通过源码分析,我们可以发现,ref的本质就是new RefImpl
我们ref传入的参数 原始对象被保存到_rawValue,同时将参数(“卖鱼强”)保存到-value中,便于后续的get set
读取
调用name.value
的时候,会触发RefImpl的get value(),方法内部返回最新的_value,完成读取。
get value() { // trackRefValue(this) // 依赖收集,后面单独说 return this._value}
赋值
name.value
发生赋值的时候,会触发RefImpl的**set value()**方法,方法内部进行_value的赋值,完成数据更新。
set value(newVal) { // 判断新旧值是否一致,不一致进入if if (hasChanged(newVal, this._rawValue)) { // 如果value是基础类型 toReactive不会做任何处理 this._value = toReactive(newVal) // triggerRefValue(this)// 依赖触发,后面单独说 }}
到此为止,ref的基础逻辑就完成,我们已经具备给ref赋值、读取的能力。
但是还不具备响应式的能力,接下来就让我们看看,ref的响应式系统是如何实现的。
依赖收集(trackRefValue)
根据我们解读reactive的源码经验,我们可以猜到,ref一定是在get中完成依赖收集的,事实也是如此。
而第一次ref的get是何时触发的呢?
答案是初始化时期的effect,effect触发后,内部fn被保存到activeEffect中,并触发fn,fn访问了name.value
,触发了ref的get行为,所以接下来我们前往RefImpl的get中,看看ref是如何完成依赖收集的。
get value() { // 依赖收集函数 将当前RefImpl实例传入方法 trackRefValue(this) return this._value}export function trackRefValue(ref) { // shouldTrack一定为true,activeEffect在effect执行阶段保存了fn,所以一定存在 if (shouldTrack && activeEffect) { // createDep我们在reactive中见过,含义为创建一个Set // 所以这个实际函数是给RefImpl实例的dep赋值为Set,然后在传入trackEffects方法 trackEffects(ref.dep || (ref.dep = createDep())) }}export function trackEffects(dep: Dep,) { // 将当前activeEffect,也就是effect的fn,保存到当前RefImpl实例的dep中,effect成功被ref依赖收集到实例的dep中 dep.add(activeEffect)}
通过以上源码,我们可以发现,他们都公用了activeEffect部分的逻辑,但是ref收集依赖的方式与reactive是存在一些差别的
-
reactive的依赖收集通过WeakMap完成,实现属性、变量与effect fn的绑定关系
-
ref则通过自身实例内部的dep变量来保存所有相关的effect fn
依赖触发(triggerRefValue)
若干时间后,name.value
的值被修改,触发RefImpl的set value
set value(newVal) { // 判断传入值是否与原始值不一致 if (hasChanged(newVal, this._rawValue)) { // 完成赋值 this._value = toReactive(newVal) // 依赖触发 triggerRefValue(this) }}export function triggerRefValue(ref: RefBase<any>) { if (ref.dep) { // dep为依赖收集阶段收集到的依赖,内部为effect的fn triggerEffects(ref.dep) }}export function triggerEffects(dep: Dep) { const effects = isArray(dep) ? dep : [...dep] // 转为数组 for (const effect of effects) { // 进入依赖触发函数 triggerEffect(effect) }}function triggerEffect(effect: ReactiveEffect) { // 依次通过run触发被收集的effect的fn,至此完成依赖触发工作 effect.run()}
依赖触发的逻辑就非常简单了,set value的同时,获取当前ref的dep,并遍历dep中的依赖,依次执行,完成依赖触发。
小结
到此为止,我们基础类型场景的ref源码解读就结束了,我们简单做一下总结,
相比较于reactive,该场景下的逻辑要稍微简单一点,相关依赖**(effect fn)被实例本身的dep管理,没有构建复杂的WeakMap**对象。
ref与reactive的收集与触发的逻辑也不相同
-
ref实际上是一个class RefImpl的实例
-
数据响应并不是通过proxy实现,而是通过class 的get set修饰符实现
-
依赖收集、触发并不是通过WeakMap实现,而是通过RefImpl实例中的变量dep实现
复杂类型场景
大家都知道ref不仅可以实现基础类型的响应式,还可以实现复杂类型的响应式,我们可以说ref是reactive的超集,那ref是如何实现既支持基础类型也支持复杂类型的呢?
接下来就让我们看看复杂类型场景下的ref是如何完成响应式的吧。
案例
let { ref, effect } = Vueconst obj = ref({ name: '卖鱼强'})effect(() => { document.querySelector('#app').innerText = obj.value.name})setTimeout(() => { obj.value.name = '狂飙强'}, 4000)
Ref初始化
首先依旧是进入ref函数中,开始new RefImpl,前面流程完全一致,所以直接我们进入RefImpl内部
class RefImpl<T> { private _value: T // 被代理对象 private _rawValue: T public dep?: Dep = undefined // Dep是reative阶段声明的Set,内部存放的是ReactiveEffect public readonly __v_isRef = true // 将RefImpl的实例全部置为true,下次isRef判断就会为true constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = toRaw(value) // toRaw 获取原始数据 this._value = toReactive(value) // 跳转到toReactive函数中 并且最终会获取到一个proxy对象 } get value() {} set value(newVal) {}}export const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value // value为object,进入reactive(value)逻辑 最终返回一个proxy的对象
在constructor逻辑中,我们可以看到this._value = toReactive(value),而toReactive函数中,会首先识别value类型,如果不是object,原路返回,如果是object,将会被reactive函数处理,所以在该场景下,value将被reactive函数处理成proxy对象。
也就是说,此时ref内部的**_value实际上成了reactive**类型。
读取
初始化阶段,effect触发的时候,将会读取obj.value.name,,首先会访问量obj.value,触发ref的get方法。
obj.value获取完成后,继续去获取obj.value.name,而name已经在初始化阶段,被toReactive处理成了proxy,所以接下来,会再触发reactive的get,来获取name
也就是说,读取阶段,实际上触发了2次get,一次是ref的get value,一次是proxy的get,进而完成了变量的读取。
get value() { // trackRefValue(this) // 依赖收集,后面单独说 return this._value // 获取到proxy类型的{name: '张三'},进而再次触发proxy的get方法}
赋值
若干时间后,obj.value.name发生set行为,首先依旧会触发ref的get,获取obj.value
,然后再触发reactive的set方法,完成name的赋值。
整个赋值过程,实际上分别触发了ref的get value,和proxy的set,进而完成变量的赋值
//ref 本身的set在value为object,并且没有直接修改ref.value的情况下,不会被触发set value(newVal) {}
到此为止,我们了解了ref在处理复杂对象时候的读取与赋值的逻辑。
读取:先触发ref的get,再触发proxy的get
赋值:先触发ref的get,再触发proxy的set
依赖收集
依赖收集是在get阶段进行完成,而通过上面的分析我们可以了解到,ref的get实际上其内部是两次get事件,所以我们分开来看。
ref的依赖收集(trackRefValue)
effect初始化阶段执行的时候,会读取obj.value.name
,首先会触发ref的get方法
get value() { // 依赖收集函数 将当前ref本身传入方法 trackRefValue(this) return this._value}
ref的get方法触发了trackRefValue,会在当前ref的dep中收集到effect,此处逻辑与ref为基础类型的逻辑一致。
proxy的依赖收集(track)
ref的的get完成后,紧接着触发了reactive的get,然后get内部通过WeakMap再次完成依赖收集
我们会发现,在该阶段,我们内部实际上触发了2次依赖收集,effect fn被ref收集的同时,也被proxy收集了。
依赖触发
因为ref内部是一个对象,所以赋值也存在多种方式,这依赖触发存在多种方式
对象属性触发依赖
obj.value.name = '狂飙强'
这种不会破坏RefImpl初始化阶段其内部构建的proxy,仅修改已有proxy内部变量的值。
首先触发的是obj.value的get行为(此时没有effet在执行,不会发生依赖收集)。然后ref的get函数返回proxy对象 {name:'卖鱼强'}
,紧接着触发proxy的set,并完成依赖触发
对象触发依赖
obj.value = { name: '狂飙强'}
第二种方式首先触发obj.value的set行为,同时替换掉ref的值,注意这会破坏RefImpl初始化构建的_value的proxy,进而导致WeakMap中已有的依赖关系断裂
然后执行triggerRefValue,触发,ref本身在get阶段收集了相关effect fn,。
effect fn被触发后,再次触发ref的get,proxy的get,并帮助proxy又重建了与effect fn之间的依赖关系。
这就是为什么存在依赖收集2次的原因。
到此为止,我们的ref核心源码分析就全部完毕了。
关于ref的一些问题
Q:为啥一定要.value,不能干掉吗?
A:非常遗憾,value是去不掉的,因为ref依赖class get set 进行实现,在当前实现的场景下,可以简写为v,但是无法去除
Q:我是不是可以完全使用ref,不用reactive?
A:是的,可以完全使用ref,因为ref会根据你传入的类型,自动识别内部是否需要使用reactive,但是读过源码的同学知道ref在处理响应式系统中,存在重复收集依赖的场景,如果你有极致的性能要求,建议复杂类型依旧使用reactive完成,业务开发场景则无所谓。
转载整合文章, 拱个人学习使用
原文地址: juejin.cn/post/721291…