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

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

具体代码实现

image.png

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// 用于接收并注册副作用函数
function effect(fn) {
 const effectFn = () => {
   // 先调用 cleanup 函数完成旧依赖的清除工作
   cleanup(effectFn)
   // 保存 fn
   activeEffect = effectFn
   // 执行 fn 函数,目的是初始化执行和触发 get 拦截
   fn()
 }
 // 用于存储所有与其关联的副作用函数的依赖集合
 effectFn.deps = []
 // 执行副作用函数
 effectFn()
}
// 清除本次依赖相关的旧副作用函数
function cleanup(effectFn) {
 for (let i = 0; i < effectFn.deps.length; i++) {
   const deps = effectFn.deps[i]
   deps.delete(effectFn)
 }
 // 重置 effectFn.deps 数组
 effectFn.deps.length = 0
}
// 响应式数据
function reactive(target) {
 return new Proxy(target, {
   get(target, key) {
     // 没有注册副作用函数,直接返回数据
     if (!activeEffect) return Reflect.get(target, key)
     track(target, key)
     return Reflect.get(target, key)
   },
   set(target, 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)
 // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中
 activeEffect.deps.push(deps)
}
// 触发依赖
function trigger(target, key) {
 // 获取对应的 depsMap
 const depsMap = bucket.get(target)
 if (!depsMap) return
 // 获取对应的 deps
 const effects = depsMap.get(key)
 // 构建新的 Set 避免递归
 const effectsToRun = new Set(effects)
 // 执行相应的 effect
 effectsToRun.forEach(effectFn => effectFn())
}
复制代码

支持嵌套的 effect 函数

为什么要支持嵌套 effect 函数?

Vuejs 来举例,如组件的嵌套就需要支持嵌套的 effect 函数,伪代码如下:

image.png

存在缺陷

假设存在如下的嵌套关系,存在的缺陷如下:

  • 当定时器执行并只更改 data.text 的值,此时只有 effect2 执行了,而期望的 effect1 却没执行执行
  • 原因是 目前使用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,意味着同一时刻 activeEffect 存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层的副作用函数的执行会覆盖 activeEffect 的值,当外部响应式数据进行依赖收集时,它们收集到的副作用函数将会是内层的副作用函数

image.png

// 获得响应式数据
const data = reactive({
  text: 'hello world...',
  ok: true
})
// 注册副作用函数
effect(() => {
  effect(() => {
    console.log('effect2 执行:', data.ok)
  })
  console.log('effect1 执行:', data.text)
})
console.log("bucket = ", bucket);
setTimeout(() => {
  console.log('setTimeout 执行,修改 data.text 的值')
  data.text = 'hello vue3...'
}, 1000);
复制代码

完善思路

通过副作用函数栈 effectStack 将正在执行的副作用函数入栈,等到副作用函数执行完毕后再弹出栈,并保证 activeEffect 始终是指向栈顶的副作用函数。

image.png

具体代码实现

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// effect 栈
const effectStack = []
// 用于接收并注册副作用函数
function effect(fn) {
  const effectFn = () => {
    // 先调用 cleanup 函数完成旧依赖的清除工作
    cleanup(effectFn)
    // 保存 fn
    activeEffect = effectFn
    // 在副作用函数调用前,将副作用函数入栈
    effectStack.push(effectFn)
    // 执行 fn 函数,目的是初始化执行和触发 get 拦截
    fn()
    // 副作用函数执行完成后出栈
    effectStack.pop()
    // 将 activeEffect 指向栈顶(原先)的副作用函数
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 用于存储所有与其关联的副作用函数的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}
// 清除本次依赖相关的旧副作用函数
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  // 重置 effectFn.deps 数组
  effectFn.deps.length = 0
}
// 响应式数据
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      // 没有注册副作用函数,直接返回数据
      if (!activeEffect) return Reflect.get(target, key)
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, 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)
  // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}
// 触发依赖
function trigger(target, key) {
  // 获取对应的 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return
  // 获取对应的 deps
  const effects = depsMap.get(key)
  // 构建新的 Set 避免递归
  const effectsToRun = new Set(effects)
  // 执行相应的 effect
  effectsToRun.forEach(effectFn => effectFn())
}
复制代码

避免无限递归循环

存在缺陷

如下面的例子,就会产生无限循环:

// 获得响应式数据
const data = reactive({
 count: 1
})
// 注册副作用函数
effect(() => {
 data.count++ // 等价于 data.count = data.count + 1
})
复制代码

其中,既会读取 data.count 的值,又会设置 data.count 的值,每次 trigger 操作触发时,本次还没有执行完,又触发了下一次的 trigger 操作,这就会产生无限递归调用自身,导致栈溢出。

完善思路

trigger 操作发生时添加是否执行副作用函数的条件:若 trigger 触发执行的副作用函数与当前的正则执行的副作用函数相同,则不触发执行

具体代码实现

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// effect 栈
const effectStack = []
// 用于接收并注册副作用函数
function effect(fn) {
 const effectFn = () => {
   // 先调用 cleanup 函数完成旧依赖的清除工作
   cleanup(effectFn)
   // 保存 fn
   activeEffect = effectFn
   // 在副作用函数调用前,将副作用函数入栈
   effectStack.push(effectFn)
   // 执行 fn 函数,目的是初始化执行和触发 get 拦截
   fn()
   // 副作用函数执行完成后出栈
   effectStack.pop()
   // 将 activeEffect 指向栈顶(原先)的副作用函数
   activeEffect = effectStack[effectStack.length - 1]
 }
 // 用于存储所有与其关联的副作用函数的依赖集合
 effectFn.deps = []
 // 执行副作用函数
 effectFn()
}
// 清除本次依赖相关的旧副作用函数
function cleanup(effectFn) {
 for (let i = 0; i < effectFn.deps.length; i++) {
   const deps = effectFn.deps[i]
   deps.delete(effectFn)
 }
 // 重置 effectFn.deps 数组
 effectFn.deps.length = 0
}
// 响应式数据
function reactive(target) {
 return new Proxy(target, {
   get(target, key) {
     // 没有注册副作用函数,直接返回数据
     if (!activeEffect) return Reflect.get(target, key)
     track(target, key)
     return Reflect.get(target, key)
   },
   set(target, 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)
 // 将与当前副作用函数存在联系的依赖集合 deps 添加到 activeEffect.deps 数组中
 activeEffect.deps.push(deps)
}
// 触发依赖
function trigger(target, key) {
 // 获取对应的 depsMap
 const depsMap = bucket.get(target)
 if (!depsMap) return
 // 获取对应的 deps
 const effects = depsMap.get(key)
 // 构建新的 Set 避免递归
 const effectsToRun = new Set(effects)
 // 执行相应的 effect
 effectsToRun && effectsToRun.forEach(effectFn => {
   // 避免递归调用自身
   if (effectFn !== activeEffect) effectFn()
 })
}


目录
相关文章
|
7天前
|
前端开发 JavaScript Java
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
52 13
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
|
8天前
|
人工智能 JavaScript 关系型数据库
【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
54 14
【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
|
7天前
|
SQL JavaScript 安全
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
44 11
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
|
11天前
|
人工智能 JavaScript 安全
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
55 13
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
|
3月前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
70 18
|
3月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
117 17
|
3月前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
58 1
|
3月前
|
监控 JavaScript 算法
深度剖析 Vue.js 响应式原理:从数据劫持到视图更新的全流程详解
本文深入解析Vue.js的响应式机制,从数据劫持到视图更新的全过程,详细讲解了其实现原理和运作流程。
|
4月前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
39 3
|
4月前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
56 2

热门文章

最新文章