# 非原始值的响应式方案
实现响应式数据并非单纯的拦截
get和set操作即可。比如拦截for...in循环、Map、Set、WeakMap、WeakSet等。
# 理解 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
}
// 省略其他拦截函数
})
}
# 浅响应与深响应
reactive与shallowReactive的区别,即深响应和浅响应的区别。
# 深响应
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 两个拦截函数,用来处理只读的情况。当 isReadonly 为 true 时,会打印警告信息并返回 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 方法,使其能够正确处理数组中的对象。 indexOf 和 lastIndexOf 也是一样的。
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)
})
上述代码会导致栈溢出的错误,原因如下:
第一个副作用函数执行。在函数内,调用了
arr.push(1)方法,也会间接的读取该数组的length属性,也会建立相应的依赖关系。第二个副作用函数执行。他也会与
length属性建立依赖关系。但是调用arr.push方法不仅会间接读数组的length属性,还会间接的修改数组的长度。当第二个函数内更改了数组的长度时,第一个函数内的依赖关系也会被触发,第二个函数还未执行完毕,就又要执行第一个函数,导致栈溢出。
解决方案:在修改数组长度时,不再触发 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
// 省略部分代码
}
# 代理 Set 和 Map
← 响应系统的作用与实现 原始值的响应式方案 →