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


相关文章
|
2月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
311 2
|
1月前
|
缓存 JavaScript
vue中的keep-alive问题(2)
vue中的keep-alive问题(2)
288 137
|
5月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
786 0
|
5月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
4月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
412 1
|
4月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
260 0
|
5月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
472 17
|
5月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
275 1
|
5月前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
515 0
|
5月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
251 0