很多同学用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
哇,一上来就一整页的代码,还骗我说是一行一行敲!哈哈哈哈,别慌,代码我是一行一行敲的,你也可以跟着一行行敲完,每一行我都写上了注解。或者,直接点开上面的 地址 拉代码下来也行
以上代码实现的主要功能:
- 定义了reactive 和 shallowReactive 函数。 vue中的 shallowReactive 实现的是浅层代理的效果,这里也附带实现一下
- 将接收到的参数用 proxy 代理
- 被代理过的对象用 WeakMap 缓存起来,目的是防止代理过的对象再次被代理
- proxy对对象的代理函数,写在了同级目录下的 baseHandlers.js中
3. 被 proxy 代理后对象的行为
src >> reactivity >> baseHandlers.js
proxy可以代理对象的13种行为,为了阅读负担这里我们只写4种
- 读取对象值的代理函数
get
- 修改对象值的代理函数
set
- 判断对象中是否存在属性的 in 方法的代理函数
has
- 删除对象中的属性 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,解释一下:
- track 用来为每个响应式属性添加副作用函数
- trigger 修改某个响应式属性时,触发它身上的副作用
- effect 是vue中设计的API,就是一个副作用函数,computed,watch中都会用到它,要理解它,我们看下面的例子
6. 测试代码
这里我们直接用一个html来测试一下 src >> reactivity >> test.html (用live-server跑一下)
浏览器控制台看到的效果是这样的
这里可以清楚的看到 effect 为什么叫做副作用函数,当响应式数据发生变更时,effect 中的回调会实时执行
这是怎么做到的呢?解释一下:
- state.age++ 涉及到了两个操作, 先读取state.age, 再执行 state.age = state.age + 1
- 那么,在读取 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 是如何为某个属性添加副作用的
- 在执行 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 源码看起来就简单多了,在这份代码中主要有以下功能
- ref 函数执行得到的结果是一个 RefImpl 对象
- ref 借助 reactive 来处理对象类型的数据
- 在 RefImpl 类中,原始类型借助面向对象的 getter 和 setter 函数来实现读取和修改 _val 的值
- 读取 ref 返回的数据时,通过 track 为属性添加副作用函数
- 修改 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 的区别
明白原理后,就只剩每个人的表达方式不一样而已了,再被问到这个问题时,希望你能对答如流
我用自己的话术总结一下(只是一个参考,我想你会有自己的表达):
- reactive 使用 proxy 代理了对象的各种操作行为,在属性 读取值,判断 等行为中,为属性添加副作用函数,在属性被 修改值,删除等行为中,触发该属性身上绑定的副作用函数来实现响应式效果
- ref 中,当参数是对象时,借助reactive 代理来实现响应式,当参数是原始值时,给值添加 value 函数并采用原生的 getter和setter方式,实现为属性添加副作用函数和触发副作用函数的能力,进而达到响应式的效果