Skip to content

Vue.js 设计与实现-响应系统-代理Set和Map

作者:江月迟迟
发表于:2024-12-10
字数统计:6135 字
预计阅读21分钟

在本章中,我们首先介绍了 Proxy与 Reflect。Vue.js 3的响应式数据是基于 Proxy实现的,Proxy可以为其他对象创建一个代理对象。所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。在实现代理的过程中,我们遇到了访问器属性的 this指向问题,这需要使用 Reflect.*方法并指定正确的 receiver来解决。

然后我们详细讨论了 JavaScript中对象的概念,以及 Proxy的工作原理。在 ECMAScript规范中,JavaScript中有两种对象,其中一种叫作常规对象,另一种叫作异质对象。满足以下三点要求的对象就是常规对象:

对于表 5-1给出的内部方法,必须使用规范 10.1.x节给出的定义实现;

对于内部方法[[Call]],必须使用规范 10.2.1节给出的定义实现;

●对于内部方法[[Construct]],必须使用规范 10.2.2节给出的定义实现。

而所有不符合这三点要求的对象都是异质对象。一个对象是函数还是其他对象,是由部署在该对象上的内部方法和内部槽决定的。

接着,我们讨论了关于对象 Object的代理。代理对象的本质,就是查阅规范并找到可拦截的基本操作的方法。有一些操作并不是基本操作,而是复合操作,这需要我们查阅规范了解它们都依赖哪些基本操作,从而通过基本操作的拦截方法间接地处理复合操作。我们还详细分析了添加、修改、删除属性对 for...in操作的影响,其中添加和删除属性都会影响 for...in循环的执行次数,所以当这些操作发生时,需要触发与 ITERATE_KEY相关联的副作用函数重新执行。而修改属性值则不影响 for...in循环的执行次数,因此无须处理。我们还讨论了如何合理地触发副作用函数重新执行,包括对 NaN的处理,以及访问原型链上的属性导致的副作用函数重新执行两次的问题。对于 NaN,我们主要注意的是 NaN=== NaN永远等于 false。对于原型链属性问题,需要我们查阅规范定位问题的原因。由此可见,想要基于 Proxy实现一个相对完善的响应系统,免不了去了解ECMAScript规范。

而后,我们讨论了深响应与浅响应,以及深只读与浅只读。这里的深和浅指的是对象的层级,浅响应(或只读)代表仅代理一个对象的第一层属性,即只有对象的第一层属性值是响应(或只读)的。深响应(或只读)则恰恰相反,为了实现深响应(或只读),我们需要在返回属性值之前,对值做一层包装,将其包装为响应式(或只读)数据后再返回。

之后,我们讨论了关于数组的代理。数组是一个异质对象,因为数组对象部署的内部方法[[DefineOwnProperty]]不同于常规对象。通过索引为数组设置新的元素,可能会隐式地改变数组 length属性的值。对应地,修改数组 length属性的值,也可能会间接影响数组中的已有元素。所以在触发响应的时候需要额外注意。我们还讨论了如何拦截 for...in和 for...of对数组的遍历操作。使用for...in循环遍历数组与遍历普通对象区别不大,唯一需要注意的是,当追踪 for...in操作时,应该使用数组的 length作为追踪的key。for...of基于迭代协议工作,数组内建了Symbol.iterator方法。根据规范的 23.1.5.1节可知,数组迭代器执行时,会读取数组的 length属性或数组的索引。因此,我们不需

要做其他额外的处理,就能够实现对 for...of迭代的响应式支持。

我们还讨论了数组的查找方法。如 includes、indexOf以及lastIndexOf等。对于数组元素的查找,需要注意的一点是,用户既可能使用代理对象进行查找,也可能使用原始对象进行查找。为了支持这两种形式,我们需要重写数组的查找方法。原理很简单,当用户使用这些方法查找元素时,我们可以先去代理对象中查找,如果找不到,再去原始数组中查找。

我们还介绍了会隐式修改数组长度的原型方法,即 push、pop、shift、unshift以及 splice等方法。调用这些方法会间接地读取和设置数组的 length属性,因此,在不同的副作用函数内对同一个数组执行上述方法,会导致多个副作用函数之间循环调用,最终导致调用栈溢出。为了解决这个问题,我们使用一个标记变量shouldTrack来代表是否允许进行追踪,然后重写了上述这些方法,目的是,当这些方法间接读取 length属性值时,我们会先将shouldTrack的值设置为 false,即禁止追踪。这样就可以断开length属性与副作用函数之间的响应联系,从而避免循环调用导致的调用栈溢出。

最后,我们讨论了关于集合类型数据的响应式方案。集合类型指Set、Map、WeakSet以及 WeakMap。我们讨论了使用 Proxy为集合类型创建代理对象的一些注意事项。集合类型不同于普通对象,它有特定的数据操作方法。当使用 Proxy代理集合类型的数据时要格外注意,例如,集合类型的 size属性是一个访问器属性,当通过代理对象访问 size属性时,由于代理对象本身并没有部署[[SetData]]这样的内部槽,所以会发生错误。另外,通过代理对象执行集合类型的操作方法时,要注意这些方法执行时的 this指向,我们需要在get拦截函数内通过.bind函数为这些方法绑定正确的 this值。我们还讨论了集合类型响应式数据的实现。我们需要通过“重写”集合方法的方式来实现自定义的能力,当 Set集合的 add方法执行时,需要调用 trigger函数触发响应。我们也讨论了关于“数据污染”的问题。数据污染指的是不小心将响应式数据添加到原始数据中,它导致用户可以通过原始数据执行响应式相关操作,这不是我们所期望的。为了避免这类问题发生,我们通过响应式数据对象的 raw属性来访问对应的原始数据对象,后续操作使用原始数据对象就可以了。我们还讨论了关于集合类型的遍历,即 forEach方法。集合的 forEach方法与对象的 for...in遍历类似,最大的不同体现在,当使用 for...in遍历对象时,我们只关心对象的键是否变化,而不关心值;但使用forEach遍历集合时,我们既关心键的变化,也关心值的变化。

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>

<script>
// 定义区
let activeEffect
const effectStack = []

const reactiveMap = new Map()

const arrayInstrumentations = {}

const pro = reactive(new Map([
    ['key1', 'value1'],
    ['key2', 'value2']
]))

;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
    const originMethod = Array.prototype[method]
    arrayInstrumentations[method] = function(...args) {
        // this时代理对象,现在代理对象中查找,将结果存储到res中
        let res = originMethod.apply(this, args)

        if (res === false || res === -1) {
            // res 为false说明没找到,通过this.raw拿到原始数组,再去其中查找并更新res值
            res = originMethod.apply(this.raw, args)
        }
        return res
    }
})

// 一个标记变量,代表是否进行追踪。默认值为true,即允许追踪
let shouldTrack = true
// 重写数组的push方法
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
    // 取得原始push方法
    const originMethod = Array.prototype[method]
    // 重写
    arrayInstrumentations[method] = function(...args) {
        // 在调用原始方法之前,禁止追踪
        shouldTrack = false
        // push方法的默认行为
        let res = originMethod.apply(this, args)
        // 在调用原始方法之后,恢复原来的行为,即允许追踪
        shouldTrack = true
        return res
    }
})

// 抽离为独立的函数,便于复用
function iterationMethod() {
    // 获取原始数据对象 target
    const target = this.raw
    // 获取原始迭代器方法
    const itr = target[Symbol.iterator] ()

    const wrap = (val) => typeof val === 'object' && val !== null ? reactive(val) : val

    // 调用track函数建立响应联系
    track(target, ITERATE_KEY)

    // 返回自定义的迭代器
    return {
        next() {
            // 调用原始迭代器的nextfangfa获取value和done
            const { value, done } = itr.next()
            return {
                // 如果value不是undefined,则对其进行包裹
                value: value ? [wrap(value[0]), wrap(value[1])] : value,
                done
            }
        },
        // 并且实现可迭代协议
        [Symbol.iterator] () {
            return this
        }
    }
}

function valuesIterationMethod() {
    // 获取原始数据对象
    const target = this.raw
    // 通过target.values获取原始迭代器方法
    const itr = target.values()

    const wrap = (val) => typeof val === 'object' ? reactive(val) : val

    track(target, ITERATE_KEY)

    // 将其返回
    return {
        next() {
            const { value, done } = itr.next()
            return {
                // value是值,而非键值对
                value: wrap(value),
                done
            }
        },
        [Symbol.iterator] () {
            return this
        }
    }
}

const MAP_KEY_ITERATE_KEY = Symbol()

function keysIterationMethod() {
    // 获取原始数据对象
    const target = this.raw
    // 通过target.keys获取原始迭代器方法
    const itr = target.keys()

    const wrap = (val) => typeof val === 'object' ? reactive(val) : val

    track(target, MAP_KEY_ITERATE_KEY)

    // 将其返回
    return {
        next() {
            const { value, done } = itr.next()
            return {
                // value是值,而非键值对
                value: wrap(value),
                done
            }
        },
        [Symbol.iterator] () {
            return this
        }
    }
}

const mutableInstrumentations = {
    add(key) {
        // this 仍然指向的是代理对象,通过raw属性获取原始数据对象
        const target = this.raw
        // 先判断值是否已经存在
        const hadKey = target.has(key)

        // 通过原始数据对象执行add方法添加具体的值
        // 注意,这里不再需要.bind了,因为是直接通过targeet调用并执行的
        const res = target.add(key)
        if (!hadKey) {
            // 通过调用trigger函数触发响应,并指定操作类型为ADD
            trigger(target, key, 'ADD')
        }
        return res
    },

    delete(key) {
        const target = this.raw
        const hadKey = target.has(key)
        const res = target.delete(key)
        if (hadKey) {
            trigger(target, key, 'DELETE')
        }
        return res
    },

    get(key) {
        // 获取原始对象
        const target = this.raw
        // 判断读取的key是否存在
        const had = target.has(key)
        // 追踪依赖,建立响应联系
        track(target, key)
        // 如果存在,则返回结果。这里要注意的是,如果得到的结果res仍然是可代理的数据
        // 则要用reactive返回包装后的响应式数据
        if (had) {
            const res = target.get(key)
            return typeof res === 'object' ? reactive(res) : res
        }
    },

    set(key, value) {
        const target = this.raw
        const had = target.has(key)
        const oldValue = target.get(key)

        // 获取原始数据,由于value本身可能已经是原始数据,所以此时value.raw不存在,则直接使用value
        const rawValue = value.raw || value
        
        target.set(key, rawValue)
        if (!had) {
            trigger(target, key, 'ADD')
        } else if (oldValue !== value || (oldValue === oldValue && value === value)) {
            trigger(target, key, 'SET')
        }
    },

    forEach(callback, thisArg) {
        // wrap函数用来把可代理的值转换为响应式数据
        const wrap = (val) => typeof val === 'object' ? reactive(val) : val
        // 取得原始数据对象
        const target = this.raw
        // 与ITERATE_KEY建立响应联系
        track(target, ITERATE_KEY)
        // 通过原始数据对象调用forEach方法
        target.forEach((v, k) => {
            // 手动调用callback,用wrap方法包裹value和key后在传给callback,这样实现深相应
            callback.call(thisArg, wrap(v), wrap(k), this)
        })
    },

    [Symbol.iterator]: iterationMethod,
    
    entries: iterationMethod,

    values: valuesIterationMethod,

    keys: keysIterationMethod
}

const bucket = new WeakMap()

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

// 用一个标志代表是否正在刷新队列
let isFlushing = false

const ITERATE_KEY = Symbol()

function flushJob() {
    // 如果任务正在刷新,则什么都不做
    if (isFlushing) return
    // 设置为true,代表正在刷新
    isFlushing = true
    // 在微任务队列刷新jobQueue队列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        // 结束后重置isFlushing
        isFlushing = false
    })
}

// 封装 createReactive函数,接受参数isShallow,代表是否为浅相应,默认为false,即非浅响应(深相应)
// 接受参数isReadonly 代表是否只读,默认为false,表示非只读,即可修改
function createReactive(obj, isShallow = false, isReadonly = false) {
    return new Proxy(obj, {
        // 拦截读取操作,接受第三个参数receiver
        get(target, key, receiver) {

            // 如果读取的是raw属性,则返回原始数据对象target
            if (key === 'raw') {
                return target
            }

            if (key === 'size') {
                // 如果读取的是size属性
                // 调用track函数建立响应联系
                track(target, ITERATE_KEY)
                // 通过指定第三个参数receiver为原始对象target从而修复问题
                return Reflect.get(target, key, target)
            }
            // 读取其他属性的默认行为
            // 将方法与原始数据对象target绑定后返回
            return mutableInstrumentations[key]
        }, 

        has(target, key) {
            track(target, key)
            return Reflect.has(target, key)
        },

        ownKeys(target) {
            // 如果操作目标target是数组,则使用length属性作为key并建立响应联系
            // 将副作用函数与ITERATE_KEY 关联
            Array.isArray(target) ? track(target, 'length') : track(target, ITERATE_KEY)
            return Reflect.ownKeys(target)
        },

        set(target, key, newValue, receiver) {
            // 拦截设置操作

            // 如果数据是只读的,打印警告信息,直接返回
            if (isReadonly) {
                console.warn(`属性 ${key} 是只读的`)
                return
            }

            // 先获取旧值
            const oldValue = target[key]

            // 如果属性不存在,则说明是添加新属性,否则时设置已有属性
            // type可能是数组或者对象
            // 数组:如果设置的索引值小于长度,那么是SET操作,否则是ADD操作
            const type = Array.isArray(target) ? Number(key) < target.length ? 'SET' : 'ADD' :
            Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
            // 设置属性值
            const res = Reflect.set(target, key, newValue, receiver)

            // target === receiver.raw 说明receiver就是target的代理对象
            if (target === receiver.raw) {
                // 比较新值和旧值,只要当不全等的时候才触发相应
                if (oldValue !== newValue) {
                    // 将type作为第三个参数传递给trigger函数
                    // target[key] = newValue // 使用了Reflect.set设置,不必使用此句
                    // 增加第四个参数,即触发响应的新值
                    trigger(target, key, type, newValue)
                }
            }
            
            return res
            
        },

        deleteProperty(target, key) {

            // 如果数据是只读的,打印警告信息,直接返回
            if (isReadonly) {
                console.warn(`属性 ${key} 是只读的`)
                return
            }

            // 检查被操作的属性是否时对象自己的属性
            const hadKey = Object.prototype.hasOwnProperty.call(target, key)
            
            // 使用Reflect.deleteProperty完成属性的删除
            const res = Reflect.deleteProperty(target, key)
            
            if (res && hadKey) {
                // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
                trigger(target, key, 'DELETE')
            }
            return res
        }
    })
}

function reactive(obj) {
    // 优先通过原始对象obj寻找之前创建的代理对象,如果找到了,直接返回已有的代理对象
    const existionProxy = reactiveMap.get(obj)
    if (existionProxy) {
        return existionProxy
    }
    // 否则,创建新的代理对象
    const proxy = createReactive(obj)
    // 存储到map中,从而米便重复创建
    reactiveMap.set(obj, proxy)
    return proxy
}

function shallowReactive(obj) {
    return createReactive(obj, true)
}

function readonly(obj) {
    return createReactive(obj, false, true)
}

function shallowReadonly(obj) {
    return createReactive(obj, true, true)
}

function track(target, key) {
    // 没有副作用 直接返回
    // 当禁止追踪时,直接返回
    if (!activeEffect || !shouldTrack) return
    
    let depsMap = bucket.get(target)
    if (!depsMap) bucket.set(target, depsMap = new Map())
    
    // dep 预期是text1
    let deps = depsMap.get(key)
    if (!deps) depsMap.set(key, deps = new Set())
    
    // 这样,修改某一属性就详细到代理的某一个对象的某个属性
    deps.add(activeEffect)
    // deps 就是一个与当前副作用函数存在联系的依赖集合
    activeEffect.deps.push(deps)
}

function trigger(target, key, type, newValue) {
    // 设置的时候 通知对应的修改函数
    let depsMap = bucket.get(target)
    if (!depsMap) return
    // 取得与key相关联的副作用函数
    let effects = depsMap.get(key)
    

    const effectsToRun = new Set()
    // 将与key相关联的副作用函数添加到effectsToRun
    effects && effects.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })
    // 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
    // 如果操作类型是SET,并且目标对象是Map类型的数据,也应该触发那些与ITERATE_KEY相关联的副作用函数重新执行
    if (type === 'ADD' || type === 'DELETE' || (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
        // 取得与ITERATE_KEY相关联的副作用函数
        const iterateEffects = depsMap.get(ITERATE_KEY)
        // 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
    }
    
    if (type === 'ADD' || type === 'DELETE' && (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
        // 将与MAP_KEY_ITERATE_KEY相关联的副作用函数也添加到effectsToRun
        const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
    }

    // 当操作类型为ADD并且目标对象是数组时,应该取出并执行属性相关联的副作用函数
    if (type === 'ADD' && Array.isArray(target)) {
        // 取出与length相关联的副作用函数
        const lengthEffects = depsMap.get('length')
        lengthEffects && lengthEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
    }

    // 如果操作目标是数组,并且修改了数组的length属性
    if (Array.isArray(target) && key === 'length') {
        // 对于索引大于或者等于新的length值的元素
        depsMap.forEach((effects, key) => {
            if (key >= newValue) {
                effects.forEach(effectFn => {
                    if (effectFn !== activeEffect) {
                        effectsToRun.add(effectFn)
                    }
                })
            }
        })
    }
    

    effectsToRun.forEach(item => {
        // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
        if (item.options.scheduler) {
            item.options.scheduler(item.func)
        } else {
            item.func()
        }
    })
}

function effect(fn, options = {}) {
    // 这个effectFn是副作用函数
    const effectFn = {
        func: () => {
            cleanup(effectFn)
            // 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
            activeEffect = effectFn
            // 在调用副作用函数之前将当前副作用函数压入栈中
            effectStack.push(effectFn)
            // 将fn的执行结果存储到res中
            const res = fn()
            // 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            return res
        }
    }
    // 将options挂载到effectFn上
    effectFn.options = options
    // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = []
    if (!options.lazy) {
        effectFn.func()
    }
    return effectFn.func
}

function computed(getter) {
    // 使用value来缓存上一次计算的值
    let value
    // dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
    let dirty = true
    // 把getter作为副作用函数,创建一个lazy的effect
    const effectFn = effect(getter, {
        lazy: true,
        // 添加调度器,在调度器中将dirty重置为true
        scheduler() {
            // 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
            if (!dirty) {
                dirty = true
                trigger(obj, 'value')    
            }
            
        }
    })
    const obj = {
        // 当读取到value才执行effectFn
        get value() {
            if (dirty) {
                value = effectFn()
                // 将dirty设置为false,下一次访问直接使用缓存到value的值
                dirty = false
            }
            // 当读取value时,手动调用track函数进行追踪
            track(obj, 'value')
            return value
        }
    }
    return obj
}

// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
    // 定义getter
    let getter
    // 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    // 定义旧值和新值
    let oldValue, newValue
    // 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
    
    // cleanup用来存储用户注册的过期回调
    let cleanup
    // 定义onInvalidate函数
    function onInvalidate(fn) {
        cleanup = fn
    }


    // 提取scheduler调度函数为一个独立的job函数
    const job = () => {
        // 在scheduler中重新执行副作用函数,得到的是新值
        newValue = effectFn()
        // 在调用回调函数cb之前,先调用过期回调
        if (cleanup) {
            cleanup()
        }

        // 当数据变化时,调用回调函数cb
        // 将旧值和新值作为回调函数的参数
        // 将onInvalidate作为回调函数的第三个参数,以便用户使用
        cb(newValue, oldValue, onInvalidate)
        // 更新旧值,不然下一次会得到错误的旧值
        oldValue = newValue
    }

    const effectFn = effect(
        // 触发读取操作,从而建立联系
        () => getter(),
        {
            lazy: true,
            // 使用job函数作为调度器函数
            scheduler: () => {
                // 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
                if (options.flush === 'post') {
                    const p = Promise.resolve()
                    p.then(job)
                } else {
                    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 key in value) {
        traverse(value[key], seen)
    }
    // 暂时的,这个返回value无用
    return value
}

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
}

// 执行区


effect(() => {
    for(const value of pro.values()) {
        console.log(value)
    }
})

pro.set('key2', 'value3')
pro.set('key3', 'value3')

</script>
</html>