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

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

开篇

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

全文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实例,它们都可以进行自己依赖收集和触发。


相关文章
|
4天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
4天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
4天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
10天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
10天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
10天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
9天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
11天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
9天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
下一篇
无影云桌面