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 不要总是省略动手的过程。


目录
相关文章
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的社区防疫物资申报系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的社区防疫物资申报系统附带文章源码部署视频讲解等
26 0
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的小型医院医疗设备管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的小型医院医疗设备管理系统附带文章源码部署视频讲解等
28 6
|
9天前
|
JavaScript 前端开发 API
什么是响应式❓Vue2/Vue3中响应式的原理
什么是响应式❓Vue2/Vue3中响应式的原理
20 2
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的乡村养老服务管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的乡村养老服务管理系统附带文章源码部署视频讲解等
23 5
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的线上学习资源智能推荐系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的线上学习资源智能推荐系统附带文章源码部署视频讲解等
22 5
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的西安旅游系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的西安旅游系统附带文章源码部署视频讲解等
18 4
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的停车场管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的停车场管理系统附带文章源码部署视频讲解等
19 4
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的社区疫情返乡管控系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的社区疫情返乡管控系统附带文章源码部署视频讲解等
17 4
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的社区医疗服务可视化系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的社区医疗服务可视化系统附带文章源码部署视频讲解等
19 4
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的网上商品订单转手系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的网上商品订单转手系统附带文章源码部署视频讲解等
26 0