一文搞清 Vue3中 Ref和Reactive原理

152 阅读28分钟

前言

大部分使用过vue3的同学都知道,vue3的底层的响应式实现由Object.defineProperty更换成了Proxy

为什么vue3要更换呢?proxy相对于前者又有何优势呢?

接下来让我们通过案例去一探究竟吧!

当响应式不存在

我们先看一个例子

let shoes = {  num: 3,  price: 10,}​let total = shoes.num * shoes.priceconsole.log(total) // 30​shoes.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) // 30​shoes.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确实满足了我们上面提到的案例,但是他在一些场景也存在很多问题。

比如大家一定都遇到过的问题

  1. object中新增字段 没有响应性

  2. 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里面还读取了被代理对象datanameage,理想情况应当是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指向了dataProxyReflect很好的解决了以上的this指向问题。

通过以上案例,我们可以看到使用target[key]*

有些情况下是不符预期的,比如案例中的被代理对象this指向问题,而使用*

Reflect则可以更加稳定的解决这些问题,在vue3源码中也确实是这么用的。

补充(WeakMap)

通过以上文章,我们了解到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,这是vue3最核心的api

但是仅仅知道理解proxy+reflect,还不太够,为了尽量轻松的阅读Vue3源码,我们还要学习一个原生API,那就是WeakMap

WeakMap MDN中文文档地址

weakMapmap一样都是key value格式,但是他们还是存在一些差别。

  • weakMapkey必须是对象,并且是弱引用关系

  • Mapkey可以是任何值(基础类型+对象),但是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也自动销毁了

  • 弱引用也解释了为什么weakMapkey不能是基础类型,因为基础类型存在栈内存中,不存在弱引用关系;

在vue3的依赖收集阶段,源码中用到了WeakMap,具体什么作用?我们下一节进行解答。

小节

我们认识到了object.defineproperty相较于proxy的劣势,以及搭配proxy同时出现的Reflect的原因,还有一个Map的原生的APIWeakMap的作用。

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)

以上测试案例,我们涉及到了三个重要的阶段

  1. reactive初始化

  2. effect初始化

  3. 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处理过的对象,都会以targetWeakMap键,proxy为值,进行一次缓存,这样同一个值再次进行reactive的时候就会读取缓存中的值。

img

接下来,让我们进入初始化阶段的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会触发一次,若干时间后,setTimeoutset触发,依赖obj.nameeffect的函数还会被触发一次,这又是如何实现的呢?

这里我要提到Vue3中第一个非常非常非常重要的概念,依赖收集(track),整个reactivity都利用到了这个概念。

接下来,我们就要通过源码去了解,effect的初始化的时候,到底发生了什么,Vue3在此阶段是如何完成依赖收集的。

packages/reactivity/src/effect.ts/** * 当前被执行的effect */export let activeEffect: ReactiveEffect | undefined​export 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执行中触发了objgetget内部触发了依赖收集(track)track内部通过构建targetMap,来维护变量effect之间的关系,进而实现所谓的依赖收集

我们来梳理一下他的数据结构

  • WeakMap

    • key:被代理对象({name:'张三'})

    • value:Map对象

      • key:响应式对象的指定属性(name)

      • value:指定对象的指定属性的使用函数(effect的匿名函数)

WeakMap中,我们不仅仅收集了effect的匿名函数,还将effecteffect中具体读取的变量建立起了联系

在未来的依赖触发逻辑中,weakMap将会发挥巨大作用。

到此为止,effect内的匿名函数执行完毕,同时我们也完成了重要的依赖收集

修改 - 依赖触发(trigger)

继续回到demo中,2s后,obj.name赋值为狂飙强,此时的现象是effect中的函数自动执行了,这又是如何实现的呢?

此处首先一定是触发了代理对象obj.nameset,所以我们由此处开始分析。

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,一定进入ifif (effect !== activeEffect || effect.allowRecurse) {    effect.run() // effect的run方法就是effect的fn,完成执行  }}

经过以上代码,我们可以了解到,obj.name的改变在触发了proxyset方法的同时,也触发了依赖触发(trigger)

trigger中,我们首先通过**{name: '狂飙强'},找到了Map**,再通过name找到Set,最终找到对应的effectfn,并进行匿名函数的执行,于是我们便看到了effect函数自动触发。

到此为止完成了整个响应式过程。

reactive源码总结

我们简单总结一下,reactive中依赖收集依赖触发的过程

  1. 通过proxy处理reactive包裹的对象,被返回proxy代理对象

  2. effect初始化,生成了类ReactiveEffect,并执行了其run方法

  3. run方法执行后,当前effect的fn函数本身被保存到了activeEffect(公共变量),随后执行了effect的fn

  4. effect的fn触发,函数内使用到了obj.name,触发了代理对象的get

  5. get方法内部触发了依赖收集(track),配合保存到局部的activeEffect,最终通过WeakMap,建立了effect的fn与当前get的属性的联系,完成了依赖收集。

  6. 若干时间后,obj.name = '狂飙强',触发proxyset,同时触发了依赖触发(trigger)

  7. trigger内部通过当前代理对象以及具体修改的属性,在依赖收集阶段保存的WeakMap中,找到所有需要触发的effect的fn

  8. 触发effect的fn函数,完成响应式。

最后反映在我们眼前,就是obj.name改变的同时,所有使用到obj.nameeffet都被自动触发其匿名函数,完成响应式。

关于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

img

前置知识

如果关于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)

上述代码现象:

  1. 页面初始化的时候显示“卖鱼强”

  2. 2s之后,name发生改变,变成了“狂飙强”。

通过现象与我们之前分析reactive的经验,这个我们可以将ref的实现分为三大模块

  1. 初始化

  2. 读取(依赖收集)

  3. 赋值(依赖触发)

初始化

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的时候,会触发RefImplget 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中完成依赖收集的,事实也是如此。

而第一次refget是何时触发的呢?

答案是初始化时期的effecteffect触发后,内部fn被保存到activeEffect中,并触发fnfn访问了name.value,触发了refget行为,所以接下来我们前往RefImplget中,看看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的值被修改,触发RefImplset 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为依赖收集阶段收集到的依赖,内部为effectfn    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触发被收集的effectfn,至此完成依赖触发工作  effect.run()}

依赖触发的逻辑就非常简单了,set value的同时,获取当前refdep,并遍历dep中的依赖,依次执行,完成依赖触发。

小结

到此为止,我们基础类型场景的ref源码解读就结束了,我们简单做一下总结,

相比较于reactive,该场景下的逻辑要稍微简单一点,相关依赖**(effect fn)被实例本身的dep管理,没有构建复杂的WeakMap**对象。

refreactive的收集与触发的逻辑也不相同

  • ref实际上是一个class RefImpl的实例

  • 数据响应并不是通过proxy实现,而是通过classget set修饰符实现

  • 依赖收集、触发并不是通过WeakMap实现,而是通过RefImpl实例中的变量dep实现

复杂类型场景

大家都知道ref不仅可以实现基础类型的响应式,还可以实现复杂类型的响应式,我们可以说refreactive的超集,那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,触发refget方法。

obj.value获取完成后,继续去获取obj.value.name,而name已经在初始化阶段,被toReactive处理成了proxy,所以接下来,会再触发reactiveget,来获取name

也就是说,读取阶段,实际上触发了2次get,一次是refget value,一次是proxyget,进而完成了变量的读取。

get value() {  // trackRefValue(this) // 依赖收集,后面单独说  return this._value // 获取到proxy类型的{name: '张三'},进而再次触发proxy的get方法}

赋值

若干时间后,obj.value.name发生set行为,首先依旧会触发refget,获取obj.value,然后再触发reactiveset方法,完成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阶段进行完成,而通过上面的分析我们可以了解到,refget实际上其内部是两次get事件,所以我们分开来看。

ref的依赖收集(trackRefValue)

effect初始化阶段执行的时候,会读取obj.value.name,首先会触发refget方法

get value() {  // 依赖收集函数 将当前ref本身传入方法  trackRefValue(this)  return this._value}

refget方法触发了trackRefValue,会在当前refdep中收集到effect,此处逻辑与ref为基础类型的逻辑一致。

proxy的依赖收集(track)

ref的的get完成后,紧接着触发了reactiveget,然后get内部通过WeakMap再次完成依赖收集

我们会发现,在该阶段,我们内部实际上触发了2次依赖收集effect fnref收集的同时,也被proxy收集了。

依赖触发

因为ref内部是一个对象,所以赋值也存在多种方式,这依赖触发存在多种方式

对象属性触发依赖

obj.value.name = '狂飙强'

这种不会破坏RefImpl初始化阶段其内部构建的proxy,仅修改已有proxy内部变量的值。

首先触发的是obj.valueget行为(此时没有effet在执行,不会发生依赖收集)。然后refget函数返回proxy对象 {name:'卖鱼强'},紧接着触发proxyset,并完成依赖触发

对象触发依赖

obj.value = {  name: '狂飙强'}

第二种方式首先触发obj.valueset行为,同时替换掉ref的值,注意这会破坏RefImpl初始化构建的_value的proxy,进而导致WeakMap中已有的依赖关系断裂

然后执行triggerRefValue,触发,ref本身在get阶段收集了相关effect fn,。

effect fn被触发后,再次触发ref的getproxy的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…