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()
 })
}


目录
相关文章
|
19小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的招投标系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的招投标系统附带文章和源代码部署视频讲解等
19 9
|
19小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的高校毕业与学位资格审核系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的高校毕业与学位资格审核系统附带文章源码部署视频讲解等
14 2
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的宠物医院系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的宠物医院系统附带文章和源代码部署视频讲解等
13 3
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的城市公交调度系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的城市公交调度系统附带文章和源代码部署视频讲解等
14 4
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的线上书籍查阅系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的线上书籍查阅系统附带文章和源代码部署视频讲解等
14 4
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的超市商品管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的超市商品管理系统附带文章和源代码部署视频讲解等
11 4
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的私人定制旅游系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的私人定制旅游系统附带文章和源代码部署视频讲解等
13 4
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的穿戴搭配系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的穿戴搭配系统附带文章和源代码部署视频讲解等
11 4
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的技术疫情防控自动售货机系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的技术疫情防控自动售货机系统附带文章和源代码部署视频讲解等
8 0
基于ssm+vue.js+uniapp小程序的技术疫情防控自动售货机系统附带文章和源代码部署视频讲解等
|
20小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的游戏后台系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的游戏后台系统附带文章和源代码部署视频讲解等
4 0