看不懂vue源码?没事,来手写一个mini-vue3吧【reactivity】

824 阅读6分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

开篇

最近在准备春招,然后看到了一个不错教程,想着记录下来,顺便也分享给大家。用笔记+个人理解的方式督促自己的学习,这个系列还会继续更新,欢迎点赞 👍

全文1800+,20分钟带你掌握vue3中的三大核心之一reactive,响应式的基本原理,加深对依赖收集的理解。

image.png

看看响应式的三个版本

1.回归原始 --v1

我们简单想一下,如果直接想要实现一个响应式的数据是不是每次将响应式的数据和与已经和响应式数据建立的某种关系,重新执行一次,再次获取两者之间的关系。

这样说可能不是很清楚(俺也一样),我们看个简单的例子。

let a = 10;
let b = a + 10  // b = a + 10 这就是 数据a和数据b之间的关系,b依赖于a,也成为依赖关系

a = 20
b = a + 10 // 重新执行这个依赖

上述代码中 a 就是一个响应式数据,b的值会受到a的影响,b = a + 10;这就是它们之间依赖关系

2.封装一个更新函数--v2

好,我们接着上面的代码继续往下看,如果响应式对象a不断的发生变化,难道我们每一次都要去执行 b = a + 10嘛,不如我们先封装一个update函数

let a = 10;
let b;
update()
function update () {
  b = a + 10
}

a = 20
update()

a = 30
update()

update函数中保存着 b和a之间的依赖关系,每一次我们修改a的值的时候,都要手动的执行这个函数,(这个时候有人就说啦 啊八八八八八,这还不是重新写了一边b = a + 10嘛。其实如果b和a之间的关系复杂一点,这个封装是很有必要的)

如何实现一个监听器 帮我们去做这样一件事情呢? 我们先来看看在vue中是怎么实现的

3.Vue中的effect和reactive --v3

废话不多说我们直接上代码

const { reactive, effect } = require('@vue/reactivity')

let a = reactive({
  value: 10
})
let b;

effect(() => {
  b = a.value + 10
  console.log(b)
})
a.value = 20
a.value = 30
a.value = 40
// output : 20 30 40 50

vue的做法很巧妙,实现了一个reactive函数,reactive返回的是一个响应式对象,以及一个effect函数,这个函数传入另一个函数作为参数(后续叫做依赖函数)

依赖函数中保存着 a 和 b 的依赖关系。它有以下几个特点

  • 初始化的时候函数会执行一次。(这一次也就是建立a,b的关系)
  • 后续响应式数据a发生改变的时候,这个依赖函数会重新执行

好了,弄清楚了上面的三个版本的案例,我们自己来实现一个响应式系统吧。

手写响应式系统

实现一个Dep的类和监听函数effectWatch

Dep类主要有两个职责

  • 收集依赖函数,初始化的时候执行一次
  • 当响应式数据发生变化的时候,依赖函数重新执行

effectWatch函数主要

  • 接受一个参数,该参数为依赖函数
  • 将传入过来的依赖函数临时存储,想办法让它被Dep收集

好了,我们弄清楚了它的职责就好办了。先把它的雏形搭建好

回看图的右侧部分

image.png

再来继续实现我们的js

let currentEffect; //暂时存储依赖
class Dep {
  constructor(val) {
    this._val = val
    this.effects = new Set() //将依赖存储
  }
  set value (value) {
    this._val = value
  }
  get value () {
    this.depend() //读取的时候进行依赖收集
    return this._val
  }
  // 1.收集依赖
  depend () {
    if (!currentEffect) {
      this.effects.add(currentEffect)
    }
  }

  // 2.触发依赖
  notice () { }
}
function effectWatch (effect) {
  // 收集依赖
  currentEffect = effect
  effect() //初始化执行
  currentEffect = null
}

好了,代码的雏形都搭建好了,并且也完成了一定的功能

我们先做个简单的测试

...
...
let dep = new Dep(10)
let b;
effectWatch(() => {
  console.log('hello')
  b = dep.value + 10
})

// output: hello

上面实现了依赖收集函数首次执行的效果,我们接下来就是实现当 dep.value发生改变的时候重新执行依赖函数

触发依赖

class Dep{
...
// 当响应式对象的值发生改变时,通知notice方法触发依赖
 set value (value) {
    this._val = value
    this.notice() 
  }
// 2.触发依赖
  notice () {
    this.effects.forEach(effect => { //当notice执行之后 将所有收集到的依赖执行
      effect()
    })
  }
...
}

我们做个测试

...
let dep = new Dep(10)
let b;
effectWatch(() => {
  console.log('hello')
  b = dep.value + 10
})

dep.value = 40
//output : hello hello 

至此我们就实现了一个简易版本的响应式系统,其实也就是对应vue中ref,现在我们就基于上面的基本数据类型的一个响应式系统,写一个reactive函数,实现对对象的响应式处理。

再来回看前文图中的左侧部分

image.png

再看实现代码

const targetsMap = new Map()

function getDep (target, key) {
  let depsMap = targetsMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetsMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  return dep
}

function reactive (row) {
  return new Proxy(row, {
    get (target, key) {
      // target 就是目标对象row
      // 1.将target中的每一个key对应一个响应式dep   key->dep
      let dep = getDep(target, key)
      // 2.收集对应这个key的dep依赖
      dep.depend()
      return Reflect.get(target, key)  // 相当于 target[key]
    },
    set (target, key, value) {
      let dep = getDep(target, key)
      let result = Reflect.set(target, key, value)
      dep.notice()
      return result
    }
  })
}

好了,写完代码我们做一个简单的测试

...

const user = reactive({
  age: 20
})
let doubleAge

effectWatch(() => {
  doubleAge = user.age * 2
  console.log(doubleAge)
})

user.age = 21
user.age = 22

// output: 40 42 44

结果符合我们的预期,那么reactive函数也就差不多写完了。

完整代码



let currentEffect; //暂时存储依赖
class Dep {
  constructor(val) {
    this._val = val
    this.effects = new Set() //将依赖存储
  }
  set value (value) {
    this._val = value
    this.notice()
  }
  get value () {
    this.depend() //告诉响应式对象去收集依赖
    return this._val
  }
  // 1.收集依赖
  depend () {
    if (currentEffect) {
      this.effects.add(currentEffect)
    }
  }

  // 2.触发依赖
  notice () {
    this.effects.forEach(effect => {
      effect()
    })
  }
}
function effectWatch (effect) {
  // 收集依赖
  currentEffect = effect
  effect() //初始化执行
  currentEffect = null
}

const targetsMap = new Map()


function getDep (target, key) {
  let depsMap = targetsMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetsMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  return dep
}

function reactive (row) {
  return new Proxy(row, {
    get (target, key) {
      // target 就是目标对象row
      // 1.将target中的每一个key对应一个响应式dep   key->dep
      let dep = getDep(target, key)
      // 2.收集对应这个key的dep依赖
      dep.depend()
      return Reflect.get(target, key)  // 相当于 target[key]
    },
    set (target, key, value) {
      let dep = getDep(target, key)
      let result = Reflect.set(target, key, value)
      dep.notice()
      return result
    }
  })
}

总结

mini-vue版本的响应式就到这里了,我们来简单做个小结

  • Dep负责收集依赖和触发依赖,effectWatch负责首次执行依赖并通过 currentEffect和Dep进行联系
  • reactive函数本质上返回的是一个Proxy代理对象,通过targetsMap来将所有响应式对象进行存储,每一个响应式对象中的key都是一个Dep实例,它们都可以进行自己依赖收集和触发。

最后 感谢啊崔CXR带来的mini-vue