面试官:请你讲解一下ref和reactive的区别?我:!?!?

前言

想必用过前端框架vue3的大家都应该接触过ref和reactive,但是当面试官问我们关于这二者的区别的时候,我是肯定答不上来的,于是我决定深入了解一下这个问题。以防万一,被面试官被刺!

二者浅显的区别

reactive只能将引用类型代理成响应式,而ref可以将引用类型和基本类型都改成响应式。

例如

const state = reactive({
    name: "Hello World!"
})

const state1 = ref('Hello World!')
const state2 = ref({
    name: 'Hello World!'
})

console.log(state.name) // "Hello World!"
console.log(state1.value) // "Hello World!"
console.log(state2.value.name) // "Hello World!"

他们三个的输出结果都是一样的,但想必大家都看出来的了他们的调用方式不一样,这是为什么呢?
下面就让我来带大家了解一下,在vue3中是怎么实现这个效果的就可以了。

reactive 实现原理

总的来说就一句话:被代理对象中的任意属性发生修改,都应该将用到了这个属性的各个函数(副作用函数)重新执行一遍,那么在此执行之前,就需要先为每一个属性都做好副作用函数的收集(依赖收集)

现在我们把它们拆开来分析

看以下代码分析:

代码1.1
import { mutableHandlers } from './baseHandlers.js'

// 保存被代理过的对象
export const reactiveMap = new WeakMap() // new Map() // new WeakMap 对内存的回收更加友好


export function reactive(target) { // 将target变成响应式
  return createReactiveObject(target, reactiveMap, mutableHandlers)
}


export function createReactiveObject(target, proxyMap, proxyHandlers) { // 创建响应式的函数
  // 判断target是不是一个引用类型
  if (typeof target !== 'object' || target === null) {  // 不是对象就不给操作
    return target
  }

  // 该对象是否已经被代理过(已经是响应式对象)
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 执行代理操作(将target处理成响应式)
  const proxy = new Proxy(target, proxyHandlers) // 第二个参数的作用:当target被读取值,设置值,判断值等等操作时会触发的函数

  // 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
  proxyMap.set(target, proxy)
  
  return proxy
}

详细的解析上面都有了,但是看起来估计还是有一点懵的,那就让我们先来了解一下什么是代理(proxy)

new Proxy(target, handler) //target是要代理的对象,handler是对这个对象进行的操作

用在上面的地24行中,我们创建了一个对象的代理,这个对象就是传入进来的target值(24行上面的就是个类型判断和是否已经代理过的判断,不是核心代码,随便看下就好),proxyHandlers就是要处理的操作,上面采用了模块化的引用,真正的操作函数被封装到了另一个js文件当中。主要的代码如下:

代码1.2
import { track, trigger } from './effect.js'

const get = createGetter()
const set = createSetter()

function createGetter() {
  return function get(target, key, receiver) {
    // console.log('target对象被读取值了');
    const res = Reflect.get(target, key, receiver)  // target[key]
    // 这个属性究竟还有哪些地方用到了(副作用函数的收集, computed, watch,....)  依赖收集
    track(target, key)

    return res
  } 
}

function createSetter() {
  return function set(target, key, value, receiver) {
    // console.log('target对象被修改值了', key, value);
    const res = Reflect.set(target, key, value, receiver)  // target[key] = value;
    // 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效, 更新浏览器的视图(响应式)
    // 触发被修改的属性身上的副作用函数   触发依赖(被修改的key再哪些地方被用到了)发布订阅
    trigger(target, key)

    return res
    
  }
}


export const mutableHandlers = {
  get,
  set,
} // 将上面的getset函数封装好导出

这就是代理对象的处理函数,到这里其实就已经有一些明了了,reactive的实现就是先将你要处理的对象代理出来,然后利用proxy的方法,对它进行处理(get函数和set函数分别是当你读取到被代理的对象是和你修改被代理的对象的会触发的回调函数,是proxy本身具有的方法,详细可以看MDN里的介绍地址如下:developer.mozilla.org/zh-CN/docs/… )到这里剩下的问题就是,怎么让被代理对象可以对一些方法产生不一样的效果。请看一下代码:

代码1.3
const targetMap = new WeakMap()
let activeEffect = null  // 得是一个副作用函数

export function effect(fn, options={}) { // 也是watch,computed 的核心逻辑
  const effectFn = () => {
    try {
      activeEffect = effectFn
      return fn()
    } finally {
      activeEffect = null
    }
  }
  if (!options.lazy) {
    effectFn()
  }
  effectFn.scheduler = options.scheduler
  return effectFn
}


// 为某个属性添加 effect
export function track(target, key) {
  // targetMap = {  // 存成这样
  //   target: {
  //     key: [effect1, effect2, effect3,...]
  //   },
  //   target2: {
  //     key: [effect1, effect2, effect3,...]
  //   }
  // }

  let depsMap = targetMap.get(target)
  if (!depsMap) { // 初次读取到值 收集effect
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) { // 该属性还未添加过effect
    deps = new Set()
  }
  if (!deps.has(activeEffect) && activeEffect) {
    // 存入一个effect函数
    deps.add(activeEffect)
  }
  depsMap.set(key, deps)

}

// 触发某个属性 effect
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) { // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
    return
  }

  const deps = depsMap.get(key)
  if (!deps) { // 这个属性没有依赖
    return
  }

  deps.forEach(effectFn => {
    if (effectFn.scheduler) {
      effectFn.scheduler()
    } else {
      effectFn() // 将该属性上的所有的副作用函数全部触发
    }
    
  });
}

在这个js文件里面封装了三个函数,分别是effect,track,trigger,我们依次来看看,

  • effect: 回调函数,为被代理的对象加入依赖于它的处理函数
  • track:为每一个被代理的对象加入依赖它的函数(effect)
  • trigger:触发特定属性的effect

reactive总结

总的来说reactive的处理步骤如下:

  1. 用Proxy代理了对象
  2. 在代理函数 get 中 对使用了的属性做 副作用函数收集
  3. 在代理函数 set 中 对修改了的属性做 副作用函数的触发

看到这里估计还有一些人不太理解,这是很正常的,所以我来写一个代码运行时的逻辑:

  1. 首先我们取一个对象{a: 1}
  2. 然后调用封装好的reactive函数 const state = reactive({a: 1})
  3. 这个时候就会进入代码片段1.1里面的reactive函数,在进入createReactiveObject函数
  4. 然后就会对我们要代理的对象进行判断,如果是对象的话就继续下去
  5. 在判断我们是不是已经代理过这个对象(就是我们预先空出一个位置,将已经代理过的对象全部存储下来),代理过就返回代理后的值,没有的话就创建一个新的代理
  6. 再然后绑定上proxy函数里面的get和set回调函数(可以看到代码1.2)

然后当我们读取或者改变这个已经代理好的对象的时候会进行以下步骤:

  1. 首先我们进入到effect函数,它可以写入我们要对函数进行的处理
  2. 我们effect函数就是一个回调函数,当它触发时,会将我们写好的处理函数封装到effectfn里面,然后传给activeEffect变量(代码1.3里面有)。
  3. 然后当我们读取到代理的函数时就会调用get函数,里面就有我们写好的track函数,它调用activeEffect变量,把它存到我们代理对象的后续依赖函数里面(可以看看代码1.3)。
  4. 最后当我们改变代理的对象中含有依赖函数的值时,set函数触发,然后里面的trigger函数触发,最后就会触发它的依赖函数。

上面的顺序可以参考以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { reactive } from './reactive.js';
    import { effect } from './effect.js'

    const state = reactive({
      name: 'Hello World!',
      age: 20
    })

    effect(
      () => {
        console.log(`${state.name}今年${state.age}岁了`);
      },
      { 
        lazy: false,
        // scheduler() {
        //   console.log('测试调度任务');
        // }
      }
    )

    
    setInterval(() => {
      state.age = state.age + 1
    }, 2000)

    // state.age++   // 把用到这个值的地方全部重新执行一遍
  </script>
</body>
</html>

ref的原理

ref的处理上就是先将基本类型转换成对象类型,然后在里面加入一个this._v_isRef的布尔类型的值,来判断它是不是被ref处理了,然后将上面写的track和trigger函数写入get和set函数里面,如果你要处理的是一个对象,那么vue3就会把它改成reactive的处理方式。
所以说,ref可以处理基本类型和引用类型,而reactive处理的是引用类型,二者的区别不是很大。

如果你好奇为什么对象也可以触发get和set函数,可以去看看zh.javascript.info/property-ac… 这里面讲解的很好

ref代码构成如下:

import { reactive } from './reactive.js'
import { track, trigger } from './effect.js'


export function ref(val) {  // 将原始类型数据变成响应式 引用类型也可以
  return createRef(val)
}

function createRef(val) {
  // 判断val是否已经是响应式
  if (val.__v_isRef) {
    return val
  }

  // 将val变为响应式
  return new RefImpl(val)
}


 // const age = ref({n: 18})
class RefImpl {  
  constructor(val) {
    this.__v_isRef = true // 给每一个被ref操作过的属性值都添加标记
    this._value = convert(val)
  }

  get value() {
    // 为this对象做依赖收集
    track(this, 'value')
    return this._value
  }

  set value(newVal) {
    // console.log(newVal);
    
    if (newVal !== this._value) {
      this._value = convert(newVal)
      trigger(this, 'value') // 触发掉 'value' 上的所有副作用函数
    }
  }

}

function convert(val) {
  if (typeof val !== 'object' || val === null) {  // 不是对象
    return val
  } else {
    return reactive(val)
  }
}

总结

本人也只是一个前端新手,上面的描述只是我个人浅显的理解,希望可以帮助到大家,写了这么多也花费了我蛮多精力的,再多就不礼貌了(dogo)所以就到这吧!

原文链接:https://juejin.cn/post/7356511140828102665 作者:丁丶知了

(0)
上一篇 2024年4月12日 下午4:37
下一篇 2024年4月12日 下午4:47

相关推荐

发表回复

登录后才能评论