前言
经过之前的探索,基本上弄明白了 Vue3 的响应式的大概内容,所以为了巩固一下,来实现一个简易的 reactivity 响应式。
实现目标
假设存在以下数据
let rawData = 0
let reactData=rawData+5
当修改数据 rawData 的时候,我们想要同步更新数据 reactData,所以我们可以将上面的代码修改为:
function reactive(){
reactData = rawData+5
}
这样,我们修改数据 rawData 的时候调用 reactive()就可以同时得到修改后的 reactData。这其实就是一个最简单的响应式了。
@vue/reactivity
Vue 的 reactivity 模块可以独立出来,这里我们来复习一下用法,并以此为模版手写一个简单的 reactivity
♣ 我们可以直接安装一下 reactivity 模块:npm i @vue/reactivity
const {effect,reactive} =require('@vue/reactivity')
//声明一个响应式对象
let rawData = reactive({
value:1
})
let reactData;
//依赖收集
effect(()=>{
reactData = rawData.value+1
console.log('reactData:',reactData)
})
rawData.value = 4//输出reactData:5
上面我们看到,经过响应式reactive处理后的 rawData,我们在修改 rawData 的数据时会自动更新 reactData 数据。这就是我们想要的最终结果了。 下面我们来理一下思路。
实现自己的 reactivity
-
首先,我们要有收集依赖和触发依赖的方法。
- 收集依赖就是收集跟响应式的数据相关的逻辑。
- 触发依赖就是当我们的响应式数据变动的时候去执行响应式逻辑。
-
其次,我们还需要有个地方去存储收集到的依赖。额外一点就是还需要防止依赖的重复收集。
简易版
现在我们先来一版简易的:
//定义一个全局变量,这个全局变量的目的就是为了让收集依赖方法和dep中的depend联系起来
let currentEffect;
class Dep ={
constructor(val){
//存储我们传入的默认值,即
this._val =val
//用来存储收集到的依赖
this.effects = new Set()//这里因为防止收集到重复的依赖,所以我们要用Set来实现。
}
//取值
get value(){
return this._val
}
//赋值
set value(newVal){
this._val = newVal
}
//依赖收集
depend(){
if(currentEffect){
this.effects.add(currentEffect)
}
}
//触发依赖
notice(){
this.effects.forEach(effect=>effect())
}
}
上面的代码仅仅是实现了依赖的触发和收集,是一个最简单的框架。下面我们来看一下使用:
const rawData =new Dep(5)
function effect(eff){
currentEffect =eff
//直接触发依赖
eff()
//触发依赖收集
rawData.depend()
//清空currentEffect
currentEffect =null
}
let reactData;
effect(()=>{
reactData = rawData.value+5
console.log('reactData:',reactData)
})
rawData.value = 10
rawData.notice()//输出 reactData:15
♣ 这里有个小细节就是,我们通过全局变量currentEffect 来和 reactive 中的depend 依赖收集联系起来,而不是直接调用 reactive 的 depend 方法。这里后面会解释原因。
上面的代码运行后,可以基本实现我们的功能了。这个实现需要我们手动的调用notice 触发依赖。需要我们显示的去进行依赖的收集,这明显不符合我们的要求。所以我们在进行下一步的改装。
进阶版:自动收集依赖和自动触发依赖
这一步改进的目的就是要将我们的触发依赖和依赖收集自动化,省去我们手动调用的过程。
这步其实很简单,同时也涉及到 Js 里面一些基本的概念,如果有不是太明白的可以参考一下 MDN 中关于get和set的相关概念。 其实就是通过 get 和 set 的特性进行数据劫持,这样我们就可以将依赖的触发和收集封装到对应的函数中:
class Dep{
...
get value(){
this.depend()//触发依赖的收集
return this._val
}
set value(newVal){
this._val = newVal
this.notice()
}
...
}
经过上面的改进后,我们下面的使用其实就省去了手动触发依赖和依赖收集的过程了:
const rawData =new Dep(5)
function effect(eff){
currentEffect =eff
eff()//这里的eff里面因为调用了响应式数据的get方法,自动触发了依赖收集的过程。
currentEffect =null
}
let reactData;
effect(()=>{
reactData = rawData.value+5
console.log('reactData:',reactData)
})
//因为响应式数据发生了变化,触发了set方法,进而完成了 执行依赖的过程。
rawData.value = 10//输出 reactData:15
再进阶版:实现正式版的 reactive
上面我们实现了依赖的自动收集和执行,但是还有个问题就是响应式数据只能是单一的类型且只能挂在到 value 属性上,这个更像是 Vue3 里面的ref。所以我们后面要改进的就是需要支持对象响应式。
这里我们额外介绍一下实现劫持对象的两种方式:
-
Object.defineProperties():这个是用来在对象上定义新的属性或者修改现有属性的。Vue2 实现数据劫持就是用的这个方法。
- 局限性:例如不能对新增属性实现劫持、不能对因数组长度引起的变化进行劫持。另外,因为他是对属性进行劫持,所以当我们的对象层层嵌套的时候需要递归处理,这也是一件很耗性能的事情。
-
Proxy:proxy 是直接代理对象。这个和 Object.defineProperties 在功能上其实没有什么区别,都是劫持。只不过 proxy 是直接拦截对象的变化,所以就避开了 defineProperties 的那些坑。
- 这里再额外提一句Reflect,它和 Proxy 一般是成对出现的。它提供拦截 JS 操作的方法
有了上面的基础,我们下面来改造一下我们的 reactive:
//targetMap的作用是存储依赖和数据的映射
const targetMap = new Map()
//获取依赖
function getDep(target,key){
let deps = targetMap.get(target)
//第一次的时候初始化一下
if (!deps) {
deps = new Map()
targetMap.set(target, deps)
}
let dep = deps.get(key)
//第一次dep不存在的时候,初始化一下
if (!dep) {
dep = new Dep()
deps.set(key, dep)
}
//上面这堆东西就是为了让key和dep映射起来
return dep
}
//包装响应式数据
function reactive(raw) {
return new Proxy(raw, {
//get的作用就是收集依赖
get(target, key) {
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key)
},
set(target, key, value) {
//set的主要作用就是触发依赖
//获取dep对象
const dep = getDep(target, key)
//! 这里需要更新值以后才去通知更新
const result = Reflect.set(target, key, value)
dep.notice()
return result
}
})
}
//这里还需要精简一下effectWatch
function effectWatch(effect) {
currentEffect = effect
//第一次收集的时候会调用收集到的依赖函数
effect()
//置空
currentEffect = null
}
这里,我们引入了 reactive 来封装响应式数据,使得我们的响应式可以支持复杂的对象类型。这里我们引入了一个 Map 用来存储数据和 Dep 依赖的映射。 这里有个需要注意的地方 targetMap 里面存储的的也是一个 Map,这里面用来存储的就是对象属性和依赖的映射了。
简单概括一下我们 reactive 的原理:我们通过 Proxy 对需要处理的响应式数据进行数据劫持,形成数据和依赖的映射。然后在我们获取数据的时候进行依赖收集,在我们更新数据的时候执行依赖。 下面我们来使用一下:
let demo = reactive({
num:1
})
let reactiveData
effectWatch(()=>{
reactiveData = demo+2
console.log(reactiveData)
})
demo.num = 4
//输出
//3
//6
总结
上面就是我们 min-vue 中的响应式了。响应式其实是一种思路,我们在 Vue 中使用的响应式基本上都涉及到了视图的更新,这只是其中的一方面。这也是为什么 Vue3 将 reactive 模块单独抽离出来的原因。我们有了这种响应式的思路,可以将其应用的更多的地方。 后面我们会结合界面的渲染慢慢实现自己的 min-vue。
PS:最后附上代码地址