文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
本文紧接《非原始值的响应式方案》和《代理数组》

set和map的代理不同于普通的对象,因为它们自身存在着的方法,独特的结构。

其中set结构的方法包括:size、add、clear、delete、has、keys、values、entries、forEach。

map的方法包括:size、clear、delete、has、get、set、keys、values、entries、forEach。

二者大部分的方法是一致,因此代理的方法也大致相同。

Set的size属性

// 尝试以下代码
// 新建一个Set集合
const s = new Set([1,2,3])
// 代理这个Set
const p = new Proxy(s, {})
// 输出代理的size
console.log(p.size)

会发现出现以下报错:大致意思为:在不兼容的reciver上调用了get Set.prototype.size方法。
在这里插入图片描述

通过查看规范,可知Set.prototype.size是一个访问器属性。首先会访问当前this的size属性,接着调用抽象方法RequireInternalSlot(S,[[SetData]])来检查s是否存在内部槽[[SetData]],很明显当this指向代理对象时,是没有这个内部槽的。因此出现错误。所以需要在get函数中新增条件判断,当访问size时,将在原对象上去执行get操作,这样就能正常执行了。代码如下:

// 新增对Set的操作
// 如果是访问size,则直接在原对象上进行操作
if(proxy === 'size') {
  return Reflect.get(target, proxy, target)
}

注:这里有个冲突,由于没有调用effect函数,所以activeEffect没有定义,直接返回了。导致测试的时候没有拿到size,但是注释掉就可以正常看到了,算是一个小问题,看源码的时候记得关注一下这个。也可能是代码放的位置没对,总之需要看源码。

Set的delete方法

直接调用delete方法同样会报错,(但是我的报错怎么是delete不是一个function…),反正总而言之,没有正确的执行。书上的报错信息显示delete的报错与size的报错基本一样。但是本质上是不一样的。

由于size时一个访问器属性,而delete是一个方法。当访问t.size的时候,访问器的getter函数会执行,因此可以修改get中this的指向来访问到原来的集合;但是执行delete时,delete方法并没有执行,而执行的是t.delete(1)这句函数调用。因此delete的this一定会指向代理对象,因为是t来调用的。

解决方法:讲delete方法与原始对象绑定即可,这样就会去原始对象上执行,而不是代理对象。代码如下:

// 读取正常属性时,正常操作
const res = target[proxy].bind(target)
建立响应联系
// 例子代码
const p = reactive(new Set([1,2,3]))
// effect中读取了代理对象的大小
effect(() => {
  console.log(p.size)
})
// 代理对象新增一个数据时,应该触发副作用函数,因为此时Set集合的大小发生了改变,但是实际上没有重新执行的,因为并没有建立响应式
p.add(4)

因此需要在访问size属性的时候,调用track函数进行依赖追踪,然后在add函数执行时调用trigger函数触发响应式。代码如下:

// 新增对Set的操作
// 如果是访问size,则直接在原对象上进行操作
if(proxy === 'size') {
  // 建立响应式联系
  track(target, ITERATE_KEY)
  return Reflect.get(target, proxy, target)
}

至于这里为什么建立的是target与ITERATE_KEY的联系,因为任何新增、删除操作都会影响size属性,这里需要与前面的for循环联系一下。接着当调用add时要触发响应式,首先要有add函数,这样才能在里面触发,因此需要重写一下add函数。代码如下:

// 集合Set的部分功能重写 
const mutableInstrumentations = {
  add(key) {
    // 由于是代理对象调用的add方法,所以this指向代理对象,通过raw属性拿到原始对象
    const target = this.raw
    // 在原始对象上执行add函数操作
    const res = target.add(key)
    // 触发副作用函数,为ADD操作
    trigger(target, key, 'ADD')
    // 返回结果
    return res
  }
}

function createReactive(obj, isShallow=false, isReadonly=false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if(key === 'raw') {
        return target
      }
      if(key === 'size') {
        track(target, ITERATE_KEY)
        return Reflect.get(target, key, target)
      }
      // 返回定义的方法
      return mutableInstrumentations[key]
    }
  })
}

由于不同类型的响应式对象,get拦截时,返回的结果也不一样。所以将会以这样的方式写在这里。add函数可以做一点优化,如果当前要插入的值已经存在于集合中,则不触发响应式。代码如下:

add(key) {
  // 由于是代理对象调用的add方法,所以this指向代理对象,通过raw属性拿到原始对象
  const target = this.raw
  // 先判断要插入的key是否已经存在于Set集合
  const isHave = target.has(key)
  // 如果不存在,则执行插入操作
  if(!isHave) {
    // 在原始对象上执行add函数操作
    const res = target.add(key)
    // 触发副作用函数,为ADD操作
    trigger(target, key, 'ADD')
  }
  // 返回结果
  return res
}

有了add函数的基础,delete是同样的。代码如下:

delete(key) {
  // 先获取原对象
  const target = this.raw
  // 判断集合中有没有要删除的元素
  const isHave = target.has(key)
  // 删除
  const res = target.delete(key)
  // 如果有要删除的元素,则触发响应式
  if(isHave) {
    trigger(target, key, 'DELETE')
  }
  // 返回结果
  return res
}

避免污染原始数据

通过Map的set和get来说明什么是污染原始数据。
首先同样是要重写set和get方法,因为这不是属性。代码如下:

get(key) {
  // 拿原始对象
  const target = this.raw
  // 判断当前的key是否存在
  const isHave = target.has(key)
  // 追踪依赖,建立连接
  track(target, key)
  // 如果存在,先拿到该value
  // 如果value是对象,则需要深度响应式,否则直接返回
  if(isHave) {
    const res = target.get(key)
    return typeof res === 'object' ? reactive(res) : res
  }
},
set(key, value) {
  const target = this.raw
  const isHave = target.has(key)
  // 先拿到旧值
  const oldVal = target.get(key)
  // 设置新值
  target.set(key, value)
  // 如果不存在key,则说明是新增,执行ADD操作
  // 否则说明是修改,且如果新值与旧值不一样才执行,且规避了NAN
  if(!isHave) {
    trigger(target, key, 'ADD')
  } else if(oldVal !== value || (oldVal === oldVal && value === value)) {
    trigger(target, key, 'SET')
  }
}

问题代码:

// 一个Map
const m = new Map()
// p1代理m
const p1 = reactive(m)
// p2是一个新的Map代理
const p2 = reactive(new Map())
// 在p1中添加p2
p1.set('p2', p2)
// 副作用函数中,在原始对象m中读取p2并访问其size属性
effect(() => {
  console.log(m.get('p2').size)
})
// 在原始对象m中读取p2并设置新的映射<foo, 1>
m.get('p2').set('foo', 1)

执行结果:
在这里插入图片描述

问题:在原始对象上的读取了size属性,又通过原始对象设置了新的映射。结果为副作用函数重新执行了。也就是原始对象具有了响应式,这是不应该出现的,因为如果原始对象有了响应式,那还要响应式干嘛…代码就会乱套。

分析:这里因为在原始对象中读取一个响应式属性,导致其追踪了这个副作用函数,然后又通过原始对象读取响应式数据来设置,因此触发了副作用函数的重新执行。

在set函数中,我们将value直接set到了target中,如果value是一个响应式数据,则意味着原始对象也成为了一个响应式数据。因此将响应式数据设置到原始数据上的行为称为数据污染。

解决方法:在设置之前判断value是不是一个响应式,如果是,则获取其raw属性,也就是响应式的原始数据设置到原始数据上;不是则直接设置上去。代码如下:

// 设置新值
// 添加响应式数据判断
const newVal = value.raw || value
target.set(key, newVal)

处理forEach

同样以Map为例。forEach接收一个回调函数,回调函数接收三个参数包括value、key和原始Map。遍历操作涉及到键值对的数量。因此当Map执行delete或add操作时,都应该重新执行一次forEach,因此,让forEach与ITERATE_KEY进行绑定,并重写forEach函数。代码如下:

forEach(callback) {
  // 先拿原始对象
  const target = this.raw
  // 追踪依赖, 建立联系
  track(target, ITERATE_KEY)
  // 通过原始对象来调用forEach,并传入callback
  target.forEach(callback)
}

上述代码虽然可以正常工作,但是是有缺陷的。因为我们仍然是在原始对象上执行的forEach函数。这就意味着丢掉了响应式数据。代码如下:

// Map中加入一个<{key: 1}, Set([1,2,3])>的键值对
const p = reactive(new Map([
  [{key: 1}, new Set([1,2,3])]
]))
// 副作用函数遍历Map,输出Set集合的size
effect(() => {
  p.forEach((value, key) => {
    console.log(value.size)
  })
})
// 通过key拿到value,然后删除Set集合的1
p.get({key: 1}).delete(1)

执行以上代码会发现副作用函数并没有重新执行。因为forEach中执行的是原始数据value,没有响应式了,但是reactive函数应该是深响应式的,与预期的不符合,因此需要修改forEach代码,让其键值具有响应式。代码如下:

// 同时接收第二个参数,用于指定callback函数执行时的this
forEach(callback, thisArg) {
  // 通过wrap来将键值中的Object改为深度响应式
  const wrap = val => {
    typeof val === "object" ? reactive(val) : val
  }
  // 先拿原始对象
  const target = this.raw
  // 追踪依赖, 建立联系
  track(target, ITERATE_KEY)
  // 通过原始对象来调用forEach,并传入callback
  target.forEach((value, key) => {
    // 每个键值对执行回调的时候,将其设置为响应式的
    // 通过call方法来调用callback,并传递thisArg
    callback.call(thisArg, wrap(value), wrap(key), this)
  })
}

最后需要考虑的一点是,Map数据结构与普通的集合对象遍历不同。普通对象的for…in循环只接收key值,因此只有在ADD或DELETE操作会影响key的数量时才会重新执行循环。但是Map的forEach循环接收value和key,所以它关心value值,因此Map在执行set操作时也应该重新执行一次循环,因为value值发生了改变。因此需要修改trigger函数,代码如下:

trigger() {
  // 只有当为新增或删除操作时,才执行迭代器的副作用函数
  if(type === TriggerType.ADD || type === TriggerType.DELETE || 
    // 新增对Map结构的SET操作时,也要执行一次循环
  (
    type === 'SET' && Object.prototype.toString.call(target) === '[Object Map]'
  )) {
    // 获取与ITERATE_KEY相关联的副作用函数
    const iterateEffects = depsMap.get(ITERATE_KEY)
    iterateEffects && iterateEffects.forEach(effect => {
      if(effect != activeEffect) {
        effectsToRun.add(effect)
      }
    })
  }
}

迭代器方法

集合的迭代器有三个:entries、keys、value。另外,由于Map和Set类型本身部署了Symbol.iterator方法,因此可以直接用for…of进行迭代;同样,可以调用迭代器对象的next方法进行手动迭代。代码如下:

const m = new Map([
    [k1, value1],
    [k2, value2]
  ])
const itr = m[Symbol.iterator]()
// 此方法与entries效果相同
console.log(itr.next())

此时如果在effect中使用for…of循环迭代一个Map对象,会返回一个错误:

const t = reactive(new Map([
  ['k1', 'value1'],
  ['k2', 'value2']
]))
effect(() => {
  for(const [key, value] of t) {
    console.log(key, value)
  }
})

图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/VfsMrofGlCsJUruw/image.png

显示t不是一个可迭代对象。同时发现在代理对象的get函数对Symbol.iterator进行了拦截,说明我们并没有实现这个方法。因此需要在mutableInstrumentations中实现;代码如下:

 [Symbol.iterator]() {
  // 先拿原对象
  const target = this.raw
  // 获取其迭代器方法
  const itr = target[Symbol.iterator]()
  // 返回
  return itr
}

可以迭代了
图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/ztTDlbQ5p9cr1Ow1/image.png

这里存在与forEach相同的问题,就是如果key或value本身是对象,就需要深层的响应式,而在原对象中调用迭代器是没有这样的功能的,同时需要进行依赖追踪,修改代码:

[Symbol.iterator]() {
  // 先拿原对象
  const target = this.raw
  // 获取其迭代器方法
  const itr = target[Symbol.iterator]()
  // 深层响应式函数
  const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
  // 依赖追踪
  track(target, ITERATE_KEY)

  // 返回自定义的迭代器
  return {
    next() {
      const {value, done} = itr.next()
      return {
        // 如果value不是undefined,则进行深度响应式判断
        value: value ? [wrap(value[0]), wrap(value[1])] : value,
        done
      }
    }
  }
}

由于对迭代器的操作不涉及内部元素,因此只需要在新增或删除时才触发响应式。至此,对for…of循环的拦截就完成了。不过考虑到entries与Symbol.iterator等价,因此可以使用相同的代码对entires函数进拦截。但是直接将上述函数给到entries会报错。因为这里要考虑到可迭代协议和迭代器协议。

额外知识:可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for…of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性 。

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器 :(参考JS高级)

图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/gmhrvOuXbVkTs4pP/image.png

因此,迭代器协议就是把数据结构加上统一接口使得该数据结构能够遍历,可迭代协议就是把可迭代对象通过Symbol.iterator迭代器函数转换为迭代器对象,通过迭代器协议对迭代器对象进行迭代。

PS:不可能判断一个特定的对象是否实现了迭代器协议,然而,创造一个同时满足迭代器协议和可迭代协议的对象是很容易的。这样做允许一个迭代器能被各种需要可迭代对象的语法所使用。因此,很少会只实现迭代器协议,而不实现可迭代协议。
因此上述问题的解决方法就是同时实现迭代器协议和可迭代协议。代码如下:

function iteratorMethod() {
  // 先拿原对象
  const target = this.raw
  // 获取其迭代器方法
  const itr = target[Symbol.iterator]()
  // 深层响应式函数
  const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
  // 依赖追踪
  track(target, ITERATE_KEY)

  // 返回自定义的迭代器
  return {
    next() {
      const {value, done} = itr.next()
      return {
        // 如果value不是undefined,则进行深度响应式判断
        value: value ? [wrap(value[0]), wrap(value[1])] : value,
        done
      }
    },
    // 可迭代协议
    [Symbol.iterator]() {
      return this
    }
  }
}
// 在mutableInstrumentations方法中直接复用
  [Symbol.iterator]: iteratorMethod,

entries:iteratorMethod
entries遍历:
图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/ksUq9QhrizEkOzyr/image.png图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/LE1kshl8c9kQB6qV/image.png

直接遍历:
图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/RXWSlyQMMWc1hXK2/image.png图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/4JwwWDdtCVkf4JNK/image.png

values与keys方法

values与entries方法相似,只不过values遍历的是Map中的value;同时keys与values页相似,只不过keys遍历的是Map中的key。因此基本上,只需要微小的改动就可以复用上一节的代码:

  values() {
    // 先拿原对象
    const target = this.raw
    // 获取其迭代器方法
    const itr = target.values()
    // 深层响应式函数
    const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
    // 依赖追踪
    track(target, ITERATE_KEY)

    // 返回自定义的迭代器
    return {
      next() {
        const {value, done} = itr.next()
        return {
          // 如果value不是undefined,则进行深度响应式判断
          value: wrap(value),
          done
        }
      },
      // 可迭代协议
      [Symbol.iterator]() {
        return this
      }
    }
  },
  keys() {
    // 先拿原对象
    const target = this.raw
    // 获取其迭代器方法
    const itr = target.keys()
    // 深层响应式函数
    const wrap = val => typeof val === 'object' && val !== null ? reactive(val) : val
    // 依赖追踪
    track(target, ITERATE_KEY)

    // 返回自定义的迭代器
    return {
      next() {
        const {value, done} = itr.next()
        return {
          // 如果value不是undefined,则进行深度响应式判断
          value: wrap(value),
          done
        }
      },
      // 可迭代协议
      [Symbol.iterator]() {
        return this
      }
    }
  }

变化在原对象方法的调用上,即拦截什么方法就在原对象上使用什么方法;还有就是深度响应式时,由于是单个对象,所以只需要直接对其进行操作就可以。
对于entries和values方法来说,如果有value的修改操作,需要重新执行副作用函数,这里直接是复用了以前的对SET类型的Map判断。代码如下:

// 只有当为新增或删除操作时,才执行迭代器的副作用函数
  if(type === TriggerType.ADD || type === TriggerType.DELETE || 
    // 新增对Map结构的SET操作时,也要执行一次循环
  (
    type === 'SET' && Object.prototype.toString.call(target) === '[object Map]'
  )) {/**/}

对于新增和删除操作来说,entries、values和keys都需要重新执行,但是对于keys方法来说,value的改变不需要重新执行副作用函数,而且不存在修改key的方法,因此需要一个新的标志来区分keys方法的遍历。代码如下:

// 只有当为新增或删除操作、对象为Map时,执行keys方法所涉及的副作用函数
if((type === TriggerType.ADD || type === TriggerType.DELETE) && 
    Object.prototype.toString.call(target) === '[object Map]') {
  // 获取与MAP_KEY_ITERATE_KEY相关联的副作用函数
  const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
  iterateEffects && iterateEffects.forEach(effect => {
    if(effect != activeEffect) {
      effectsToRun.add(effect)
    }
  })
}

验证效果如下,修改value不需要重新执行副作用函数。
图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/ttEQWkBwd7gJObwG/image.png图片: https://cooper.didichuxing.com/cooper_gateway/cn/shimo-images/Ei2mpFvcKxUx7sRN/image.png

Logo

前往低代码交流专区

更多推荐