Vuejs设计与实现 —— 实现响应式系统(一)

简介: Vuejs设计与实现 —— 实现响应式系统

image.png


前言

若你想了解虚拟 DOM 的部分,可见 Vuejs设计与实现 —— 为什么需要虚拟 DOM

Vuejs 三大核心模块:

  • Compiler 模块:涉及 AST 抽象语法树的内容,再通过 generateAST 生成渲染函数等
  • Runtime 模块:也称为 Renderer 模块,将虚拟 DOM 生成真实 DOM 元素,并渲染到浏览器上
  • Reactivity 模块:响应式系统,将 JavaScript 对象代理为数据模型,而当你修改数据模型时,视图会进行更新

其中响应式系统是 Vuejs 中的重要组成部分,相信这一部分大家都是深有体会的,下面就尝试实现响应式系统,目的是为了更好的了解响应式系统的设计与实现的过程。

响应式数据 & 副作用函数

什么是副作用函数?

顾名思义,副作用函数指的就是会产生 副作用函数

其中的 函数 不难理解,那么 副作用 是什么呢?

下面举个栗子,存在如下两个函数(具体作用看注释):

// 设置 body 中文本内容
function setTextForBody(text = 'hello vue3'){
  document.body.innerText = text
}
// 获取 body 中的文本并输出
function getTextFromBody(){
  console.log("document.body = ", document.body.innerText)
}
复制代码

当函数 setTextForBody 函数执行时,会将页面中 body 的内容默认设置为 'hello vue3',而 getTextFromBody 函数是负责获取 body 的文本内容。那么当 setTextForBody 调用时传入的参数不同,会导致 getTextFromBody 函数中获取到的内容也会发生改变,甚至是在其他函数中也有类似的设置或读取 body 中文本内容的操作,都会 受到直接或间接的影响,那么就可以称这个函数(这里是 setTextForBody 函数)产生了 副作用(effect)

什么是响应式数据?

请看下面的栗子:

// 初始数据
const data = { text: 'hello world' }
// 副作用函数
function effect(){
  document.body.innerText = data.text
}
// 修改数据
setTimeout(() => {
  data.text = 'hello vue3'
}, 3000);
复制代码

如上代码中,副作用函数 effect 会设置 body 的文本内容为数据 data 对象中的 text 属性,而 setTimeout 则负责 3s 后将 data.text 的值进行修改。

期望的是,当代码执行 data.text = 'xxx' 的代码时,副作用函数 effect 可以自动执行,而省略手动调用的过程,那如果能实现这个目标,那么就可以将 data 对象称为响应式数据。

响应式数据的基本实现

通过上面的代码,不难发现(毕竟代码量很少):

  • 当副作用函数 effect 执行时,会通过 data.text 进行 读取操作
  • 当需要修改 text 字段值时,会通过 data.text = xxx 进行 设置操作其中 读取操作设置操作 正好对应 JavaScript 中的 gettersetter,在 Vue.js 2 中采用的是 Object.defineProperty 函数进行实现,而在 Vue.js 3 中已经转向 Proxy 的实现方式。

基本思路

  • 将原始数据进行代理,实现  gettersetter 函数
  • 当执行副作用 effect 函数时,会触发对应数据的 getter 函数,此时将这个 effect 函数保存到容器 bucket 中,等待在未来某时刻执行
  • 当执行 data.text = xxx 操作时,会触发对应数据的 setter 函数,此时从容器 bucket 中取出所有 effect 函数并执行它们
// 存储副作用函数的容器
const bucket = new Set()
// 原始数据
const rawData = { text: 'hello world' }
// 副作用函数
function effect(){
  document.body.innerText = proxyData.text
}
// 响应式函数
function reactive(target){
  return new Proxy(target, {
      get(target, key){
        // 保存副作用函数 effect
        bucket.add(effect)
        // 返回访问的值
        return target[key]
      },
      set(target, key, newValue){
        // 设置新值
        target[key] = newValue
        // 从容器 bucket 中取出 effect 函数并执行
        bucket.forEach(fn => fn())
        // 表示设置成功
        return true
      }
    })
}
// 对原始数据进行代理
const proxyData = reactive(rawData)
复制代码

现在可以使用下面的代码来进行测试:

// 初始化执行,触发 getter 函数,收集 effect
effect()
// 2s 后对 proxyData.text 进行修改
setTimeout(() => {
  console.log('定时器执行,触发修改')
  proxyData.text = 'hello vue3'
}, 2000)
复制代码

得到的结果如下:

image.png

完善响应式系统

明确副目标对象和作用函数关系

存在缺陷

上面通过硬编码的形式进行的实现,存在着如下的缺陷:

  • 强制副作用的函数名为 effect,这会导致一旦产生副作用的函数名不是 effect,那么上述的代码实现就无法达到预期的效果
  • 最佳实现 应该是哪怕副作用函数是匿名的,也能被正确的进行收集到容器中,从而在未来某个班时刻被执行
  • 仅仅使用 set 数据结构作为副作用函数的容器,会导致 副作用函数和被操作目标的字段之间无法建立明确的关系
  • 目前的实现是将所有的副作用函数全部放到同一个容器中进行存储,导致的结果就是一旦某个被操作的目标字段进行更新操作,这会导致容器中的所有的副作用(即使是无关的)全部都会执行一遍
  • 最佳实现 应该是只执行和当前被操作目标中具体字段相关的副作用函数,可以是一个或多个

完善思路

  • 由于期望的副作用函数可以是任意形式的函数,因此需要一个全局变量 activeEffect 存储当前被注册的副作用函数
  • 为了 副作用函数和被操作目标的字段之间建立明确的关系,需要使用 WeakMap、Map、Set 重构对应的数据结构
  • 通过 WeakMap 创建依赖容器 bucket,它的键是对应不同的被操作目标对象,它的值的类型是一个 Map 对象,这个 Map 对象的键是对应被操作目标对象中的不同字段,其中每个字段对应的值是 Set 数据结构,里面可以存储多个对应的副作用函数
  • 可以通过下图进行辅助理解
     image.png

【关于数据结构选择上的解释】:使用 WeakMap 用来存储不同的被操作目标对象,是因为 WeakMap 中的键是弱引用的,简单的说一旦外部环境没有对这个目标对象的引用,那么垃圾回收机制可以正常进行回收;而 Map 的键属于强引用,即便外部没有对目标对象的引用,但这个 Map 本身的键也会被认为是对目标对象的引用,因此会导致垃圾回收无法正常进行。

具体代码实现

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// 用于接收并注册副作用函数
function effect(fn) {
  // 保存 fn
  activeEffect = fn
  // 执行 fn 函数,目的是初始化执行和触发 get 拦截
  fn()
}
// 响应式数据
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      console.log("get =", key, Reflect.get(target, key));
      // 没有注册副作用函数,直接返回数据
      if (!activeEffect) return Reflect.get(target, key)
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, newVal) {
      console.log("set ", key, newVal);
      target[key] = newVal
      trigger(target, key)
      return Reflect.set(target, key, newVal)
    }
  })
}
// 收集依赖
function track(target, key) {
  // 从 bucket 获取 depsMap 的依赖关系
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  // 从 depsMap 获取 deps 集合
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}
// 触发依赖
function trigger(target, key) {
  // 获取对应的 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return
  // 获取对应的 deps
  const deps = depsMap.get(key)
  // 执行相应的 effect
  deps && deps.forEach(effect => effect())
}
复制代码

动态清除无用副作用函数

存在缺陷

若执行下面的测试代码,那么会产生 遗留的副作用函数依赖

// 获得响应式数据
const data = reactive({
  text: 'hello world...',
  ok: true
})
// 注册副作用函数
effect(() => {
  console.log('effect running ...')
  document.body.innerText = data.ok ? data.text : 'not ok'
})
复制代码
  • 当初始化 ok = true 时执行,会产生依赖关系为:
  • image.png
  • 当发生修改操作 ok = false 后,此时会执行对应副作用函数,同时意味着 data.text 字段将不会再被访问到,理想情况是此时 data.text 字段所对应的副作用函数依赖应该要被清除

完善思路

  • 每次副作用函数执行时,将副作用函数从所有与之有关联的依赖集合中进行删除
  • 当副作用函数执行完毕后,又会产生新的依赖关系,但这个新的依赖关系就不会包含遗留的副作用函数


目录
相关文章
|
22天前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
21 1
|
22天前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
27 1
|
25天前
|
监控 JavaScript 算法
深度剖析 Vue.js 响应式原理:从数据劫持到视图更新的全流程详解
本文深入解析Vue.js的响应式机制,从数据劫持到视图更新的全过程,详细讲解了其实现原理和运作流程。
|
22天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
50 0
|
2月前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
29 3
|
2月前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
37 2
|
2月前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
2月前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
2月前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
27 0
|
2月前
|
JavaScript 前端开发 开发者
使用 Vue.js 和 Vuex 构建响应式前端应用
【10月更文挑战第2天】使用 Vue.js 和 Vuex 构建响应式前端应用
32 0