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
}

结果:

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

解决思路

  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
}

结果:

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

解决思路:

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

结果:

新增、删除触发

修改不触发

相关推荐
旭久18 分钟前
vue-计算两个日期之前的天数小方法
前端·javascript·vue.js
游王子22 分钟前
Vue.js组件(6):echarts组件
前端·vue.js·echarts
Smile_Gently1 小时前
Ubuntu环境 nginx.conf详解(二)
运维·服务器·前端·nginx·ubuntu
m0_748233881 小时前
黑马程序员JavaWeb开发教程(前端部分) ---笔记分享
前端·笔记
温轻舟1 小时前
前端开发 -- 自定义鼠标指针样式
开发语言·前端·javascript·css·html·温轻舟
冰镇屎壳郎2 小时前
前端安全 常见的攻击类型及防御措施
前端·安全·前端安全
2401_857617622 小时前
“无缝购物体验”:跨平台网上购物商城的设计与实现
java·开发语言·前端·安全·架构·php
2401_857439692 小时前
智慧社区电商系统:提升用户体验的界面设计
前端·javascript·php·ux
我是高手高手高高手2 小时前
ThinkPHP8多应用配置及不同域名访问不同应用的配置
linux·服务器·前端·php
小李小李不讲道理2 小时前
行动+思考 | 2024年度总结
前端·程序员·年终总结