30分钟手写一遍源码带你搞明白vue3中的reactive和ref的本质区别

很多同学用vue3写业务溜的飞飞起,但是又始终不想打开vue源码仓库 vuejs/core 一探究竟,然而面试官们又很喜欢问你各种各样的vue的源码实现思想的问题,这让你很难受。好吧,没关系,接下来这篇文章将花费你30分钟的时间,带你一行一行的手写一份vue3中的 reactivity 源码,并成功运行起来的那一刻希望你能有所收获

文章中的代码皆来自vue3的源码,或存在轻微变动,只为更好理解,尽量做到每一行都用大白话注解,让新手阅读也无压力。 文中的代码 地址在这里

1. 准备工作

创建一个 vue 项目(以下vue3统称vue)

lua 复制代码
yarn create vite mini-vue --template vue

(当然这是非必须的,也可以随意创建一个文件夹,我创建它是为了测试用)然后删除src下的一些非必要的目录,再创建一个reactivity文件夹

为了方便理解,我们不使用ts,并且用取其精华去其一部分精华的的方式,接下来只写核心代码。附带将另一个API shallowReactive(浅层代理,只代理最多外层对象) 一起实现

2. 从定义 reactive 函数开始

src >> reactivity >> reactive.js

哇,一上来就一整页的代码,还骗我说是一行一行敲!哈哈哈哈,别慌,代码我是一行一行敲的,你也可以跟着一行行敲完,每一行我都写上了注解。或者,直接点开上面的 地址 拉代码下来也行

以上代码实现的主要功能:

  1. 定义了reactive 和 shallowReactive 函数。 vue中的 shallowReactive 实现的是浅层代理的效果,这里也附带实现一下
  2. 将接收到的参数用 proxy 代理
  3. 被代理过的对象用 WeakMap 缓存起来,目的是防止代理过的对象再次被代理
  4. proxy对对象的代理函数,写在了同级目录下的 baseHandlers.js中

3. 被 proxy 代理后对象的行为

src >> reactivity >> baseHandlers.js

proxy可以代理对象的13种行为,为了阅读负担这里我们只写4种

  1. 读取对象值的代理函数 get
  2. 修改对象值的代理函数 set
  3. 判断对象中是否存在属性的 in 方法的代理函数 has
  4. 删除对象中的属性 delete obj.x 的代理函数 deleteProperty

搞明白对象增删改查这4种代理写法,其它的差不多雷同

4. 用到的两个工具函数

src >> shared >> index.js

5. effect 副作用函数

(如果告诉你vue2中称这个步骤思想称之为依赖收集&依赖触发,这样你是否好理解一点)

src >> reactivity >> effect.js

步骤3当中代理的get,set,has,deleteProperty 行为中,分别都用到了 track 和 trigger 这两个函数

这段代码中定义了三个函数, track,trigger 和 effect,解释一下:

  1. track 用来为每个响应式属性添加副作用函数
  2. trigger 修改某个响应式属性时,触发它身上的副作用
  3. effect 是vue中设计的API,就是一个副作用函数,computed,watch中都会用到它,要理解它,我们看下面的例子

6. 测试代码

这里我们直接用一个html来测试一下 src >> reactivity >> test.html (用live-server跑一下)

浏览器控制台看到的效果是这样的

这里可以清楚的看到 effect 为什么叫做副作用函数,当响应式数据发生变更时,effect 中的回调会实时执行

这是怎么做到的呢?解释一下:

  1. state.age++ 涉及到了两个操作, 先读取state.age, 再执行 state.age = state.age + 1
  2. 那么,在读取 state.age 时,执行了proxy中的get代理函数,get中执行 track函数,为age属性添加了一个副作用函数,存储的状态长这样
arduino 复制代码
 // 存储的结构是这样的
 targetMap = {  // targetMap 是一个WeakMap对象
    state: {  // 直接将整个响应式对象作为key,value值为 Map 对象
      age: [effectFn]  // 读取的 age 属性作为Map中的key, 值为一个Set对象,这里用数组模拟了一下
    }
  }
  
  // 回到上面的effect.js 中再看一眼 effect函数,你会发现这里的 effectFn 是在effect函数中定义的,effectFn内部调用的就是测试代码中 effect 的回调 
  // () => {
  //   console.log(`${state.name}今年${state.age}岁了`);
  // }

这就一起解释了 effect.js 中的 track 是如何为某个属性添加副作用的

  1. 在执行 state.age = state.age + 1 中的赋值语句时,触发了 proxy 代理的 set 函数,set 函数中调用的 trigger(state, 'set', age) 方法,这里直接上帝视角传入了实参为了让看官更好接受,trigger 干的操作就是 读取到曾为 age 属性添加的副作用函数,并执行循环执行掉

所以,你现在应该明白了为什么在vue中,一旦响应式数据变更,watch,computed,effect 等一系列的函数中的回调都会重新触发。 (注:watch,computed等响应式API内部都用到了effect来实现)

7. 再测一下 effect 中的 scheduler

在 test.html 的 effect 中添加以下代码

再看一眼执行结果

所以 scheduler 调度器的作用其实是当某一个属性值变更导致它身上的副作用执行后,再隐式的去执行其它任务的一个功能。这和 scheduler 在 watch API中最好理解

我们假设 watch 就是我们写的effect函数,watch的效果是,第一个回调执行后,第二个回调会自动执行,哇,那不就是我们的 scheduler 在工作嘛!!!

8. ref 的原理

src >> reactivity >> ref.js

当你明白了 reactive 的原理后,ref 源码看起来就简单多了,在这份代码中主要有以下功能

  1. ref 函数执行得到的结果是一个 RefImpl 对象
  2. ref 借助 reactive 来处理对象类型的数据
  3. 在 RefImpl 类中,原始类型借助面向对象的 getter 和 setter 函数来实现读取和修改 _val 的值
  4. 读取 ref 返回的数据时,通过 track 为属性添加副作用函数
  5. 修改 ref 返回的数据时,通过 trigger 执行掉该属性身上的副作用函数

你必须要知道的是对象的 getter 和 setter 的用法:

kotlin 复制代码
const obj = {
  a: 1,
  get b() {
    return this.a
  },
  set c(val) {
    this.a = val
  }
}
obj.b        // 1  
obj.c = 10
obj.b        // 10

// 函数前面有get或者set, 函数当做属性来用

这个你明白的话,那 ref 的实现过程你一定看的懂,所以这也解释了很多同学心中的疑惑,为什么用 ref 将数据变成响应式,每次取用要写 xxx.value

到这里,你可能还会想问一个问题,既然都用到了 reactive,vue中为什么要有ref,只要一个 reactive 不就够了嘛?

其实你转念一想就能明白, 如果一个组件中的数据量很少,或者说数据都是原始类型的,用 ref 这种原生的getter,setter,明显比reactive 里面的递归代理性能更高呀

9. 同样测试一下 ref

src >> reactivity >> testRef.html

看到的效果是:

当 ref 返回值变更时,同样会触发 effect 的回调,完美!!!

10. 总结

面试官:说说 reactive 和 ref 的区别

明白原理后,就只剩每个人的表达方式不一样而已了,再被问到这个问题时,希望你能对答如流

我用自己的话术总结一下(只是一个参考,我想你会有自己的表达):

  1. reactive 使用 proxy 代理了对象的各种操作行为,在属性 读取值,判断 等行为中,为属性添加副作用函数,在属性被 修改值,删除等行为中,触发该属性身上绑定的副作用函数来实现响应式效果
  2. ref 中,当参数是对象时,借助reactive 代理来实现响应式,当参数是原始值时,给值添加 value 函数并采用原生的 getter和setter方式,实现为属性添加副作用函数和触发副作用函数的能力,进而达到响应式的效果
相关推荐
zhougl99640 分钟前
html处理Base文件流
linux·前端·html
花花鱼44 分钟前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!5 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
Alo3656 小时前
面试考点复盘(二)
面试