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

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

实现可调度执行 — 调度器函数

什么是可调度性?

可调度指的是当 trigger 触发副作用函数重新执行时,提供给使用者决定副作用函数执行的时机、次数和方式。

通过下面的代码举个栗子:

// 获得响应式数据
const data = reactive({
 count: 1
})
// 注册副作用函数
effect(() => {
 console.log(data.count)
})
data.count++
console.log('结束了')
复制代码

其对应的数据输出结果为:1 2 '结束了',假设使用者需要的输出顺序是:1 '结束了' 2,那么就需要当前的响应式系统支持 调度

实现思路

  • 给现有的 effect 函数多添加一个可选参数 options,允许使用者指定调度器,例如:
effect(()=>{
    console.log(data.count)
},
{
    // 将调度器设置名为 scheduler 的函数
    scheduler(fn){
     ...
    }
})
复制代码
  • effect 函数中注册副作用函数时,将这个 options 选项挂载到对应的副作用函数上
  • trigger 函数触发副作用函数重新执行时,通过直接调用 options 中传入的调度器函数,把控制权移交给使用者

具体代码实现

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// effect 栈
const effectStack = []
// 用于接收并注册副作用函数
function effect(fn, options = {}) {
 const effectFn = () => {
   // 先调用 cleanup 函数完成旧依赖的清除工作
   cleanup(effectFn)
   // 保存 fn
   activeEffect = effectFn
   // 在副作用函数调用前,将副作用函数入栈
   effectStack.push(effectFn)
   // 执行 fn 函数,目的是初始化执行和触发 get 拦截
   fn()
   // 副作用函数执行完成后出栈
   effectStack.pop()
   // 将 activeEffect 指向栈顶(原先)的副作用函数
   activeEffect = effectStack[effectStack.length - 1]
 }
 // 将 options 挂载到 effectFn 上
 effectFn.options = options
 // 用于存储所有与其关联的副作用函数的依赖集合
 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 && effects.forEach(effectFn => {
   // 避免递归调用自身
   if (effectFn !== activeEffect) effectsToRun.add(effectFn)
 })
 // 是否执行调度器函数
 effectsToRun.forEach(effectFn => {
   // 若副作用函数存在调度器,则调用调度器,并将 effectFn 函数作为参数传递
   if (effectFn.options.scheduler) {
     effectFn.options.scheduler(effectFn)
   } else {
     // 否则直接执行副作用函数
     effectFn()
   }
 })
}
复制代码

基于调度器控制执行次数

为什么需要控制执行次数?

直接通过如下栗子进行解释:

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

在没有指定调度器时,以上代码执行后输出结果为:1 2 3,但假设其中的 2 只是个过渡阶段,使用者只关心最后的结果 3,那么执行三次打印操作就是多余的,即期望输出为:1 3

实现思路

  • 定义一个任务队列 jobQueue,选择 Set 数据结构,目的是利用它的自动去重功能
  • 每次调度执行时,先将当前副作用函数添加到 jobQueue 队列中
  • 定义一个 flushJob 函数刷新 jobQueue 队列中的副作用函数
  • 其中需要设定一个 isFlushing 表示正在刷新的标志,用于去判断是否需要执行,只有当 isFlushing = false 时才需要执行,保证 flushJob 函数在一个周期内只调用一次
  • 最后通过 promise.then 来将刷新 jobQueue 队列的执行添加到微任务队列中

即支持通过如下方式进行调用:

// 获得响应式数据
const data = reactive({
  count: 1
})
// 注册副作用函数
effect(() => {
  console.log(data.count);
}, {
  scheduler(effectFn) {
    // 将副作用函数添加到 jobQueue 队列中
    jobQueue.add(effectFn)
    // 调用 flushJob 刷新队列,减少不必要的执行
    flushJob()
  }
})
data.count++
data.count++
复制代码

具体代码实现

// 存储副作用函数
const bucket = new WeakMap()
// 用于存储被注册的副作用函数
let activeEffect = null
// effect 栈
const effectStack = []
// 定义 jobQueue 任务队列
const jobQueue = new Set()
// 通过 promise 微任务实现异步执行
const resolvedPromise = Promise.resolve()
function nextTick(fn) {
  return fn ? resolvedPromise.then(fn) : resolvedPromise
}
// 表示当前是否正在刷新队列
let isFlushing = false
// 刷新队列函数
function flushJob() {
  // 当前正在刷新队列,则直接结束
  if (isFlushing) return
  // 一旦需要执行刷新队列,先将 isFlushing 置为 false
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  nextTick(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 刷新队列结束后,重置 isFlushing
    isFlushing = false
  })
}
// 用于接收并注册副作用函数
function effect(fn, options = {}) {
  const effectFn = () => {
    // 先调用 cleanup 函数完成旧依赖的清除工作
    cleanup(effectFn)
    // 保存 fn
    activeEffect = effectFn
    // 在副作用函数调用前,将副作用函数入栈
    effectStack.push(effectFn)
    // 执行 fn 函数,目的是初始化执行和触发 get 拦截
    fn()
    // 副作用函数执行完成后出栈
    effectStack.pop()
    // 将 activeEffect 指向栈顶(原先)的副作用函数
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 将 options 挂载到 effectFn 上
  effectFn.options = options
  // 用于存储所有与其关联的副作用函数的依赖集合
  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 && effects.forEach(effectFn => {
    // 避免递归调用自身
    if (effectFn !== activeEffect) effectsToRun.add(effectFn)
  })
  // 是否执行调度器函数
  effectsToRun.forEach(effectFn => {
    // 若副作用函数存在调度器,则调用调度器,并将 effectFn 函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否则直接执行副作用函数
      effectFn()
    }
  })
}
复制代码

最后

以上内容都是 Vue.js 内部响应式系统的实现思路,但其内部拥有一个更完善处理机制和边界情况,作为学习者而言了解其设计思路和设计原因其实也足够了,不过作为 coder 不要总是省略动手的过程。


目录
相关文章
|
8天前
|
前端开发 JavaScript Java
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
53 13
【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
|
9天前
|
人工智能 JavaScript 关系型数据库
【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
55 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

热门文章

最新文章