响应系统的设计与实现
// 存储副作用函数的桶 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实现数据响应的方式了解了。
“他的眼睛很小,跟猪的差不多,却闪动着十足的热情。不知是怎的,人们见到这样的人总是心生厌恶” ——《巴黎伦敦落魄记》
这么回事嘛,要内敛一些呀。