Vue设计与实现:非原始值的响应式方案

理解 Proxy 和 Reflect

未使用Reflect.get:

当原始数据的bar使用了this.foo,再在watch监听obj.foo,最后修改obj.foo

js 复制代码
const data = {
  foo: 1,
  get bar() {
    return this.foo
  }
};
// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    track(target, key);
    return target[key];
  },
  // 拦截写入操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    trigger(target, key);
  }
})

watch(() => console.log(obj.bar))

obj.foo++

结果:

只执行了watch的effect,是因为在obj的getter函数中的this是指向data,所以访问obj.bar跟访问data.bar是一样的,data并不是proxy数据所以不会跟effect建立联系,就需要通过Reflect.get来修正this

使用Reflect.get:

diff 复制代码
const data = {
  foo: 1,
  get bar() {
    return this.foo
  }
};
// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
-  get(target, key) {
+  get(target, key, receiver) {
    track(target, key);
-    return target[key];
+    return Reflect.get(target, key, receiver)
  },
  // 拦截写入操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    trigger(target, key);
  }
})

watch(() => console.log(obj.bar))

obj.foo++

结果:

watch先执行一次effect会触发obj.bar的getter通过Reflect.get修改this指向obj就触发了obj.foo,所以obj.foo也跟watch的effect建立了联系,修改obj.foo就会触发effect

代理 Object

一个普通对象的所有可能的读取操作:

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

get

js 复制代码
const data = { foo: 1 }

// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key, receiver) {
    track(target, key);
    return Reflect.get(target, key, receiver)
  },
})

in 操作符

js 复制代码
const data = { foo: 1 }

// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
  has(target, key) {
    track(target, key);
    return Reflect.has(target, key)
  },
})

watch(() => "foo" in obj)

结果:

for...in

例子:

由于ownKeys只有target并没有key,所以需要定义一个key

js 复制代码
const data = { foo: 1 }

const ITERATE_KEY = Symbol()

// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
  ownKeys(target) {
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target)
  },
})

watch(() => {
  for (const key in obj) {
    console.log(key)
  }
})

结果:

添加新属性:

向obj添加a属性并未重新遍历,这是因为向obj添加a属性会触发set,set向depsMap根据key也就是a拿到对应的effect,但是一开始obj并没有a属性所以depsMap并没有a

diff 复制代码
const data = { foo: 1 }

const ITERATE_KEY = Symbol()

// 对原始的数据进行代理
const obj = new Proxy(data, {
  // 拦截读取操作
  ownKeys(target) {
    track(target, ITERATE_KEY);
    return Reflect.ownKeys(target)
  },
})

watch(() => {
  for (const key in obj) {
    console.log(key)
  }
})

+ obj.a = 1
解决思路:

当添加属性时,将那些与 ITERATE_KEY 相关联的副作用函数也取出来执行,这样添加新属性时就会触发ITERATE_KEY 相关联的副作用函数

diff 复制代码
function trigger(target, key) {
  // 获得对应key的effect set
  const depsMap = bucket.get(target);
  
+  // 取得与 ITERATE_KEY 相关联的副作用函数
+  const iterateEffects = depsMap.get(ITERATE_KEY)
  
  if (!depsMap) return;
  console.log("depsMap", depsMap)
  const effects = depsMap.get(key);
  // 临时的 set
  const effectsToRun = new Set();
  // 将副作用函数 effect 取出并执行
  effects && effects.forEach(effect => {
    // 如果trigger触发的副作用函数和当前正在执行的函数相同,则跳过
    if (effect !== activeEffect) {
      effectsToRun.add(effect);
    }
  });

+  // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+  iterateEffects && iterateEffects.forEach(effectFn => {
+    if (effectFn !== activeEffect) {
+      effectsToRun.add(effectFn)
+    }
+  })

  effectsToRun.forEach(effect => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  })
}
修改已有属性问题:

新加属性或者修改现有属性上面的代码都会执行,对于forin来说修改现有数据只需要遍历一次,增加新属性才需要重新遍历
在修改属性时在setter判断是新增属性还是修改现有属性,把type传入trigger再判断如果是修改现有属性就不执行与 ITERATE_KEY 相关联的副作用函数,这样forin只有新增属性时才执行

diff 复制代码
const obj = new Proxy(data, {
  // 拦截写入操作
  set(target, key, newVal, receiver) {
+    // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
+    const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 把副作用函数从桶里取出并执行
+    trigger(target, key, type);
    return res
  },
})

- function trigger(target, key) {
+ function trigger(target, key, type) {
  // 获得对应key的effect set
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 取得与 ITERATE_KEY 相关联的副作用函数
  const iterateEffects = depsMap.get(ITERATE_KEY)
  const effects = depsMap.get(key);
  // 临时的 set
  const effectsToRun = new Set();
  // 将副作用函数 effect 取出并执行
  effects && effects.forEach(effect => {
    // 如果trigger触发的副作用函数和当前正在执行的函数相同,则跳过
    if (effect !== activeEffect) {
      effectsToRun.add(effect);
    }
  });

+  if (type === "ADD") {
+    // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+    iterateEffects && iterateEffects.forEach(effectFn => {
+      if (effectFn !== activeEffect) {
+        effectsToRun.add(effectFn)
+      }
+    })
+  }


  effectsToRun.forEach(effect => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  })
}

删除属性

首先检查被删除的属性是否属于对象自身,然 后调用 Reflect.deleteProperty 函数完成属性的删除工作,只有 当这两步的结果都满足条件时,才调用 trigger 函数触发副作用函数 重新执行

diff 复制代码
const obj = new Proxy(data, {
+  deleteProperty(target, key) {
+    // 检查被操作的属性是否是对象自己的属性
+    const hasKey = Object.prototype.hasOwnProperty.call(target, key)
+    // 使用 Reflect.deleteProperty 完成属性的删除
+    const res = Reflect.deleteProperty(target, key)
+    // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
+   if(hasKey && red) {
+     trigger(target, get, "DELETE")
+   }
+    return res
+  }
})

由于删除操作会使得对象的键变少,它会影响 for...in 循环的次数,因此当操作类型为 'DELETE' 时,也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行

diff 复制代码
function trigger(target, key, type) {
+  if (type === "ADD" || type === "DELETE") {
+    // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
+    iterateEffects && iterateEffects.forEach(effectFn => {
+      if (effectFn !== activeEffect) {
+        effectsToRun.add(effectFn)
+      }
+    })
  }

合理地触发响应

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

js 复制代码
const obj = { foo: 1 }
effect(() => {
  console.log(obj.foo)
})
obj.foo = 1

effect先自执行一次打印1,再obj.foo再修改一次打印1

解决思路:

  1. 修改属性会先触发setter所以在setter判断
  2. target[key]拿到之前的旧值,再跟newVal判断是否真正修改了,如果修改了才执行trigger
diff 复制代码
const obj = new Proxy(data, {
  // 拦截写入操作
  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 进行不全等比较总会得到true,这仍然会触发响应,并导致不必要的更新

js 复制代码
NaN === NaN // false 
NaN !== NaN // true

const obj = { foo: NaN }
effect(() => {
  console.log(obj.foo)
})
obj.NaN = 1
解决思路:

因为NaN跟NaN进行全等时为false,当oldVal === oldVal || newVal === newVal为true就能排除NaN

diff 复制代码
const obj = new Proxy(data, {
  // 拦截写入操作
  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
  },
})

原型上继承属性问题

obj上没有bar属性,parent设置为child的原型,在effect函数中访问child.bar会创建child的bar的depsMap,由于child上没有bar所以会向原型上找proto.bar会创建proto的bar的depsMap,所以child.bar = 2会触发child的bar的depsMap、proto的bar的depsMap打印两次2

js 复制代码
function reactive(data) {
  return new Proxy(data, {
    // 省略前文讲解的拦截函数
}

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)
})

child.bar = 2

解决思路:

  1. child、parent的setter中的target都是对应原始对象,但是receiver都指向child
  2. 在getter中添加可以返回target的row属性,在setter访问row会触发getter拿到target再跟setter中的target比较,如果相同说明receiver就是 target 的代理对象
diff 复制代码
function reactive(data) {
  return new Proxy(data, {
    // 拦截读取操作
    get(target, key, receiver) {
+      if (key === "row") return target
      track(target, key);
      return Reflect.get(target, key, receiver)
    },
    // 拦截写入操作
    set(target, key, newVal, receiver) {
      console.log(target, receiver.row, target === receiver.row)
      // 拿到旧值
      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.row) {
        // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type);
        }
      }
      return res
    },
  })
}

浅响应与深响应

js 复制代码
function reactive(obj) {
  return createReactive(obj)
}
function shallowReactive(obj) {
  return createReactive(obj, true)
}

深响应

读取obj.foo.bar时,首先要读取 obj.foo 的值,但得到是普通的对象非响应对象,所以在副作用函数中访问 obj.foo.bar时,是不能建立响应联系的

解决思路:

在getter得到的是原始数据对象就递归传入reactive转成响应数据

diff 复制代码
function reactive(data) {
  return new Proxy(data, {
    // 拦截读取操作
    get(target, key, receiver) {
      if (key === "row") return target
      track(target, key);
      // 得到原始值结果
+      const res = Reflect.get(target, key, receiver)
+      // 调用 reactive 将结果包装成响应式数据并返回
+      if (typeof res === "object" && res !== null) return reactive(res)
+      return res
    }
  })
}

结果:

浅响应

只有对象的第一层属性是响应的,第二层及更深层次的属性则不是响应的

解决思路:

  1. 单独封装个createReactive函数,并支持传入isShallow来控制是否是浅响应
  2. 如果是浅响应就直接返回原始值,否则就递归转换成深响应
js 复制代码
function createReactive(obj, isShallow = false) {
  return new Proxy(obj, {
    // 拦截读取操作
    get(target, key, receiver) {
      if (key === "row") return target
      track(target, key);
      // 得到原始值结果
      const res = Reflect.get(target, key, receiver)
      // 如果是浅响应,则直接返回原始值
      if (isShallow) return res
      // 调用 reactive 将结果包装成响应式数据并返回
      if (typeof res === "object" && res !== null) return reactive(res)
      return res
    }
  })
}

结果:

深只读和浅只读

js 复制代码
function readonly(obj) {
  return createReactive(obj, false, true)
}
function shallowReadonly(obj) {
  return createReactive(obj, true /* shallow */, true)
}

深只读

解决思路:

  1. 只读是属性改动时触发的,涉及到属性改动操作有set、deleteProperty
  2. createReactive添加isReadonly参数默认值为false,在set、deleteProperty判断isReadonly,如果为true就直接报错
diff 复制代码
-function createReactive(obj, isShallow = false) {
+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.row) {
        // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type);
        }
      }
      return res
    },
    deleteProperty(target, key) {
+     if (isReadonly) {
+       console.warn(`属性 ${key} 是只读的`)
+       return true
+     }
      // 检查被操作的属性是否是对象自己的属性
      const hasKey = Object.prototype.hasOwnProperty.call(target, key)
      // 使用 Reflect.deleteProperty 完成属性的删除
      const res = Reflect.deleteProperty(target, key)
      // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
      if (hasKey && red) {
        trigger(target, get, "DELETE")
      }
      return res
    }
  })
}

结果:

只读时不绑定effect

当一个数据为只读时,不应该跟effect关联起来

js 复制代码
const obj = readonly({ foo: 1 })

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

obj.foo = 2

解决思路:

  1. 因为在effect读取值会触发getter,所以在getter判断是否只读
  2. 只有非只读才能执行track建立响应联系
  3. 如果原始值是对象并且isReadonly为true,继续递归readonly让更深层次属性转换成只读
diff 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    // 拦截读取操作
    get(target, key, receiver) {
      if (key === "row") return target
      // 非只读的时候才需要建立响应联系
+      if (!isReadonly) track(target, key);
      // 得到原始值结果
      const res = Reflect.get(target, key, receiver)
      // 如果是浅响应,则直接返回原始值
      if (isShallow) return res
      // 调用 reactive 将结果包装成响应式数据并返回
      if (typeof res === "object" && res !== null) {
+        // 如果数据为只读,则调用 readonly 对值进行包装
+        return isReadonly ? readonly(res) : reactive(res)
      }
      return res
    }
  })
}

浅只读

只需要修改 createReactive 的第二个参数,因为isShallow为true就直接返回原始值

js 复制代码
function shallowReadonly(obj) {
  return createReactive(obj, true, true)
}

代理数组

所有对数组元素或属性的"读取"操作。

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

数组的索引与 length

通过索引设置元素值时,可能会隐式地修改length

数组的原长度为1,并且在副作用函数中访问了length属性。然后设置数组索引为1的元素值,这会导致数组的长度变为2,应该触发副作用函数重新执行。但目前的实现还做不到这一点

js 复制代码
const arr = reactive(["foo"])

effect(() => {
  console.log(arr.length)
})

arr[1] = "bar"
解决思路:
  1. 在track中length已经跟effect建立关联,但在setter中打印key能访问到arr[1]的key,但是trigger并没有根据length拿到对应的effect
  2. 判读target是数组再判断key跟target的length大小,key小于length说话就是修改数据,否则就是新增
diff 复制代码
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]

+      // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
+      // 如果是,则视作 SET 操作,否则是 ADD 操作
+      const type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver)
      if (target === receiver.row) {
        // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type);
        }
      }
      return res
    },
  })
}
  1. 只有在是新增并且target是数组时,才把length关联的effect拿出添加到effectsToRun,修改是不会改变length所以不需要判断
diff 复制代码
function trigger(target, key, type) {
+  if (type === "ADD" && Array.isArray(target)) {
+    // 取出与 length 相关联的副作用函数
+    const lengthEffects = depsMap.get("length")
+    lengthEffects && lengthEffects.forEach(effectFn => {
+      if (effectFn !== activeEffect) {
+        effectsToRun.add(effectFn)
+      }
+    })
+  }
}
结果:

修改数组的length属性

当修改 length 属性值时,只有那些索引值大于或等于新的 length 属性值的元素才需要触发响应

js 复制代码
const arr = reactive(["foo"])

effect(() => {
  console.log(arr[0])
})

arr.length = 0
解决思路:
  1. 设置length一般都是设置数字代表是数组的长度,意味着赋值给length的值是最大的key,只有target的索引值比length的值大才需要触发effect,newVal就是length的值需要传入trigger进行比较
diff 复制代码
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]

      // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
      // 如果是,则视作 SET 操作,否则是 ADD 操作
      const type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"
      // 设置属性值
      const res = Reflect.set(target, key, newVal, receiver)
      if (target === receiver.row) {
        // 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
-          trigger(target, key, type);
+          trigger(target, key, type, newVal);
        }
      }
      return res
    },
  })
}
  1. 只有在target是数组并且修改length时触发,因为arr[0]通过索引值来访问,所以depsMap存放在关于索引值对应的effect,遍历depsMap跟newVal也就是length的值比较,如果索引值比length大就添加到effectsToRun
diff 复制代码
function trigger(target, key, type, newVal) {
  // 获得对应key的effect set
  const depsMap = bucket.get(target);
  if (!depsMap) return;

+  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(effect => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  })
}
结果:

遍历数组

for...in

哪些操作会影响 for...in 循环对数组的遍历

  1. 添加新元素:arr[100] = 'bar'
  2. 修改数组长度:arr.length = 0
js 复制代码
effect(() => {
  for (const key in arr) {
    console.log(key)
  }
})

arr[1] = 'bar'
arr.length = 0

目前并不会触发

解决思路:
  1. for...in会触发ownKeys,之前object是添加了ITERATE_KEY的key,但是无论是为数组添加新元素,还是直接修改数组的长度,本质上都是因为修改了数组的 length 属性,所以只需要把length跟target关联起来
diff 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    ownKeys(target) {
-      track(target, ITERATE_KEY)
+      track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
  })
}
结果:

for...of

无论是使用 for...of 循环,还是调用 values 等方法,它们都会读取数组的 Symbol.iterator 属性。该属性是一个 symbol 值,为了避免发生意外的错误,以及性能上的考虑,我们不应该在副作用函数与 Symbol.iterator 这类 symbol 值之间建立响应联系

js 复制代码
const arr = reactive(["foo"])

effect(() => {
  for (const key of arr) {
    key
  }
})

arr[1] = 'bar'
arr.length = 0
解决思路:

在getter判断key不是symbol才执行track

数组的查找方法

根据索引查找数组中的对象

obj是个对象又作为arr数组的第一位,arr[0]会触发getter得到是一个对象,对象又会递归reactive函数产生新的代理对象,includes方法内部也会通过 arr 访问数组元素又会触发getter又递归reactive函数产生新的代理对象,导致两个代理对象不一样为false

js 复制代码
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) 
解决思路:
  1. 之前得知会重复调用reactive函数并传入obj,但obj没有变化所以需要定义map来存储原始对象跟代理对象的映射,如果存在就直接返回,不存在就创建新的代理对象并以原始对象为key存到map中
diff 复制代码
+// 定义一个 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
}
结果:

根据原始对象查找

obj是原始数据而arr是代理对象,arr.includes会遍历arr中每个元素,得到的每个元素也是代理对象,所以用原始数据查找会得到false

js 复制代码
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj))
解决思路:
  1. arr.includes会触发getter,在getter中判断操作的目标对象是否数组,并且 key 存在于arrayInstrumentations 上includes方法
diff 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    // 拦截读取操作
    get(target, key, receiver) {
      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
      // 调用 reactive 将结果包装成响应式数据并返回
      if (typeof res === "object" && res !== null) {
        // 如果数据为只读,则调用 readonly 对值进行包装
        return isReadonly ? readonly(res) : reactive(res)
      }
      return res
    },
  })
}
  1. 在自定义的includes函数中先调用数组原生的includes再改变this也就是arr代理对象来判断args也就是要判断的值是否在arr中,如果不在arr中再判断是否在原始对象通过this.raw来触发getter来得到target
js 复制代码
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
    }
  })

修改数组长度方法

push既会读取数组的 length 属性值会触发getter,也会设置数组的 length属性值也会触发setter,在第一个effect中跟length关联,第二个effect调用push会触发setter的length于是把与 length 属性相关联的effect全部取出并执行,但是第二个effect还没执行完就要执行第一个才导致栈溢出

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

解决思路:

  1. 导致栈溢出的原因是两个effect相互执行,定义一个变量shouldTrack来判断是否执行完默认为true再重写push方法
js 复制代码
// 一个标记变量,代表是否进行追踪。默认值为 true,即允许追踪
let shouldTrack = true
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
  // 取得原始 push 方法
  const originMethod = Array.prototype[method]
  arrayInstrumentations[method] = function (...args) {
    shouldTrack = false
    // push 方法的默认行为
    let res = originMethod.apply(this, args)
    // 在调用原始方法之后,恢复原来的行为,即允许追踪
    shouldTrack = true
    return res
  }
})
  1. 开始执行自定义push将shouldTrack设置为false,push会触发length读取触发track,在track中判断shouldTrack为false就退出不会让length跟effect建立关联,当一个push执行完再将shouldTrack重置为true让第二个push可以执行,第二个effect的push执行type === "ADD" && Array.isArray(target)会发现length关联的effect是为空
diff 复制代码
function track(target, key) {
  // 没有 activeEffect、禁止追踪时 则直接 return
+  if (!activeEffect || !shouldTrack) return;
  // 根据target从桶中取得 depsMap,也是一个 Map 类型:key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap,就新建一个 Map 并和 target 关联
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 根据 key 从 depsMap 中取得 deps, 也是一个 Set 类型
  // 里面存储着所有与当前 key 相关联的副作用函数: effects
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

结果:

代理 Set 和 Map

size

因为代理对象上没有size属性所以报错了

js 复制代码
const s = new Set([1, 2, 3])
const p1 = reactive(s)
console.log(p1.size) 

解决思路:

  1. 使用Reflect.get把this指向原始对象
js 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target,key, receiver) {
      if (key === "size") {
        return Reflect.get(target, key, target)
      }
    }
  })
}

delete

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

js 复制代码
const s = new Set([1, 2, 3])
const p1 = reactive(s)
console.log(p1.delete(1)) 

解决思路:

把this绑定到原始对象并返回函数可以进行调用所以用bind

diff 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target,key, receiver) {
      if (key === "size") {
        return Reflect.get(target, key, target)
      }
+      // 将方法与原始数据对象 target 绑定后返回
+      return target[key].bind(target) 
    }
  })
}

结果:

建立响应联系

add

在effect中访问了size,但是add会改变size未重新触发effect,是因为在访问size时没有让size跟effect关联

js 复制代码
const p1 = reactive(new Set([1, 2, 3]))
effect(() => {
  // 在副作用函数内访问 size 属性
  console.log(p1.size)
})
// 添加值为 1 的元素,应该触发响应
p1.add(1)
解决思路:
  1. 当访问size时调用track跟ITERATE_KEY关联起来,是因为不管修改、新增都会影响size的状态
diff 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target,key, receiver) {
+     if (key === 'raw') return target
      if (key === "size") {
+      // 调用 track 函数建立响应联系
+      track(target, ITERATE_KEY)
        return Reflect.get(target, key, target)
      }
      // 将方法与原始数据对象 target 绑定后返回
      return mutableInstrumentations[key]
    }
  })
}
  1. 定义mutableInstrumentations对象来存放自定义实现的方法,当访问add时执行的是自定义add,通过this.raw来拿到target,再执行target的add并执行完返回得到结果,再触发trigger并传入ADD参数让执行if (type === "ADD" || type === "DELETE")拿到ITERATE_KEY相关联的effect
js 复制代码
// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
  add(key) {
    // this 仍然指向的是代理对象,通过 raw 属性获取原始数据对象
    const target = this.raw
    // 先判断值是否已经存在
    const hadKey = target[key]
    // 通过原始数据对象执行 add 方法添加具体的值,
    // 注意,这里不再需要 .bind 了,因为是直接通过 target 调用并执行的
    const res = target.add(key)
    // 调用 trigger 函数触发响应,并指定操作类型为 ADD
    if (!hadKey) {
      trigger(target, key, 'ADD')
    }
    // 返回操作结果
    return res
  }
}
结果:

delete

delete方法只有在要删除的元素确实在集合中存在时,才需要触发响应

js 复制代码
// 定义一个对象,将自定义的 add 方法定义到该对象下
const mutableInstrumentations = {
  delete(key) {
    const target = this.raw
    const hadKey = target.has(key)
    const res = target.delete(key)
    // 当要删除的元素确实存在时,才触发响应
    if (hadKey) {
      trigger(target, key, 'DELETE')
    }
    return res
  }
}
结果:

避免污染原始数据

当调用 get 方法读取数据时,需要调用 track 函数追踪依赖建立响应联系;当调用 set 方法设置数据时,需要调用 trigger 方法触发响应

get

重写get方法,让get跟effect关联起来,并让值转换成响应数据

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

set

Map的set方法接受key跟新的value参数,判断key是否有对应的值区分新增还是修改,通过旧值跟新值比对触发对应effect

js 复制代码
const mutableInstrumentations = {
  set(key, value) {
    const target = this.raw
    const had = target.has(key)
    // 获取旧值
    const oldValue = target.get(key)
    // 设置新值
    target.set(key, value)
    // 如果不存在,则说明是 ADD 类型的操作,意味着新增
    if (!had) {
      trigger(target, key, 'ADD')
    } else if (oldValue !== value || (oldValue === oldValue &&
      ue === value)) {
      // 如果不存在,并且值变了,则是 SET 类型的操作,意味着修改
      trigger(target, key, 'SET')
    }
  }
}

处理 forEach

代理对象的this上没有forEach所以会报错需要重写forEach

js 复制代码
const m = reactive(new Map([
  [{ key: 1 }, { value: 1 }]
]))
effect(() => {
  m.forEach(function (value, key, m) {
    console.log(value) // { value: 1 }
    console.log(key) // { key: 1 }
  })
})
m.set(0,1)

解决思路:

在mutableInstrumentations添加自定义forEach,因为forEach跟key、value的数量有关,所以forEach要跟ITERATE_KEY关联起来,只要新增删除就要重新触发forEach的effect

diff 复制代码
const mutableInstrumentations = {
+  forEach(callback) {
+    // 取得原始数据对象
+    const target = this.raw
+    // 与 ITERATE_KEY 建立响应联系
+    track(target, ITERATE_KEY)
+    // 通过原始数据对象调用 forEach 方法,并把 callback 传递过去
+    target.forEach(callback)
+  }
}

结果:

forEach回调参数转换响应数据

forEach回调的value参数不是响应数据所以修改并不会触发effect

js 复制代码
const key = { key: 1 }
const value = new Set([1, 2, 3])
const p1 = reactive(new Map([
  [key, value]
]))
effect(() => {
  p1.forEach(function (value, key) {
    console.log(value.size) // 3
  })
})
p1.get(key).delete(1)
解决思路:

使用target.forEach处理每个key、value,因为是map需要将key传入wrap转换成代理对象再传给callback

diff 复制代码
const mutableInstrumentations = {
  forEach(callback, thisArg) {
+    const wrap = (val) => typeof val === "object" ? reactive(val) : val
    // 取得原始数据对象
    const target = this.raw
    // 与 ITERATE_KEY 建立响应联系
    track(target, ITERATE_KEY)
    // 通过原始数据对象调用 forEach 方法,并把 callback 传递过去
+    target.forEach((val, key) => {
+      callback.call(thisArg, wrap(val), wrap(key), this)
+    })
+  }
}
结果:

Map的set触发forEach

Map的set是修改value的,而且Map的forEach中的回调有返回value,所以在使用set需要查询触发forEach

js 复制代码
const p1 = reactive(new Map([
  ['key', 1]
]))
effect(() => {
  p1.forEach(function (value, key) {
    // forEach 循环不仅关心集合的键,还关心集合的值
    console.log(value) // 1
  })
})
p1.set('key', 2) // 即使操作类型是 SET,也应该触发响应
解决思路:

增加判断type为set并且target是Map就触发ITERATE_KEY关联的effect

diff 复制代码
function trigger(target, key, type, newVal) {
  if (
    type === "ADD" ||
    type === "DELETE" ||
+    // 如果操作类型是 SET,并且目标对象是 Map 类型的数据,
+    // 也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行
+    (type === "SET" && Object.prototype.toString.call(target) === '[object Map]')
  ) {
    // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
  }

  effectsToRun.forEach(effect => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  })
}
结果:

迭代器方法

for...of

forof一个代理对象会触发[Symbol.iterator]方法,但代理对象没有实现该方法

js 复制代码
const p1 = reactive(new Map([
  ['key1', 'value1'],
  ['key2', 'value2']
]))
effect(() => {
  for (const [key, value] of p1) {
    console.log(key, value)
  }
})
p1.set('key3', 'value3')

解决方法:

  1. 在mutableInstrumentations添加[Symbol.iterator]方法
  2. 因为forof会涉及新增删除value所以需要跟ITERATE_KEY关联
  3. 因为forof中拿到的value是响应数据,所以通过itr.next()拿到value、done,再把value传入wrap转换成代理对象
diff 复制代码
const mutableInstrumentations = {
+  [Symbol.iterator]() {
+    // 获取原始数据对象 target
+    const target = this.raw
+    // 获取原始迭代器方法
+    const itr = target[Symbol.iterator]()
+    const wrap = (val) => typeof val === "object" && val !== null ? reactive(val) : val
+    // 调用 track 函数建立响应联系
+    track(target, ITERATE_KEY)
+    // 将其返回
+    return {
+      next() {
+       // 调用原始迭代器的 next 方法获取 value 和 done
+        const { value, done } = itr.next()
+        return {
+          // 如果 value 不是 undefined,则对其进行包裹,value[0]是key,value[1]是value
+          value: value ? [wrap(value[0]), wrap(value[1])] : value,
+          done
+        }
+      }
+    }
}

结果:

entries

指entries的返回值不是一个可迭代对象

js 复制代码
const p1 = reactive(new Map([
  ['key1', 'value1'],
  ['key2', 'value2']
]))
effect(() => {
`  for (const [key, value] of p1.entries()) {
    console.log(key, value)
  }
})
console.log("修改")
p1.set('key3', 'value3')

解决方法:

Symbol.iterator\]与entries相等所以抽离一个共用函数,再添加可迭代协议 ```diff // 抽离为独立的函数,便于复用 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 + } } } const mutableInstrumentations = { + [Symbol.iterator]: iterationMethod, + entries: iterationMethod } ``` #### 结果: ![image.png](https://file.jishuzhan.net/article/1688483691644325890/18e84dfc4dca422cacf8d6dd4f78dd36.png) ### values 代理对象上没有values ```js const p1 = reactive(new Map([ ['key1', 'value1'], ['key2', 'value2'] ])) effect(() => { for (const value of p1.values()) { console.log(value) } }) console.log("修改") p1.set('key3', 'value3') ``` ![image.png](https://file.jishuzhan.net/article/1688483691644325890/0b22b0214c10457dbe1977c7254eda58.png) #### 解决思路 1. 新增valuesIterationMethod,通过`target.values()`拿到原始迭代器方法 2. 因为在forof中value会影响length,所以要跟ITERATE_KEY关联起来 3. 把value传入wrap转换成想要对象 4. 再实现可迭代协议 ```js function valuesIterationMethod() { // 获取原始数据对象 target const target = this.raw // 通过 target.values 获取原始迭代器方法 const itr = target.values() const wrap = (val) => typeof val === 'object' ? reactive(val) : val track(target, ITERATE_KEY) // 将其返回 return { next() { const { value, done } = itr.next() return { // value 是值,而非键值对,所以只需要包裹 value 即可 value: wrap(value), done } }, [Symbol.iterator]() { return this } } } ``` ```diff const mutableInstrumentations = { + values: valuesIterationMethod } ``` #### 结果: ![image.png](https://file.jishuzhan.net/article/1688483691644325890/780df1ae4adb4de99594e4e19d53471d.png) ### keys 代理对象上没有keys方法 ```js const p1 = reactive(new Map([ ['key1', 'value1'], ['key2', 'value2'] ])) effect(() => { for (const key of p1.keys()) { console.log(key) } }) console.log("修改") p1.set('key3', 'value3') ``` ![image.png](https://file.jishuzhan.net/article/1688483691644325890/5aa12ac668d7445f92e536d89edf5434.png) #### 解决思路: 1. 新增keysIterationMethod,通过`target.values()`拿到原始迭代器方法 2. 把value传入wrap转换成想要对象 3. 再实现可迭代协议 ```js const MAP_KEY_ITERATE_KEY = Symbol() function keysIterationMethod() { // 获取原始数据对象 target 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 } } } ``` ```diff const mutableInstrumentations = { + keys: keysIterationMethod } ``` 1. 因为keys只有新增或者是删除才会影响到key的数量set不会影响,所以需要重新定义MAP_KEY_ITERATE_KEY,只有新增、删除、并且是 Map 类型的数据才触发所以需要重新定义MAP_KEY_ITERATE_KEY,set不触发只触发ITERATE_KEY ```diff function trigger(target, key, type, newVal) { + if ( + // 操作类型为 ADD 或 DELETE + (type === 'ADD' || type === 'DELETE') && + // 并且是 Map 类型的数据 + 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) + } + }) + } } ``` #### 结果: 新增、删除触发 ![image.png](https://file.jishuzhan.net/article/1688483691644325890/abed1bde888d4e4caf520dfbb66f3af2.png) ![image.png](https://file.jishuzhan.net/article/1688483691644325890/c77c601bf1204c78889b3ddd5c0d52e2.png) 修改不触发 ![image.png](https://file.jishuzhan.net/article/1688483691644325890/02113d5f5e1d433c857b185a21f97bf7.png)

相关推荐
Boilermaker199235 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson2 小时前
青苔漫染待客迟
前端·设计模式·架构