响应系统的设计与实现

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据

export const listenInit = (data) => {
  return new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal

      trigger(target, key)
      return Reflect.set(...arguments)
    },
  })
}

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects &&
    effects.forEach((effectFn) => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })

  effectsToRun.forEach((effectFn) => {
    // 如果一个副作用函数存在调度器,调用调度器,然后将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      //否则直接执行副作用函数
      effectFn()
    }
  })
}

// 用一个全局变量存储被注册的副作用函数
let activeEffect
let effectStack = []

// effect 函数用于注册副作用函数
export const jobEffect = (fn, options = {}) => {
  const effectFn = () => {
    cleanup(effectFn)
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn
    effectStack.push(effectFn)
    const res = fn()

    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    return res
  }

  // 将options挂载到effectFn上
  effectFn.options = options

  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集合
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后需要重置 effectFn.deps 数组
  effectFn.deps.length = 0
}

// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  console.log('flushing!!')
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return
  console.log('flusingDone!!!')
  // 设置为 true,代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach((job) => job())
  }).finally(() => {
    // 结束后重置 isFlushing
    isFlushing = false
  })
}

export const scheduler = (fn) => {
  // 每次调度时,将副作用函数添加到 jobQueue 队列中
  jobQueue.add(fn)
  // 调用 flushJob 刷新队列
  flushJob()
}

export const jobWatch = (source, cb, options = {}) => {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  // 定义旧值与新值
  let oldValue, newValue

  // 提取 scheduler 调度函数为一个独立的 job 函数
  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue)
    oldValue = newValue
  }

  // 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
  const effectFn = jobEffect(() => getter(), {
    lazy: true,
    scheduler: job,
  })

  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job,从而触发回调执行
    job()
  } else {
    oldValue = effectFn()
  }
}

function traverse(value, seen = new Set()) {
  // 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  // 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
  seen.add(value)
  // 暂时不考虑数组等其他结构
  // 假设 value 就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
  for (const k in value) {
    traverse(value[k], seen)
  }

  return value
}


// 调用方法
import { listenInit, jobWatch } from './jobQueue'
let errorMessage = listenInit({ msg: '' })
jobWatch(
  () => errorMessage.msg,
  (newVal, oldVal) => {
    if (newVal != oldVal) {
      message.error(newVal)
    }
  }
)

  《Vue.js设计与实现》,书写的真好,学习到很多,vue3实现数据响应的方式了解了。

  “他的眼睛很小,跟猪的差不多,却闪动着十足的热情。不知是怎的,人们见到这样的人总是心生厌恶” ——《巴黎伦敦落魄记》

  这么回事嘛,要内敛一些呀。

posted @ 2023-01-31 10:14  艾路  阅读(34)  评论(0编辑  收藏  举报