# 非原始值的响应式方案

实现响应式数据并非单纯的拦截 getset 操作即可。比如拦截 for...in 循环、 MapSetWeakMapWeakSet 等。

# 理解 Proxy 和 Reflect

# Proxy (opens new window)

Vue.js 3 的响应式数据是基于 Proxy 实现的。使用 Proxy 可以创建一个代理对象,这个对象只能是 Object , 不能代理非对象,例如字符串、布尔等。 Proxy 可以允许我们拦截并重新定义一个对象的基本操作。

读取、设置属性值

const obj = {
    foo: 1
}
const p = new Proxy(obj, {
    get() {
        return obj.foo
    },
    set(target, key, value) {
        obj[key] = value
    }
})
console.log(p.foo)
p.foo++
console.log(p.foo)

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

const fn = (name) => {
    console.log('我是:', name)
}
const p2 = new Proxy(fn, {
    apply(target, thisArg, argArray) {
        target.call(thisArg, ...argArray)
    }
})
p2('hcy')

# Reflect (opens new window)

任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数。

const obj = {
    foo: 1
}
// 直接读取
console.log(obj.foo) // 1
// 使用 Reflect.get 读取
console.log(Reflect.get(obj, 'foo')) // 1

# 使用 Reflect 意义。

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

const obj = {
    foo: 1
}
console.log(Reflect.get(obj, 'foo', {
    foo: 2
})) // 输出的是 2 而不是 1

Reflect.* 方法在响应式中的应用

const obj = {
    foo: 1,
    get bar() {
        return this.foo
    }
}
// 对原始数据的代理
const p = new Proxy(obj, {
    // 拦截读取操作
    get(target, key, receiver) {
        // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
        track(target, key)
        // 返回属性值
        return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        trigger(target, key)
    }
})
effect(() => {
    console.log(p.bar)
})
  • 上述代码中通过 target 完成对属性的读取和设置,访问 bar 属性时,返回了 this.foo 属性的值。如果修改了 foo 属性的值,需求应该是会触发副作用函数的执行,但是实际并非如此。
  • get 拦截器中,通过 target[key] 返回属性值。其中 target 是原始对象 obj,而 key 就是字符串 'bar',所以 target[key] 相当于 obj.bar 。在副作用函数中访问原始对象的某个属性是不会建立响应的。
const p = new Proxy(obj, {
    // 拦截读取操作,接收第三个参数 receiver
    get(target, key, receiver) {
        track(target, key)
        // 使用 Reflect.get 返回读取到的属性值
        return Reflect.get(target, key, receiver)
    },
    // 省略部分代码
})
  • 通过上述代码可以解决访问原始对象的属性的问题。因为在访问obj.bar对象时通过改变this的指向变为了访问receiver对象,所以通过Reflect.get方法可以正确返回属性值。

# 如何代理 Object

通过 get 拦截函数去拦截对属性的读取操作。 读取 是一个很宽泛的概念。

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在某个属性:key in obj
  • 使用·for...in·循环遍历对象上的属性:for (let key in obj)

#obj.foo 的读取操作,通过 get 拦截器拦截

const obj = {
    foo: 1
}
const p = new Proxy(obj, {
    get(target, key, receiver) {
        // 建立联系
        track(target, key)
        // 返回属性值
        return Reflect.get(target, key, receiver)
    },
})

# 对于 in 操作符

ECMA-262 规范中, in 操作符是通过调用一个 HasProperty 内部方法来实现的。它对应的拦截函数是 has

const obj = {
    foo: 1
}
const p = new Proxy(obj, {
    has(target, key) {
        track(target, key)
        return Reflect.has(target, key)
    }
})
effect(() => {
    'foo' in p // 将会建立依赖关系
})

# 看如何拦截 for...in 循环。

for...in 循环是通过调用一个 Enumerate 内部方法来实现的。它对应的拦截函数是 ownKeys

const obj = {
    foo: 1,
}
const ITERATE_KEY = Symbol()
const p = new Proxy(obj, {
    ownKeys(target) {
        track(target, ITERATE_KEY)
        return Reflect.ownKeys(target)
    }
})
effect(() => {
    for (let key in p) {
        console.log(key)
    }
})

for...in 循环的 key 值作为 ITERATE_KEY 传入 track 函数,建立依赖关系。 这样做的原因是因为 在 ownKeys 拦截器中,我们只能获取到对象的 target 。所以构造唯一的 ITERATE_KEY 作为依赖项,来建立依赖关系。

# 添加对象上的属性

如果添加了一个新的属性,应该会对 for...in 循环产生影响, 需要触发副作用函数的执行。但是 在 set 拦截器中,我们并没有执行 ITERATE_KEY 的依赖关系。

当为对象添加属性时,会触发 set 拦截函数的执行,最终调用也是触发了当前属性的副作用函数重新执行。

const p = new Proxy(obj, {
    // 拦截设置操作
    set(target, key, newVal, receiver) {
        // 设置属性值
        const res = Reflect.set(target, key, newVal, receiver)
        // 把副作用函数从桶里取出并执行
        trigger(target, key)
        return res
    },
    // 省略其他拦截函数
})

解决方案:通过对 set 操作的拦截 将 ITERATE_KEY 相关的所有副作用函数重新执行。

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 取得与 key 相关联的副作用函数
    const effects = depsMap.get(key)
    // 取得与 ITERATE_KEY 相关联的副作用函数
    const iterateEffects = depsMap.get(ITERATE_KEY)
    const effectsToRun = new Set()
    // 将与 key 相关联的副作用函数添加到 effectsToRun
    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()
        }
    })
}

# 修改对象上的属性

与添加新属性不一样,修改属性不会对 for...in 循环产生影响。所以修改属性不应该执行 ITERATE_KEY 相关的副作用函数。

解决方案:通过对 set 操作的拦截,判断当前属性是否存在,如果存在,则当前操作类型为 SET ,否则为 ADD 添加属性。

const p = new Proxy(obj, {
    // 拦截设置操作
    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)
        return res
    },
    // 省略其他拦截函数
})

function trigger(target, key, type) {
    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)
        }
    })
    console.log(type, key)
    // 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行
    if (type === 'ADD') {
        const iterateEffects = depsMap.get(ITERATE_KEY)
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
    }
    effectsToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}
// 通常我们会将操作类型封装为一个枚举值, 例如:
const TriggerType = {
    SET: 'SET',
    ADD: 'ADD'
}

# 删除 obj.foo 属性

删除属性时,应该触发 delete 相关的副作用函数的执行。

const p = new Proxy(obj, {
    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
    }
})

当属性被删除时,会触发 delete 相关的副作用函数的执行。但是 delete 操作并不会触发 ITERATE_KEY 相关的副作用函数的执行。所以需要手动触发 ITERATE_KEY 相关的副作用函数的执行。

function trigger(target, key, type) {
    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)
        }
    })
    // 当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
    if (type === 'ADD' || type === 'DELETE') {
        const iterateEffects = depsMap.get(ITERATE_KEY)
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
    }
    effectsToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

# 合理地触发响应

代理对象需要知道操作的类型是 ADD 还是 SET , 需要正确的触发响应,还需要一些逻辑。

# 当值没有发生变化时,应该不需要触发响应。

const obj = {
    foo: 1
}
const p = new Proxy(obj, {
    /* ... */
})
effect(() => {
    console.log(p.foo)
})
// 设置 p.foo 的值,但值没有变化
p.foo = 1

当把p.foo设置新的值,如果值没有发生变化,不需要触发响应。

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) {
            trigger(target, key, type)
        }
        return res
    },
})

单独处理NaN值

NaN 值与NaN 值比较时,会返回 false。

// NaN === NaN // false
// NaN !== NaN // true
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)
        // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
        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)
// 使用 parent 作为 child 的原型
Object.setPrototypeOf(child, parent)
effect(() => {
    console.log(child.bar) // 1
})
// 修改 child.bar 的值
child.bar = 2 // 会导致副作用函数重新执行两次

执行两次的原型是:当设置 bar 属性,由于 child 对象没有 bar 属性,会从原型链上查找 bar 属性,找到 parent 对象,然后设置 bar 属性。由于 parent 对象也是响应式对象,所以会触发 bar 属性的副作用函数的执行。由于 child 对象也有 bar 属性,所以会触发 bar 属性的副作用函数的执行。所以会执行两次副作用函数的执行。

//解决思路: 屏蔽一次副作用函数的执行

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

// parent 的 set 拦截函数
set(target, key, value, receiver) {
    // target 是原始对象 proto
    // receiver 仍然是代理对象 child
}

上述逻辑发现,当 parent 代理对象的 set 拦截执行时, target 是原始对象 proto ,而 receiver 仍然是代理对象 child

 function reactive(obj) {
     return new Proxy(obj {
         get(target, key, receiver) {
             // 代理对象可以通过 raw 属性访问原始数据
             if (key === 'raw') {
                 return target
             }

             track(target, key)
             return Reflect.get(target, key, receiver)
         }
         // 省略其他拦截函数
     })
 }

 function reactive(obj) {
     return 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)
             // target === receiver.raw 说明 receiver 就是 target 的代理对   象
             if (target === receiver.raw) {
                 if (oldVal !== newVal && (oldVal === oldVal || newVal ===
                         newVal)) {
                     trigger(target, key, type)
                 }
             }
             return res
         }
         // 省略其他拦截函数
     })
 }

# 浅响应与深响应

reactiveshallowReactive 的区别,即深响应和浅响应的区别。

# 深响应

const obj = reactive({
    foo: {
        bar: 1
    }
})
effect(() => {
    console.log(obj.foo.bar)
})
// 修改 obj.foo.bar 的值,并不能触发响应
obj.foo.bar = 2

属性 foo 也是一个对象,在副作用函数中访问 obj.foo.bar 时,不会触发副作用函数重新执行。

function reactive(obj) {
    return new Proxy(obj {
        get(target, key, receiver) {
            if (key === 'raw') {
                return target
            }
            track(target, key)
            // 当读取属性值时,直接返回结果
            return Reflect.get(target, key, receiver)
        }
        // 省略其他拦截函数
    })
}

由上述逻辑分析,当读取 obj.foo.bar 时,先读取 obj.foo 的值。直接使用了 Reflect.get 方法返回 obj.foo 的值是一个普通的对象,并不是一个响应式对象。所以不能建立响应联系。

function reactive(obj) {
    return new Proxy(obj {
        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)
            }
            // 返回 res
            return res
        }
        // 省略其他拦截函数
    })
}

当读取属性值时,检测是否是对象,如果是对象则递归的调用 reactive 方法包装成响应式数据并返回。这样每个对象都会被包装成响应式数据。

# 浅响应

浅响应只有第一层属性时响应式的,第二层以及更深的层次不会响应式。

const obj = shallowReactive({
    foo: {
        bar: 1
    }
})
effect(() => {
    console.log(obj.foo.bar)
})
// obj.foo 是响应的,可以触发副作用函数重新执行
obj.foo = {
    bar: 2
}
// obj.foo.bar 不是响应的,不能触发副作用函数重新执行
obj.foo.bar = 3
// 封装 createReactive 函数,接收一个参数 isShallow,代表是否为浅响应,默认为 false,即非浅响应
function createReactive(obj, isShallow = false) {
    return new Proxy(obj, {
        // 拦截读取操作
        get(target, key, receiver) {
            if (key === 'raw') {
                return target
            }
            const res = Reflect.get(target, key, receiver)
            track(target, key)
            // 如果是浅响应,则直接返回原始值
            if (isShallow) {
                return res
            }
            if (typeof res === 'object' && res !== null) {
                return reactive(res)
            }
            return res
        }
        // 省略其他拦截函数
    })
}

function reactive(obj) {
    return createReactive(obj)
}

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

# 只读和浅只读

# 浅只读

组件的 props 对象是一个只读对象, readonly 函数可以将对象包装成只读对象。

const obj = readonly({
    foo: 1
})
// 尝试修改数据,会得到警告
obj.foo = 2
// 增加第三个参数 isReadonly,代表是否只读,默认为 false,即非只读
function createReactive(obj, isShallow = false, isReadonly =
    false) {
    return new Proxy(obj, {
        // 拦截读取操作
        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
        }
        //
        // 拦截设置操作
        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
        }
        // 省略其他拦截函数
    })
}

上述代码新增了 set 和· deleteProperty 两个拦截函数,用来处理只读的情况。当 isReadonlytrue 时,会打印警告信息并返回 true

提示

如果一个数据是只读的,那就意味着任何方式都无法修改它。因此,没有必要为只读数据建立响应联系。当在副作用函数中读取一个只读数据时,不需要调用 track 函数追踪响应。

const obj = readonly({
    foo: 1
})
effect(() => {
    obj.foo // 可以读取值,但是不需要在副作用函数与数据之间建立响应联系
})

function createReactive(obj, isShallow = false, isReadonly =
    false) {
    return new Proxy(obj, {
        // 拦截读取操作
        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
        }

    })
}

# 深只读

浅只读还可以修改属性为对象的值,深只读需要将在 get 拦截器中递归的调用 readonly 函数包装成只读对象。

 function createReactive(obj, isShallow = false, isReadonly =
     false) {
     return new Proxy(obj, {
         // 拦截读取操作
         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) {
                 // 如果数据为只读,则调用 readonly 对值进行包装
                 return isReadonly ? readonly(res) : reactive(res)
             }
             return res
         }
         // 省略其他拦截函数
     })
 }

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

 function shallowReadonly(obj) {
     return createReactive(obj, true /* shallow */ , true)
 }

# 代理数组

当我们通过索引读取或设置数组元素的值时,代理对象的 get/set 拦截函数也会执行。

对数组元素或属性的“读取”操作。

  • 通过索引访问数组元素值:arr[0]
  • 访问数组的长度:arr.length
  • 把数组作为对象,使用 for...in 循环遍历。
  • 使用 for...of 迭代遍历数组。
  • 数组的原型方法,如concat/join/every/some/find/findIndex/includes等,以及其他所有不改变原数组的原型方法。

对数组元素或属性的“设置”操作。

  • 通过索引修改数组元素值:arr[1] = 3
  • 修改数组长度:arr.length = 0
  • 数组的栈方法:push/pop/shift/unshift
  • 修改原数组的原型方法:splice/fill/sort 等。

# 数组的索引与 length 属性

# 数组索引修改的响应式处理

const arr = reactive(['foo']) // 数组的原长度为 1
effect(() => {
    console.log(arr.length) // 1
})
// 设置索引 1 的值,会导致数组的长度变为 2
arr[1] = 'bar'

当数组的长度发生变化时,会触发 length 属性的副作用函数的执行。

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 = 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
            }
            // 省略其他拦截函数
        }
    }

    function trigger(target, key, type) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        // 省略部分内容
        // 当操作类型为 ADD 并且目标对象是数组时,应该取出并执行那些与 length属性相关联的副作用函数
        if (type === 'ADD' && Array.isArray(target)) {
            // 取出与 length 相关联的副作用函数
            const lengthEffects = depsMap.get('length')
            // 将这些副作用函数添加到 effectsToRun 中,待执行
            lengthEffects && lengthEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) {
                    effectsToRun.add(effectFn)
                }
            })
        }
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }

上述逻辑在 set 拦截器中,增加了对数组类型的判断。如果代理目标是数组,那么对于操作类型的判断会有所区别。如果被设置的索引值如果小于数组长度,视为 SET 操作,因为不会改变数组的长度。如果设置的索引值大于等于数组长度,视为 ADD 操作,因为会改变数组的长度。需要把和数组 length 属性相关联的副作用函数添重新执行。

# 数组长度修改的响应式处理

const arr = reactive(['foo'])
effect(() => {
    // 访问数组的第 0 个元素
    console.log(arr[0]) // foo
})
// 将数组的长度修改为 0,导致第 0 个元素被删除,因此应该触发响应
arr.length = 0

我们在修改数组的长度时,会触发 length 属性的副作用函数的执行。如果将数组的长度修改为 0,所有的元素都会被删除,因此副作用函数应该重新执行。但是将 length 属性设置为 100 , 不会影响数组中第 0 个元素的值,只有大于或等于新的 length 的值才需要触发响应。

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 = Array.isArray(target) ?
                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 函数增加第四个参数,newVal,即新值
function trigger(target, key, type, newVal) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    // 省略其他代码
    // 如果操作目标是数组,并且修改了数组的 length 属性
    if (Array.isArray(target) && key === 'length') {
        // 对于索引大于或等于新的 length 值的元素,
        // 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
        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 循环遍历数组

数组也是对象,也可以使用 for...in 循环遍历。(但是不建议这么做, 因为也会把数组对象上的属性遍历出来)

 const arr = reactive(['foo'])

 effect(() => {
     for (const key in arr) {
         console.log(key) // 0
     }
 })

我们使用 for...in 循环遍历数组与遍历常规对象没有差异,可以用 ownKeys 方法拦截。

function createReactive(obj, isShallow = false, isReadonly =
    false) {
    return new Proxy(obj, {
        // 省略其他拦截函数
        ownKeys(target) {
            track(target, ITERATE_KEY)
            return Reflect.ownKeys(target)
        }
    })
}

为了追踪普通对象的 for...in 操作,创造了一个 ITERATE_KEY 作为追踪的key。但是对应数组来说,影响 for...in 操作的只有数组的 length 属性。

function createReactive(obj, isShallow = false, isReadonly = false) {
    return new Proxy(obj, {
        // 省略其他拦截函数
        ownKeys(target) {
            // 如果操作目标 target 是数组,则使用 length 属性作为 key 并建立响应联系
            track(target, Array.isArray(target) ? 'length' :
                ITERATE_KEY)
            return Reflect.ownKeys(target)
        }
    })
}

const arr = reactive(['foo'])
effect(() => {
    for (const key in arr) {
        console.log(key)
    }
})
arr[1] = 'bar' // 能够触发副作用函数重新执行
arr.length = 0 // 能够触发副作用函数重新执行

# for...of 迭代遍历数组

for...of 是用来遍历可迭代对象(iterable object)的。

提示

ES2015 为 JavaScript 定义了迭代协议(iteration protocol),它不是新的语法,而是一种协议。具体来说,一个对象能 否被迭代,取决于该对象或者该对象的原型是否实现了 @@iterator 方法。

数组内建了 Symbol.iterator 方法的实现

const arr = [1, 2, 3, 4, 5]
// 获取并调用数组内建的迭代器方法
const itr = arr[Symbol.iterator]()
console.log(itr.next()) // {value: 1, done: false}
console.log(itr.next()) // {value: 2, done: false}
console.log(itr.next()) // {value: 3, done: false}
console.log(itr.next()) // {value: 4, done: false}
console.log(itr.next()) // {value: 5, done: false}
console.log(itr.next()) // {value: undefined, done: true}

const arr = [1, 2, 3, 4, 5]
for (const val of arr) {
    console.log(val) // 1, 2, 3, 4, 5
}

数组迭代器的执行会读取数组的 length 属性。如果迭代的是数组元素值,还会读取数组的索引

const arr = reactive([1, 2, 3, 4, 5])
effect(() => {
    for (const val of arr) {
        console.log(val)
    }
})
arr[1] = 'bar' // 能够触发响应
arr.length = 0 // 能够触发响应

console.log(Array.prototype.values ===
    Array.prototype[Symbol.iterator]) // true

const arr = reactive([1, 2, 3, 4, 5])
effect(() => {
    for (const val of arr.values()) {
        console.log(val)
    }
})
arr[1] = 'bar' // 能够触发响应
arr.length = 0 // 能够触发响应

无论是使用 for...of 循环 ,还是调用 values 等方法,它们都会读取数组的 Symbol.iterator 属性。 所以需要修改 get 拦截器

function createReactive(obj, isShallow = false, isReadonly =
    false) {
    return new Proxy(obj, {
        // 拦截读取操作
        get(target, key, receiver) {
            console.log('get: ', key)
            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'、 'indexOf'、'lastIndexOf' 这三个方法都可以用来查找数组中的元素。

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

上述代码中,调用 reactive 函数包装了数组,数组中第一个元素是一个对象 obj ,但是调用 includes 方法查找 obj 时,返回的结果是 false 。原因是因为 this 的问题, arr.includes 在执行中函数执行时指向的事代理对象,并非 arr

if (typeof res === 'object' && res !== null) {
    // 如果值可以被代理,则返回代理对象
    return isReadonly ? readonly(res) : reactive(res)
}

并且在访问arr[0]得到的也是一个代理对象,每次再访问的时候返回的代理对象都是不同的。因为每次访问时都会调用 reactive 函数时创建一个新的代理对象

function reactive(obj) {
    // 每次调用 reactive 时,都会创建新的代理对象
    return createReactive(obj)
}

解决方案:

// 定义一个 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(arr[0])) // true
 const obj = {}
 const arr = reactive([obj])

 console.log(arr.includes(obj)) // false

上述代码中依旧输出 false , 因为 includes 内部的 this 指向的事代理对象 arr , 并且在获取数组元素时得到的也是代理对象,所以原始对象不存在

解决方案:重写 includes 方法,使其能够正确处理数组中的对象。 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) {
            // res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中查找, 并更新 res 值
            res = originMethod.apply(this.raw, args)
        }
        // 返回最终结果
        return res
    }
})

function createReactive(obj, isShallow = false, isReadonly =
    false) {
    return new Proxy(obj, {
        // 拦截读取操作
        get(target, key, receiver) {
            console.log('get: ', key)
            if (key === 'raw') {
                return target
            }
            // 如果操作的目标对象是数组,并且 key 存在于arrayInstrumentations 上,
            // 那么返回定义在 arrayInstrumentations 上的值
            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
        },
    })
}

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

push/pop/shift/unshift/splice 隐式地修改的数组的长度。

const arr = reactive([])
// 第一个副作用函数
effect(() => {
    arr.push(1)
})
// 第二个副作用函数
effect(() => {
    arr.push(1)
})

上述代码会导致栈溢出的错误,原因如下:

  1. 第一个副作用函数执行。在函数内,调用了 arr.push(1) 方法,也会间接的读取该数组的 length 属性,也会建立相应的依赖关系。

  2. 第二个副作用函数执行。他也会与length属性建立依赖关系。但是调用arr.push方法不仅会间接读数组的length属性,还会间接的修改数组的长度。

  3. 当第二个函数内更改了数组的长度时,第一个函数内的依赖关系也会被触发,第二个函数还未执行完毕,就又要执行第一个函数,导致栈溢出。

解决方案:在修改数组长度时,不再触发 length 属性的依赖关系。所以也需要修改数组的 push/pop/shift/unshift/splice 方法,使其不会触发 length 属性的依赖关系。

// 一个标记变量,代表是否进行追踪。默认值为 true,即允许追踪
let shouldTrack = true;
// 重写数组的 push、pop、shift、unshift 以及 splice 方法
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
    const originMethod = Array.prototype[method]
    arrayInstrumentations[method] = function(...args) {
        shouldTrack = false
        let res = originMethod.apply(this, args)
        shouldTrack = true
        return res
    }
})

function track(target, key) {
    // 当禁止追踪时,直接返回
    if (!activeEffect || !shouldTrack) return
    // 省略部分代码
}

# 代理 SetMap