05-非原始值的响应式方案

254 阅读37分钟

非原始值的响应式方案

理解Proxy和Reflect

Proxy

vue3的响应式数据是基于proxy实现的

Proxy可以创建一个代理对象,能够实现对其他对象的代理,但是并不能实现对非对象值的代理

代理:对一个对象的基本语义的代理,允许我们拦截重新定义对一个对象的基本操作

基本语义:

  • 类似于读取属性值、设置属性值的操作,就属于基本语义的操作,即基本操作

  • 调用函数也是对一个对象的基本操作

     const fn = (name) => {
         console.log("我是:", name)
     }
     fn()
    

    拦截函数操作:

     const p = new Proxy(fn, {
         apply(target, thisArg, argArray){
             target.call(thisArg, ...argArray)
         }
     })
     p('zsw')
    

复合操作:

Proxy只能拦截一个对象的基本操作,而不能拦截非基本操作

典型的非基本操作有:调用对象下面的方法obj.fn() ,因为其是由两个基本语义组成的,第一个基本语义是get,即先通过get操作得到obj.fn属性,第二个基本语义是函数调用,即通过get得到obj.fn的值后再调用它,也就是上面的apply

Reflect

Reflect是一个全局对象,拥有许多方法:getsetapply

Proxy拦截器中能找到的方法,在Reflect中都能找到同名函数,但是两者的使用还是有一点区别的:

Reflect.get能够接受第三个参数,即指定接收者receiver,可以理解为函数调用过程中的this

 const obj = {
     get foo(){
         return this.foo
     }
 }
 console.log(Reflect.get(obj, 'foo', {foo: 2}))  //会输出2

上一节中代码存在的不足:

  • 实现响应式数据的代码:

     const obj = {foo: 1}
     const p = new Proxy(obj, {
         get(target, key) {
             track(target, key)
             //没用Reflect.get完成读取
             return target[key]
         },
         set(target, key, newVal) {
             //没用Reflect.set完成设置
             target[key] = newVal
             trigger(target, key)
         }
     })
    
  • 修改obj对象,为其添加bar属性

     const obj = {
         foo: 1,
         get bar(){
             return this.foo
         }
     }
    
  • 设置副作用函数

     effect(() =>{
         console.log(p.bar)  //1
     })
    
  • 尝试修改p.foo的值触发副作用函数

     p.foo++
    
  • 分析:

    p.bar是一个访问器属性,因此执行getter函数,由于在getter函数中通过this.foo读取foo属性值,所以理论上应该副作用函数应该也会与foo之间建立联系,但是实际上并没有触发响应,也就是不会建立联系

    真正流程:

    首先通过代理对象p访问p.bar的时候,这会触发代理对象的get拦截函数执行

    get拦截函数中,通过target[key]返回属性值,其中target是原始对象objkey是字符串bar,所以target[key]相当于obj.bar,所以使用p.bar访问属性bar时,他的**getter函数内的this实际指向原始对象obj,最终访问的是obj.foo**

    而访问obj.foo是不会建立响应联系的

  • 解决:

    使用Reflect.get函数

     const p = new Proxy(obj, {
         //接收第三个参数receiver,代表谁在读取属性
         get(target, key, receiver){
             track(target, key)
             return Reflect.get(target, key, receiver)
         },
         //...
     })
    

    现在使用p.bar去访问bar属性,这是第三个参数receiver就是p,可以简单的理解为就是函数调用中的this

    const obj = {
        foo: 1,
        get bar(){
            //现在此处的this就会变成代理对象p
            return this.foo
        }
    }
    

JS对象及Proxy的工作原理

JS对象

JS中的所有对象,分为两类:常规对象和异质对象

任何不属于常规对象的对象都是异质对象,探讨这两个对象,需要先了解对象的内部方法和内部槽

  • 例如,如何判断一个对象是普通对象还是函数?

    在JS中,对象的实际语义是由对象的内部方法指定的

    内部方法:当我们对一个对象进行操作时在引擎内部调用的方法,这些方法对于JS使用者来说并不可见

    一个对象必须部署11个必要的内部方法:[[Get]][[Set]][[Delete]]

    还有两个额外的必要内部方法:[[Call]][[Construct]]

    所以如果要判断一个对象是普通对象还是函数,只需要通过内部方法和内部槽区分,函数对象会部署内部方法[[Call]],普通对象则不会

常规对象:

  1. 对于11个必须的内部方法,必须使用ECMA规范10.1.x节给出的定义实现
  2. 对于内部方法[[Call]],必须使用ECMA规范 10.2.1 节给出的定义实现
  3. 对于内部方法[[Construct]],必须使用ECMA规范10.2.2节给出的定义实现

异质对象:不满足上面三点要求的都是异质对象

Proxy对象

由于proxy对象的内部方法[[Get]]没有使用ECMA规范 10.1.x实现,而是使用 10.5.8 实现,所以proxy是一个异质对象

代理对象和普通对象的区别:

内部方法的多态性,两者对于内部方法的实现不同

具体的不同体现在:

如果在创建代理对象的时候没有指定对应的拦截函数,例如没有指定get()拦截函数,那么通过代理对象访问属性值的时候,代理对象的内部方法[[Get]]会调用原始对象的内部方法[[Get]]来获取属性值,这就是代理透明性质

创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象内部方法和行为的

代理Object

对一个普通对象的读取操作

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在给定的keykey in obj
  • 使用foo...in循环遍历对象:for(const key in obj){}
  • 删除属性:delete obj.foo

现在应该拦截所有的读取操作,以便当数据变化的时候能够正确触发响应

拦截属性读取操作

这里可以直接通过设置get拦截器来实现拦截

拦截in操作符

按照正常思路应该先去找与in操作符对应的拦截函数,但是并不存在该拦截函数

所以这时就需要查看in操作符的相关规范,会发现其运算结果是由一个HasProperty的抽象方法得到的

HasPropery抽象方法的规范内容:

  1. 断言:Type(O)Object
  2. 断言:IsProperyKey(P)true
  3. 返回?O.[[HasProperty]](P)

可以发现,HasProperty抽象方法的返回值是通过调用对象的内部方法[[HasProperty]]得到的

[[HasProperty]]内部方法可以找到对应的拦截函数:has,所以我们可以通过has拦截函数实现对in操作符的代理

这样就可以实现通过in操作符操作响应式数据时,能够建立依赖关系

const p = new Proxy(obj, {
    has(target, key, receiver){
        track(target, key)
        return Reflect.has(target, key, receiver)
    },
    //...
})

拦截for...in循环

for...in也没有一个对应的拦截函数可以拦截,所以需要查阅规范

实际上obj就是被for...in循环遍历的对象,其关键点在于使用Reflect.ownKeys(obj)来获取只属于自身拥有的键

所以我们可以使用ownKeys拦截函数来拦截Reflect.ownKeys操作

const ITERATE_KEY = Symbol()
const p = new Proxy(obj, {
    ownKeys(target){
        track(target, ITERATE_KEY)
        return Reflect.ownKeys(target)
    },
    //...
})

使用ITERATE_KEY的原因:

这是因为ownKeysget/set拦截函数不同

get/set中,可以得到具体操作的key,所以只需要在该属性与副作用函数之间建立联系即可

ownKeys中,只能拿到目标对象target,获取这个对象所有属于自己的键值,这个操作明显不与任何具体的键进行绑定,因此我们只能构造唯一的key值作为标识,即ITERATE_KEY

问题:什么情况下对数据的操作需要触发与ITERATE_KEY相关联的副作用函数重新执行?

  • 副作用函数代码:

    const obj = {foo: 1}
    const p = new Proxy(obj, {/* ... */})
    effect(() =>{
        //for...in循环
        for (const key in p) {
            console.log(key);
        }
    })
    
  • 添加对象属性bar

    p.bar = 2
    
  • 添加属性之后应该让for...in循环重新执行,需要触发与ITERATE_KET相关联的副作用函数重新执行,但是并没有重新执行

    问题出在set拦截函数上,set函数接收的key只是字符串bar,所以副作用函数也只是与bar建立的联系,而**for...in是在副作用函数与ITERATE_KEY之间建立联系**,这和bar没关系,所以不能触发响应

  • 解决:添加属性时,将那些与ITERATE_KEY相关联的副作用函数也取出来执行

    function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const iterateEffects = depsMap.get(ITERATE_KEY)
        const effectsToRun = new Set()
        // 将与key相关的副作用函数添加到待执行的副作用函数集合中
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
        //将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
        //执行副作用函数
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }
    
  • 除了添加对象属性,现在我们研究修改对象属性

    p.foo = 2
    
  • 目前的情况是修改对象属性,会让for...in循环重新执行一遍

    因为只是修改属性,我们并不需要触发其重新执行,否则会造成不必要的性能开销

  • 解决:在set拦截器中区分操作的类型,并且将类型传递给trigger函数进行区分

    //将操作类型封装为一个枚举值
    const TriggerType = {
        SET: 'SET',
        ADD: 'ADD'
    }
    
    //修改set拦截器
    set(target, key, newVal, receiver) {
        //如果属性不存在,就说明添加新属性,否则就是设置已有属性
        const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
        //修改属性值
        const res = Reflect.set(target, key, newVal, receiver)
        //触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
        trigger(target, key, type)
        //设置成功返回true
        return res
    },
    
    function trigger(target, key, type) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const iterateEffects = depsMap.get(ITERATE_KEY)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
        //只有操作类型为ADD的时候才触发与ITERATE_KEY相关联的副作用函数重新执行
        if(type === TriggerType.ADD){
            //将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
            iterateEffects && iterateEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) effectsToRun.add(effectFn)
            })
        }
        //执行副作用函数
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }
    

拦截delete操作符

由于找不到能够直接操作delete的拦截函数,所以需要查看规范

可知delete内部的行为依赖[[Delete]]内部方法,可以使用deleteProperty拦截

//拦截delete操作
deleteProperty(target, key){
    //检查被操作的属性是否是对象自己的属性
    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
}

由于删除操作会让键变少,所以会影响for...in循环,所以应该触发与ITERATE_KEY相关联的副作用函数重新执行

function trigger(target, key, type) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    const iterateEffects = depsMap.get(ITERATE_KEY)
    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
        if (effectFn !== activeEffect) effectsToRun.add(effectFn)
    })
    //只有操作类型为ADD的时候才触发与ITERATE_KEY相关联的副作用函数重新执行
    if(type === TriggerType.ADD || type === TriggerType.DELETE){
        //将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
    }
    effectsToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

合理触发响应

触发响应不仅需要以上工作,还有很多边界条件需要考虑

值没有发生变化

值没有发生变化的时候,不应该触发响应

const obj = {foo: 1}
const p = new Proxy(obj, {/* ... */})

effect(() => {
    console.log(p.foo)
})

//设置p.foo的值,但是值没有变化
p.foo = 1

但是现在还是可以触发响应的,因为在set拦截中,只要绑定的值被设置,则需要重新触发副作用函数

解决:修改set响应函数,在调用trigger之前判断值是否真的改变了

特例:对NaN进行处理不能简单的判断新旧值,因为NaN === NaN会得到false

const p = new Proxy(obj, {
        set(target, key, newVal, receiver){
            const oldVal = target[key]
        const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
        const res = Reflect.set(target, key, newVal, receiver)
        //比较新旧值
        if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
            trigger(target, key, type)
        }
        return res
    }
})

从原型上继承属性

首先先需要封装一个reactive函数,该函数接受一个对象作为参数,并返回其创建的响应式数据

function reactive(obj) {
    return new Proxy(obj, {
        //省略前文的拦截函数
    })
}
  • 问题例子:

    在下述代码中,最后一行让副作用函数重新执行两次

    const obj = {}
    const proto = {bar: 1}
    const child = reactive(obj)
    const parent = reactive(proto)
    Object.setPrototypeOf(child, parent)
    effect(() => {
        console.log(child.bar);	// 1
    })
    child.bar = 2	// 2
    
  • 绑定分析:

    在上述代码第七行中,要获取child.bar的值

    所以必须调用child对象所部署的[[Get]]内部方法,所以会child.bar与副作用函数之间建立响应联系

    找不到child.bar的值之后,根据规范:就必须去parent对象上寻找,最终在parent上找到

    由于parent也是响应式数据,所以也必须调用其部署的[[Get]]方法,使parent.bar与副作用函数之间建立响应联系

  • 触发分析:

    在上述代码的最后一行中,要设置child.bar的值

    所以必须调用[[Set]]内部方法,由于与副作用函数建立了联系,所以此处就会触发一次副作用函数

    由于找不到要设置的属性,根据规范:则要去原型上面找,也就是**parent,需要执行其[[set]]内部方法,此时也会重新调用一次副作用函数**

  • 解决思路:

    只需要屏蔽其中一次副作用函数的执行就可以了

    我们选择屏蔽parent.bar触发的那次副作用函数屏蔽掉

  • 解决前置基础:

    receiver参数在父子对象上的表现是有区别的

    child对象:

    set(target, key, value, receiver){
        //target是原始对象obj
        //receiver是代理对象child
    }
    

    parent对象:

    set(target, key, value, receiver){
        //target是原始对象proto
        //receiver仍然是代理对象child
    }
    

    从这里可以看出,当parent代理对象的set拦截函数执行时,此时target是原始对象proto,而receiver仍然是代理对象child,而不是target的代理对象

    receiver 存在的意义就是为了正确的在陷阱中传递上下文,确保陷阱函数中调用者的正确的上下文访问

    Proxy 中接受的 Receiver 形参表示代理对象本身或者继承于代理对象的对象

  • 解决:

    由于由上述的前置知识,所以我们只需要判断receiver是否是target的代理对象即可只有是target的代理对象才触发更新,这样就能屏蔽由原型引起的更新了

    首先需要get拦截函数添加一个能力让其访问raw能返回target

    get(target, key, receiver) {
        //代理对象可以通过raw属性访问原始数据
        if(key === 'raw') return target
        track(target, key)
        return Reflect.get(target, key, receiver)
    }
    
    child.raw === obj	//true
    

    然后在**set拦截函数中判断receiver是不是target的代理对象**即可

    set(target, key, newVal, receiver) {
        const oldVal = target[key]
        const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
        const res = Reflect.set(target, key, newVal, receiver)
        //target === target.raw说明receiver就是target的代理对象
        if(target === receiver.raw){
            //比较新旧值
            if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
                //触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
                trigger(target, key, type)
            }
        }
        return res
    },
    

浅响应和深响应

现在实现的reactive响应式都是浅响应,而没有做到深响应

  • 问题:

    在下面代码中,在副作用函数中读取obj.foo.bar,后续对其进行修改并不能让副作用函数重新执行

    const obj = reactive({foo: {bar: 1}})
    effect(() => {
        console.log(obj.foo.bar);
    })
    obj.foo.bar = 2
    
  • 分析:

    读取obj.foo.bar的时候,首先要先读取obj.foo的值

    我们使用Reflect.get函数返回obj.foo的结果,由于通过该方法得到的结果是一个普通对象,并不是一个响应式数据,所以在副作用函数中访问obj.foo.bar是不能建立响应联系的

    所以修改该对象的时候也就不会触发副作用函数了

  • 解决:

    只需要Reflect.get()的返回结果做一层封装即可

    get(target, key, receiver) {
        if(key === 'raw') return target
        track(target, key)
        //得到原始结果
        const res = Reflect.get(target, key, receiver)
        //判断拿到的数据是不是一个对象
        if(typeof res === 'object' && res !== null){
            //是对象的话要调用reactive将结果包装成响应式数据并返回
            return reactive(res)
        }
        //返回属性值
        return res
    }
    

这样就可以实现深响应了,但是并非所有情况我们都希望深响应,这就催生了**shallowReactive,即浅响应**

也就是只有对象的第一层属性是响应的

这样只需要对响应函数在进行一次封装即可:

function createReactive(obj, isShallow = false) {
    return new Proxy(obj, {
        //拦截读取操作
        get(target, key, receiver) {
            if (key === 'raw') return target
            track(target, key)
            const res = Reflect.get(target, key, receiver)
            //如果是浅响应,则直接返回原始值
            if(isShallow) return res
            //判断拿到的数据是不是一个对象
            if (typeof res === 'object' && res !== null) {
                //是对象的话要调用reactive将结果包装成响应式数据并返回
                return reactive(res)
            }
            //返回属性值
            return res
        }
        /* 省略其他操作 */
    })
}

统一对外暴露接口:

// 深响应函数
function reactive(obj) {
    return createReactive(obj)
}

//浅响应函数
function shallowReactive(obj){
    return createReactive(obj, true)
}

只读和浅只读

只读

希望一些数据只是可读,而不可以做修改

如果用户要对其进行修改,就需要发出一条警告消息,这样就实现了对数据的保护

所以我们可以通过修改拦截函数来实现:

首先,为createReactive添加第三个参数isReadonly,并且修改setdeleteProety函数,因为只读意味着既不可以设置属性值,也不可以删除

function createReactive(obj, isShallow = false, isReadonly = false){
    return new Proxy(obj, {
        set(target, key, newVal, receiver) {
            //如果是只读的,则打印警告并返回
            if(isReadonly){
                console.warn(`属性${key}是只读的`)
                return true
            }
            const oldVal = target[key]
            const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
            const res = Reflect.set(target, key, newVal, receiver)
            if (target === receiver.raw) {
                if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
                    trigger(target, key, type)
                }
            }
            return res
        },
        deleteProperty(target, key) {
            //如果是只读的,则打印警告并返回
            if(isReadonly){
                console.warn(`属性${key}是只读的`)
                return true
            }
            const hadKey = Object.prototype.hasOwnProperty.call(target, key)
            const res = Reflect.deleteProperty(target, key)
            if (res && hadKey) {
                trigger(target, key, 'DELETE')
            }
            return res
        }
    })
}

其次,既然是只读属性,也就没必要让其建立响应联系,所以在副作用函数读取一个只读属性的时候,不需要调用track函数进行追踪响应,所以我们应该修改get拦截函数

get(target, key, receiver) {
    if (key === 'raw') return target
    //非只读的时候才建立响应联系
    if(!isReadonly){
        //追踪函数,添加副作用函数
        track(target, key)
    }
    const res = Reflect.get(target, key, receiver)
    if(isShallow) return res
    if (typeof res === 'object' && res !== null) {
        return reactive(res)
    }
    return res
},

深只读

在返回属性之前,判断是否是只读的,如果是只读的则再利用readonly进行包装,并把包装后的只读对象返回

get(target, key, receiver) {
    if (key === 'raw') return target
    if(!isReadonly){
        track(target, key)
    }
    const res = Reflect.get(target, key, receiver)
    if(isShallow) return res
    //判断拿到的数据是不是一个对象
    if (typeof res === 'object' && res !== null) {
        //是对象的话要调用reactive将结果包装成响应式数据并返回,
        //如果只读,则调用readonly对结果进行包装
        return isReadonly ? readonly(res) : reactive(res)
    }
    return res
},

进行统一对外暴露接口:

//只读函数
function readonly(obj){
    return createReactive(obj, false, true)
}

//浅只读函数
function shallowReadonly(obj){
    return createReactive(obj, true, true)
}

代理数组

数组只是一个特殊的对象,上述的大部分代码还可以正常使用

但是数组和对象还是有区别的:

  • 数组是一个异质对象,因为数组对象中的[[DefineOwnProperty]]与常规对象不同,但是其他内部方法的逻辑与常规对象相同

  • 数组的读取操作与普通对象存在不同,数组的读取操作:

    • 通过索引访问元素
    • 访问数组长度
    • 把数组作为对象,使用for...in循环
    • 使用for...of迭代遍历
    • 数组的原型方法:concatjoineverysome
  • 数组的设置操作:

    • 通过索引修改数组元素值
    • 修改数组长度
    • 数组的栈方法:pushpop
    • 数组的原型方法:splicefill

数组索引与length

如果只是单纯的通过数组的索引值去设置元素的话,已经能够建立响应联系

const arr = reactive(['foo'])
effect(() => {
    console.log(arr[0]);	//'foo'
})
arr[0] = 'bar'	//能够触发响应

但是通过索引值设置数组元素与设置对象属性值还存在根本上的不同,因为数组对象部署的内部方法[[DefineOwnProperty]]不同于常规对象:

规范中对其的描述:

如果设置的索引值大于当前数组的长度,那么就要更新数组的长度

所以通过索引设置元素值的时候,可能会隐式修改length的属性值触发响应时,也应该触发于length属性相关联的副作用函数重新执行

  • 问题一:修改索引值引起length变化

    const arr = reactive(['foo'])
    effect(() => {
        console.log(arr.length);	// 1
    })
    arr[1] = 'bar'	//应该能够重新触发响应
    
  • 实现:

    首先需要修改set拦截函数,让其对数组做一个判断,并区分SET和ADD操作

     set(target, key, newVal, receiver) {
         if(isReadonly){
             console.warn(`属性${key}是只读的`)
             return true
         }
         const oldVal = target[key]
         //如果属性不存在,就说明添加新属性,否则就是设置已有属性
         const type = Array.isArray(target)
         //如果代理目标是数组,则检测被设置的索引值是否小于数组长度
         //如果是,则视作SET操作,狗则视为ADD操作
         ? Number(key) < target.length ? 'SET' : 'ADD'   
         : Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
         //修改属性值
         const res = Reflect.set(target, key, newVal, receiver)
         if (target === receiver.raw) {
             if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
                 trigger(target, key, type)
             }
         }
         return res
     },
    

    其次,要在trigger函数中针对不同操作进行修改

    function trigger(target, key, type) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const iterateEffects = depsMap.get(ITERATE_KEY)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
        if (type === TriggerType.ADD || type === TriggerType.DELETE) {
            iterateEffects && iterateEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) effectsToRun.add(effectFn)
            })
        }
        //当操作类型为ADD且目标对象是数组的时候,应该取出并执行那些与length属性相关联的副作用函数
        if(type === TriggerType.ADD && Array.isArray(target)){
            //取出与length相关联的副作用函数
            const lengthEffects = depsMap.get('length')
            //将这些副作用函数添加到effectToRun中执行
            lengthEffects && lengthEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) effectsToRun.add(effectFn)
            })
        }
    
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }
    
  • 问题二:数组的length属性影响数组元素

    const arr = reactive(['foo'])
    effect(() => {
        console.log(arr[0]);	// 1
    })
    arr.length = 0	//应该能够重新触发响应
    
  • 实现:

    由于只有当索引值大于或等于新的length属性的元素才需要重新触发响应,所以我们在调用trigger函数的时候,把新的属性值也传递过去

     set(target, key, newVal, receiver) {
         if(isReadonly){
             console.warn(`属性${key}是只读的`)
             return true
         }
         const type = Array.isArray(target)
             //如果代理目标是数组,则检测被设置的索引值是否小于数组长度
             //如果是,则视作SET操作,狗则视为ADD操作
             ? Number(key) < target.length ? 'SET' : 'ADD'   
             : Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
         const res = Reflect.set(target, key, newVal, receiver)
         if (target === receiver.raw) {
             if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
                 //增加第四个参数,即触发响应的新值
                 trigger(target, key, type, newVal)
             }
         }
         return res
     },
    

    修改trigger函数

    //增加第四个参数,代表新值
    function trigger(target, key, type, newVal) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const iterateEffects = depsMap.get(ITERATE_KEY)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
        if (type === TriggerType.ADD || type === TriggerType.DELETE) {
            iterateEffects && iterateEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) effectsToRun.add(effectFn)
            })
        }
        if(type === TriggerType.ADD && Array.isArray(target)){
            const lengthEffects = depsMap.get('length')
            lengthEffects && lengthEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) effectsToRun.add(effectFn)
            })
        }
        //如果操作目标是数组,并且修改了数组的length属性
        if(Array.isArray(target) && key === 'length'){
            //对于所有大于或等于新的length值的元素
            //需要把所有相关联得副作用函数取出并添加到effectToRun中待执行
            depsMap.forEach((effects, key) => {
                if(key >= newVal){
                    effects.forEach(effectFn => {
                        if(effectFn !== activeEffect) effectsToRun.add(effectFn)
                    })
                }
            })
        }
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }
    

遍历数组

使用for...in遍历数组:

  • 本质上数组也是一个对象,而数组对象和常规对象的不同仅体现在[[DefineOwnPropery]] 这个内部方法上,所以使用**for...in循环遍历数组与常规对象并没有差异**,因此也是同样在ownKeys拦截函数上进行拦截

  • 在普通对象中,只有添加或删除属性值才会影响for...in循环的结果;但是数组不同,数组本质上只要length属性被修改,那么for...in循环对数组的遍历结果就会改变,所以这时候就应该重新触发响应

  • 实现:ownKeys中判断当前操作目标是不是数组,是的话则使用length作为key去建立响应联系

    ownKeys(target) {
        //追踪函数,添加副作用函数,如果有数组则使用length作为key建立追踪
        track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
        //拦截ownKeys操作
        return Reflect.ownKeys(target)
    },
    

使用for...of遍历数组:

  • for...of遍历是用来遍历可迭代对象的,可迭代对象就是该对象或该对象的原型实现了@@iterator方法,而@@iterator也就是Symbol.iterator这个值(略)

  • for...of的规范中,可以看到数组迭代器的执行会读取数组的length属性如果迭代的是数组元素值,那么还会读取数组的索引

    //模拟实现代码
    const arr = [1, 2, 3, 4, 5]
    arr[Symbol.iterator] = function() {
        const target = this
        const len = target.length
        let index = 0
        return {
            next() {
                return {
                    value: index < len ? target[index] : undefined,
                    done: index++ >= len
                }
            }
        }
    }
    
  • 从上述代码可以看出,只需要在副作用函数与数组长度和索引直接建立响应式联系即可

    而在我们之前的代码中,已经实现了该需求,所以现在无需改动代码技能实现响应for...of

数组的values方法:

  • 数组values方法的返回值实际上就是数组内建的迭代器

    console.log(Array.prototype.values === Array.prototype[Symbol.iterator])	//true
    
  • 所以不增加任何代码的情况下,也能让数组的迭代器方法正常工作

for...ofvalues存在的问题:

两者在直接修改length的时候都会报错,因为不管是调用for...of还是调用values,都会读取数组上的Symbol.iterator属性,该属性是一个symbol值,所以为了避免发生上述错误,以及性能消耗,我们不应该在副作用函数与这类symbol之间建立响应联系,所以应该修改get拦截函数:

get(target, key, receiver) {
            if (key === 'raw') return target
            //非只读并且key不=为symbol的时候才建立响应联系
            if(!isReadonly && typeof key !== 'symbol'){
                //追踪函数,添加副作用函数
                track(target, key)
            }
            const res = Reflect.get(target, key, receiver)
            if(isShallow) return res
            if (typeof res === 'object' && res !== null) {
                return isReadonly ? readonly(res) : reactive(res)
            }
            return res
        },

数组的查找方式

通过上文,我们已经知道数组方法内部其实都依赖了对象的基本语义,所以大多数情况下,我们并不需要做处理就可以让这些方法按预期工作

例如:includes

正常情况下通过includes查找给定元素:

const arr = reactive([1, 2])
effect(() => {
    console.log(arr.includes(1));	//初始打印 true
})
arr[0] = 3	//重新执行副作用函数,打印 false

但是,includes方法并不会总是按照预期执行

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0]));	//返回 false

includes的规范:

  1. 让O的值为 ? ToObject(this value),而这里的this其实就是代理对象arr
  2. 省略一部分
  3. includes会通过索引读取数组元素的值,而这里的O是代理对象arr,并且,通过代理对象访问元素值得时候,如果这些值可以被代理,那么得到得值则会是新的代理对象

通过规范,我们进一步来分析arr.includes(arr[0])这句代码:

由于**arr[0]直接访问了代理对象**,并且值为非原始值,所以会得到一个新的代理对象

includes方法内部会通过arr访问数组元素,从而也得到一个代理对象

但是现在两个代理对象并不相同,所以才会输出false

解决:

定义一个map结构存储原始对象到代理对象的映射,每次调用reactive之前先来这里查找有没有相应的代理对象,有的话直接返回,没有的话则创建,并且将其新创建的代理对象存储到这个结构中

//定义一个Map实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()

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

新问题:

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj));	//false

分析:

因为includes内部的this指向是代理对象arr,并且再获取数组元素得到的值也是代理对象所以拿原始对象obj去找肯定找不到,因此返回false

解决:

重写includes方法,由于includes方法本质上也就是读取代理对象arrincludes属性,所以也会触发get拦截函数,所以下面第六行也就是拦截includes方法,返回自定义的includes方法

get(target, key, receiver) {
    //代理对象可以通过raw属性访问原始数据
    if (key === 'raw') return target
    //如果操作的目标对象是数组,并且key存在于arrayInstrumentations上
    //那么返回定义在arrayInstrumentaions上的值,返回的值也就是重新定义的includes
    if(Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)){
        return Reflect.get(arrayInstrumentations, key, receiver)
    }
    if(!isReadonly && typeof key !== 'symbol'){
        track(target, key)
    }
    const res = Reflect.get(target, key, receiver)
    if(isShallow) return res
    if (typeof res === 'object' && res !== null) {
        return isReadonly ? readonly(res) : reactive(res)
    }
    return res
},

自定义includes方法,其中第五行是先实现includes的默认行为,如果找不到再去原始数组中查找

const originMethod = Array.prototype.includes
const arrayInstrumentations = {
    includes: function(...args) {
        //this是代理对象,先在代理对象中查找,将结果存储到res中
        let res = originMethod.apply(this, args)
        //没有找到
        if(res === false){
            //通过this.raw拿到原始数据,再在原始数据中查找并更新res
            res = originMethod.apply(this.raw, args)
        }
        return res
    }
}

类似数组方法**indexOflastIndexOf也要进行重写**:

const arrayInstrumentations = {}
;['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){
            //通过this.raw拿到原始数据,再在原始数据中查找并更新res
            res = originMethod.apply(this.raw, args)
        }
        return res
    }
})

隐式修改数组长度的原型方法

这种隐式修改数组长度的方法主要是数组的栈方法:pushpopshiftunshift

通过push的规范,我们可以知道,在该流程中不仅需要读取数组的length属性,还要设置这个属性

所以这样的话,会导致两个独立的副作用函数相互影响,最终导致爆栈

const arr = reactive([])
effect(() => {
    arr.push(1)
})
effect(() => {
    arr.push(2)
})

分析:

第一个副作用函数执行,会读取length属性,所以第一个副作用函数执行完毕后,会与length建立响应联系

第二个副作用函数去执行的时候,因为会读取length属性,所以会与length建立响应联系

现在第二个副作用函数由于push会间接设置length的值,导致会把length关联的所有副作用函数全部取出执行,即第一个副作用函数会被取出执行

第二个副作用函数还没执行完毕,第一个副作用函数就会开始执行,第一个副作用函数开始执行会导致第二个副作用函数执行,循环往复则导致调用栈溢出

解决:

问题的核心就是push方法调用会间接读取length属性,所以只要屏蔽对length属性的读取,就能避免与副作用函数建立响应联系,故需要重写push方法:

//标记变量,代表是否进行追踪,默认值允许追踪
let shouldTrack = true
//重写数组push方法
;['push'].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
    }
})

这样在调用默认方法之前先禁止追踪,调用完在恢复原来行为,所以还要改写一下track函数:

//追踪函数,添加副作用函数逻辑
function track(target, key) {
    //当禁止追踪时,直接返回
    if (!activeEffect || !shouldTrack) return
    /** 省略 */
}

代理Set和Map

如何代理Set和Map

由于**SetMap结构是通过特定的属性和方法来操作自身**的,与普通对象并不一样,所以不能用普通对象的响应式操作

size方法调用报错:

const s = new Set([1, 2, 3])
const p = new Proxy(s, {})
console.log(p.size);
//报错:Uncaught TypeError: Method get Set.prototype.size called on incompatible receiver #<Set>

报错的大概意思是:在不兼容的receiver上调用了get Set.prototype.size方法

规范:

  1. Set.prototype.size是一个访问器属性,他的set访问器函数是undefined,他的get访问器函数会执行以下步骤
  2. S的值为this,此处的this就是代理对象p
  3. 执行? RequireInternalSlot(S, [[SetData]])通过抽象方法来检查S是否存在内部槽[[SetData]],但是代理对象p并不存在,所以就会抛出错误

解决:

const p = new Proxy(s, {
    get(target, key, receiver){
        //拦截读取size属性的行为
        if(key === 'size'){
            //通过指定第三个参数receiver为原始对象target来解决问题
            //因为原始对象就存在[[SetData]]内部槽
            return Reflect.get(target, key, target)
        }
        //读取其他属性的默认行为
        return Reflect.get(target, key, receiver)
    }
})

delete方法调用报错:

p.delete(1)

分析:

此处的报错与size十分相似,但是两者的访问又是不同的

当访问p.size的时候,访问器属性的getter函数会立即执行,所以我们可以通过修改receiver来改变getter函数的this指向

访问delete的时候,delete方法并没有执行,真正使其执行的语句是p.delete(1)这句函数调用

所以我们无论怎么修改receiverdelete方法执行时this都会指向代理对象p,而不会指向原始对象

解决:

delete方法与原始数据对象绑定即可

const p = new Proxy(s, {
    get(target, key, receiver){
        if(key === 'size'){
            return Reflect.get(target, key, target)
        }
        //该方法与原始数据对象target绑定后返回
        return target[key].bind(target)
    }
})

建立响应联系

  • 目标:

    建立响应联系之后,使用add能够触发响应

    const p = reactive(new Set([1, 2, 3]))
    effect(() => {
        //副作用函数内访问size属性
        console.log(p.size)
    })
    //添加1之后应该触发响应
    p.add(1)
    
  • 思路:

    需要在访问size属性的时候调用track函数进行依赖追踪

    然后add方法时调用trigger函数触发响应

  • 实现:

    进行依赖追踪:

    此处需要使用ITERATE_KEY与副作用函数之间建立联系,因为任何新增、删除操作都会影响size属性

    function createReactive(obj, isShallow = false, isReadonly = false) {
        return new Proxy(obj, {
            get(target, key, receiver) {
              if (key === 'size') {
                //调用track函数建立响应联系
                track(target, ITERATE_KEY)
                return Reflect.get(target, key, target)
              }
                //返回定义在mutableInstrumentations对象下的方法
              return mutableInstrumentations[key]
            }
        })
    }
    

    重写add方法:

    此处定义一个对象mutableInstrumentations,存储所有自定义方法

    //将所有自定义实现的方法定义到该对象上
    const mutableInstrumentations = {
        //自定义add方法
        add(key) {
            //this返回一个代理对象,通过raw属性获取原始数据对象
            const target = this.raw
            //通过原始数据对象执行add方法添加具体的值
            //这里不需要bind,因为直接通过target调用并执行的
            const res = target.add(key)
            //调用trigger函数触发响应,并指定操作类型为ADD
            trigger(target, key, 'ADD')
            //返回操作结果
            return res
        }
    }
    
  • 优化:

    只有添加的元素不存在与原来Set集合中才触发响应,否则不触发

    add(key) {
        const target = this.raw
        //先判断值是否已经存在
        const hadKey = target.has(key)
        const res = target.add(key)
        //只有值不在的情况下才触发响应
        if(!hadKey) trigger(target, key, 'ADD')
        return res
    }
    
  • 按照同样的思路,实现delete

    //自定义delete方法
    delete(key) {
        //this返回一个代理对象,通过raw属性获取原始数据对象
        const target = this.raw
        //先判断值是否已经存在
        const hadKey = target.has(key)
        // 删除值
        const res = target.delete(key)
        //只有值在的情况下才触发响应
        if(hadKey) trigger(target, key, 'DELETE')
        //返回操作结果
        return res
    }
    

避免污染原始数据

首先,由于要借助map类型的数据开讲解这一节,所以我们先实现一下这两个方法:

get方法:

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

set方法:

//自定义set方法
set(key, value){
    //获取原始对象
    const target = this.raw
    //判断读取的key是否存在
    const had = target.has(key)
    //获取旧值
    const oldValue = target.get(key)
    //设置新值
    target.set(key, value)
    if(!had){
        //如果不存在,则说明是新增,所以是ADD操作类型
        trigger(target, key, 'ADD')
    }else if(oldValue !== value || (oldValue === oldValue && value === value)){
        //如果存在,并且值变了,则说明是SET类型操作,修改
        trigger(target, key, 'SET')
    }
}

问题:

如下述代码所示,现在的代码通过操作原始数据m来设置数据值,会触发副作用函数重新执行

const m = new Map()
const p1 = reactive(m)
const p2 = reactive(new Map())
//p1有一个键值对是代理对象p2
p1.set('p2', p2)

effect(() => {
    //通过原始数据m访问p2
  	console.log(m.get('p2').size)
})
//通过原始数据m为p2设置一个键值对foo ---> 1
m.get('p2').set('foo', 1)

分析:

在上述的set方法之中,我们将value原封不动的设置到原始数据上,所以这样会导致,如果value是响应式数据,则意味着设置到原始对象上的也是响应式数据,这种响应式数据设置到原始数据上的行为称为数据污染

解决:

只需要在调用target.set之前对值进行检查即可 ,如果是响应式数据,则通过raw属性获取原始数据,再把原始数据设置到target

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

由于遍历操作与键值对的数量有关,所以任何会修改Map对象键值对数量的操作都应该触发副作用函数重新执行,例如adddelete等方法

所以forEach函数被调用的时候,我们都应该让副作用函数与ITERATE_KEY建立响应联系

//自定义forEach函数
forEach(callback){
    //获取原始对象
    const target = this.raw
    //与ITERATE_KEY建立相应联系
    track(target, ITERATE_KEY)
    //通过原始数据对象调用forEach方法,并把callback传递过去
    target.forEach(callback)
}

但是上述的代码还不完善,现在有一个问题:

const key = { key: 1 }
const value = new Set([1, 2, 3])
const p = reactive(new Map([
  [key, value]
]))

effect(() => {
  p.forEach(function (value, key) {
    console.log(value.size) // 3
  })
})
p.get(key).delete(1)

在上述代码中,执行了最后一行代码后,并未能重新出发副作用响应函数,这是不符合逻辑的,因为我们通过reactive去代理对象,也就是想要使用深响应,所以代理对象的值如果变化的话, 应该触发forEach重新执行

原因就是使用value.size去访问size的时候,由于value是原始数据类型,所以并不会建立响应联系,要解决这个问题,只要将callback的参数转化成响应式的即可

//自定义forEach函数
forEach(callback){
    //wrap函数可以把代理的值转化成响应式数据
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    //获取原始对象
    const target = this.raw
    //与ITERATE_KEY建立相应联系
    track(target, ITERATE_KEY)
    //通过原始数据对象调用forEach方法,并把callback传递过去
    target.forEach((v, k) => {
        //手动调用callback,用wrap函数包裹value和key后再传参给callback,这样就实现了深响应
        callback(wrap(v), wrap(k), this)
    })
}

完善上述forEach代码,由于**forEach还可以接收第二个参数thisArg**,可以指定callback函数执行时的this值:

forEach(callback, thisArg){
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    const target = this.raw
    track(target, ITERATE_KEY)
    target.forEach((v, k) => {
        //通过.call调用callback,并传递thisArg
        callback.call(thisArg, wrap(v), wrap(k), this)
    })
}

现在for...inforEach遍历对象都是建立在ITERATE_KEY与副作用函数之间,但是两者还是存在本质的不同的:for...in循环遍历对象,只关心键,不关心值,只有新增删除操作才能触发副作用函数重新执行,而这个规则不适用于forEach

所以现在修改Map数据的时候,forEach不会被触发执行,故应该修改trigger

/** 省略 */
if (type === TriggerType.ADD || type === TriggerType.DELETE || 
    //如果操作类型是SET,并且目标对象是Map类型的数据,那么也应该触发于ITERATOR_KEY关联的副作用函数
    (type === TriggerType.SET && Object.prototype.toString.call(target) === '[object Map]')
   ) {
    //将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
    iterateEffects && iterateEffects.forEach(effectFn => {
        if (effectFn !== activeEffect) effectsToRun.add(effectFn)
    })
}

迭代器方法

集合类型有三个迭代器方法:entrieskeysvalues

调用上面的方法会返回迭代器对象,可以使用for...of循环遍历

其中,entries[[Symbol.iterator]]是等价的,两者都可以使用for...of进行迭代

const m = new Map([
    ['key1', 'value1'],
    ['key2', 'value2'],
])
for(const [key, value] of m.entries()){
    console.log(key, value)
}
for(const [key, value] of m){
    console.log(key, value) 
}
//输出一样:
//key1 value1
// key2 value2

直接使用[[Symbol.iterator]]也可以获得迭代器对象,可以手动调用迭代器的next方法获取相应的值

初步实现响应式:

const p = reactive(new Map([
  ['key1', 'value1'],
  ['key2', 'value2']
]))
effect(() => {
  for(const [key, value] of p){
    console.log(key, value)
  }
})
p.set('key3', 'value3')
//TypeError: p is not iterable

使用上面这段代码是会报错的,因为p并不是一个迭代器,而是一个代理对象

而要,所以现在我们[[Symbol.iterator]]添加到mutableInstrumentations

//自定义[[Symbol.iterator]]
[Symbol.iterator](){
    //获取原始数据对象target
    const target = this.raw
    //获取原始迭代器方法
    const itr = target[Symbol.iterator]()
    //将迭代器方法返回
    return  itr
}

接下来要一步步完善这个自定义方法

首先,与forEach一样,如果迭代产生的值也是可以被代理的,那么应该将其包装成响应式数据

	[Symbol.iterator](){
        const target = this.raw
        const itr = target[Symbol.iterator]()
        const wrap = (val) => typeof val === 'object' ? reactive(val) : val
        //返回自定义迭代器对象
        return  {
            next(){
                //调用原始迭代器的next方法获取value和done
                const {value, done} = itr.next()
                return {
                    //如果value不是undefined,则对其进行包装
                    value: value ? [wrap(value[0]), wrap(value[1])] : value,
                    done
                }
            }
        }
    }

然后,要for...of进行追踪,调用track函数,让副作用函数与ITERATE_KEY建立联系

由于迭代操作与集合中的元素数量有关,所以只要集合的size发生变化,就应该触发副作用函数重新执行

[Symbol.iterator](){
    const target = this.raw
    const itr = target[Symbol.iterator]()
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    //调用track函数建立响应联系
    track(target, ITERATE_KEY)
    return  {
        next(){
            const {value, done} = itr.next()
            return {
                value: value ? [wrap(value[0]), wrap(value[1])] : value,
                done
            }
        }
    }
}

其次,由于p.entriesp[Symbol.iterator]等价,所以可以使用同样的方式对p.entries进行拦截

const mutableInstrumentations = {
    //共用一个方法
    [Symbol.iterator]: iterationMethod,
    entries: iterationMethod
}
//迭代器函数,抽离为独立的函数,便于复用
function iterationMethod(){
    //获取原始数据对象target
    const target = this.raw
    //获取原始迭代器方法
    const itr = target[Symbol.iterator]()
    //wrap函数可以把代理的值转化成响应式数据
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    //调用track函数建立响应联系
    track(target, ITERATE_KEY)
    //返回自定义迭代器对象
    return  {
        next(){
            //调用原始迭代器的next方法获取value和done
            const {value, done} = itr.next()
            return {
                //如果value不是undefined,则对其进行包装
                value: value ? [wrap(value[0]), wrap(value[1])] : value,
                done
            }
        }
    }
}

最后,由于目前使用entries()会报错,因为返回值不是一个可迭代对象

//p.entries is not a function or its return value is not iterable

所以我们要为其增加一个Symbol.iterator方法,让返回的对象同时实现可迭代协议和迭代器协议

  • 可迭代协议:一个对象实现了Symbol.iterator方法
  • 迭代器协议:一个对象实现了next方法
function iterationMethod(){
    const target = this.raw
    const itr = target[Symbol.iterator]()
    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[0]), wrap(value[1])] : value,
                done
            }
        },
        //实现可迭代协议
        [Symbol.iterator](){
            return this
        }
    }
}

values与keys方法

values方法返回的仅仅是数据的值,所以只需要iterationMethod方法修改一下即可

const mutableInstrumentations = {
    //自定义values方法
    values: valuesIterationMethod,
}
function valuesIterationMethod(){
    const target = this.raw
    //获取原始迭代器方法
    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即可
                value: wrap(value),
                done
            }
        },
        [Symbol.iterator](){
            return this
        }
    }

}

实现keys的方式则和上述基本一样,只需要改动一行代码,第8行

const itr = target.keys()

但是上述代码存在一个问题,如果尝试设置一个值,就会出乎意料的重新调用副作用函数

const p = reactive(new Map([
  ['key1', 'value1'],
  ['key2', 'value2']
]))
effect(() => {
  for(const value of p.keys()){
    console.log(value)
  }
})
//因为keys并不关心值,所以修改值不应该触发副作用函数重新执行
p.set('key2', 'value3')

原因:之前对Map类型的数据进行了特殊处理,操作类型为SET的时候,会触发与ITERATE_KEY相关联的副作用函数,而这种处理方式对于valuesentries是必须的,但是对于keys并没有必要

解决:只要重新定义一个MAP_KEY_ITERATE_KEY新的symbol

const MAP_KEY_ITERATE_KEY = Symbol()
function trigger(target, key, type, newVal){
    /** 省略部分代码 */
    //只有操作类型为ADD或DELETE并且为map的数据结构
    if ((type === TriggerType.ADD || type === TriggerType.DELETE ) && 
        //如果操作类型是SET,并且目标对象是Map类型的数据,那么也应该触发于ITERATOR_KEY关联的副作用函数
        Object.prototype.toString.call(target) === '[object Map]'
    ) {
        //取出那些与MAP_KEY_ITERATE_KEY相关联的副作用函数并执行
        const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
    }
    /** 省略部分代码 */
}
//keys自定义方法的实现逻辑
function keysIterationMethod(){
    const target = this.raw
    const itr = target.keys()
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    //调用track函数建立响应联系,要用MAP_KEY_ITERATE_KEY建立响应联系
    track(target, MAP_KEY_ITERATE_KEY)
    return  {
        next(){
            const {value, done} = itr.next()
            return {
                value: wrap(value),
                done
            }
        },
        [Symbol.iterator](){
            return this
        }
    }
}