Vue3 响应式系统 - 4 - object的响应式方案

1. 理解Proxy和Reflect

使用Proxy可以创建一个代理对象,允许对被代理对象的基本操作进行拦截并重新定义。

const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    …
  },
  set(target, key, newVal, receiver) {
    …
  }
})

如上代码所示,Proxy对象接受两个参数,一个是data对象,第二个是参数是个对象,这个对象中包含了各种函数,比如其中的get用来拦截数据的读取操作,set用来拦截数据的赋值操作。

在js中函数也是一种对象,函数的调用也可以使用Proxy拦截器中apply函数来拦截。

const proxyFn = new Proxy(fn, {
  apply(target, that, arg) {
    …
    return target.call(that, ...arg)
  }
})

理解了Proxy,再来看下Reflect,它是一个全局对象,Proxy拦截器中的方法都能在Reflect中找到同名的方法,其实它就是提供了一个对象操作属性的默认行为。

const d = {
  a: 1
}

d.a
Reflect.get(d, 'a')

上述两种读取数据的方式是等价的,那Reflect存在的意义又是什么呢?

其实Reflect.get还可以接收第三个参数receiver,可以理解为函数调用过程中的this,以下这行事例代码读取到的值不是1而是2。

Reflect.get(d, 'a', { a: 2 })

其实Reflect还有更多方面的意义,我们只讨论这个特点,是因为它与响应式数据的实现密切相关。

2. 响应式数据的实现与Reflect的关系

在之前的代码基础上运行以下代码

const origin = {
  a: 1,
  b: 2,
  c: 3,
  get d() {
    return this.a
  }
}
const data = reactive(origin)
effect(() => {
  console.log(data.d)
})
data.a++

当data.a改变时副作用函数会响应吗?答案是不会,原因就在于这个this,当Proxy拦截到data.d的get操作时访问了this.a,这个this指向的其实是原始对象origin的a属性,而origin并不是响应式的对象。原因找到了,再来考虑如何解决问题,如果让this.a最终访问到data.a问题不就解决了嘛。Proxy拦截器可接收的参数中有一个receiver,它代表的就是当前的代理对象,再配合Reflect对象的对应方法,将reactive方法稍加改造就ok啦。

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver)
      trigger(target, key)
      return res
    }
  })
}

3. 如何代理object

目前为止我们知道了当响应式对象的属性被读取的时候,会将依赖收集起来,但在js世界中“读取”是个非常宽泛的操作,仅用get函数拦截不到全部。

effect(() => {
  console.log('a' in data)
})

比如in操作符,它是不会被get函数拦截住的。但通过对ECMA规范的解读,得知它最终是依赖对象的内部方法HasProperty来实现的,而内部方法HasProperty对应的拦截器函数叫做has,那么就可以通过has方法来拦截。

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver)
      trigger(target, key)
      return res
    },
    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    }
  })
}

那for … in遍历对象读取其属性的时候应该用什么来拦截呢?根据ECMA规范分析得到结论拦截器的ownKeys方法可以拦截for in遍历读取。但有个问题,ownKeys函数只接收一个target函数,并没有key,而track方法是需要一个key来收集依赖的,那么就需要我们来手动定一个ITERATE_KEY来协助依赖的收集。除此以外,我们还需要trigger(target, ITERATE_KEY),target.ITERATE_KEY永远都不会被重置所以set函数拦截的方式是无法完成的,因此我们可以对trigger函数本身下手。

const ITERATE_KEY = Symbol()
const trigger = (target, key) => {
  const targetMap = bucketMap.get(target)
  const deps = targetMap.get(key)
  const iterate_deps = targetMap.get(ITERATE_KEY)
  const functionRun = new Set([...(deps || []), ...(iterate_deps || [])])

  functionRun.forEach(f => {
    if (f !== activeFuction) {
      f.options?.scheduler ? f.options.scheduler(f) : f()
    }
  });
}
const reactive = (data) => {
  return new Proxy(data, {
    get (target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver)
      trigger(target, key)
      return res
    },
    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
  })
}

我们知道track(target, ITERATE_KEY)只会在for in的时候执行,那么只需要在影响for in的操作的时候来执行trigger(target, ITERATE_KEY)就可以了,即属性的增删。

增加的属性实质上触发的就是set函数,我们需要在set函数中使用Object.prototype.hasOwnProperty.call(target, key)来确定当前变动的属性是否为新增的属性就可以了。属性的删除操作delete target.key,用到的是delete操作符,根据EMCA规范的分析我们可以了解到它依赖的是object的内部函数Delete,对应拦截器里的函数也就是deleteProperty。那么我们的reactive和trigger方法就可以稍加改造,变成以下代码:

const ITERATE_KEY = Symbol()
const trigger = (target, key, type) => {
  const targetMap = bucketMap.get(target)
  const deps = targetMap.get(key)

  const functionRun = new Set(deps || [])
  if (type === 'ADD' || type === 'DELETE') {
    const iterate_deps = targetMap.get(ITERATE_KEY) || []
    iterate_deps.forEach(f => {
      functionRun.add(f)
    })
  }

  functionRun.forEach(f => {
    if (f !== activeFuction) {
      f.options?.scheduler ? f.options.scheduler(f) : f()
    }
  });
}

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      const TYPE = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
      const res = Reflect.set(target, key, newValue, receiver)
      trigger(target, key, TYPE)
      return res
    },
    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
    deleteProperty(target, key, receiver) {
      const res = Reflect.deleteProperty(target, key, receiver)
      trigger(target, key, 'DELETE')
      return res
    }
  })
}

4. 合理的触发响应

基于目前的响应式逻辑,只要拦截器set函数被触发就会执行依赖,也就是说只要进行赋值操作就会执行依赖即使值没有改变

const data = reactive({
  a: 1
})

effect(() => {
  console.log(data.a)
})

data.a = 1

首先能想到的第一步,就是在set中做新旧值的比较,若新旧值不相等则执行trigger方法

const reactive = (data) => {
  return new Proxy(data, {
    …
    set (target, key, newValue, receiver) {
      const oldVal = target[key]
      const TYPE = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
      const res = Reflect.set(target, key, newValue, receiver)
      if (oldVal !== newValue) {
        trigger(target, key, TYPE)
      }
      return res
    },
    …
  })
}

NaN比较特殊,若新旧值都为NaN,则oldVal !== newVal的结果是true就会造成不必要的trigger,那么我们需要再加上一个条件oldVal === oldVal || newValue === newValue来过滤新旧值都为NaN的情况。

const originParent = {
  a: 1
}
const parent = reactive(originParent)

const originChild = {}
const child = reactive(originChild)

Object.setPrototypeOf(child, parent)

effect(() => {
  console.log(child.a)
})

child.a++

但只做这种程度的过滤还远远不够,如果如下代码:

const originParent = {
  a: 1
}
const parent = reactive(originParent)

const originChild = {}
const child = reactive(originChild)

Object.setPrototypeOf(child, parent)

effect(() => {
  console.log(child.a)
})

child.a++

我们把child的原型设为parant,那么child.a应该会继承parent.a的值即为1,那么child.a++应该会触发副作用函数,实际上也触发了而且触发两次,猜都猜的出child.a的set触发了一次parent.a的set又触发了一次,如何解释这个现象呢?

当要访问的对象没有该属性的时候,会从对象的原型上查找,如果查找到就会调用原型对象get函数从而获得值,而child的原型对象也是一个响应式对象,访问child.a的时候child和parent都建立了依赖关系。而这还是不能解释为啥改动child.a会触发parent.a的set,这其实就是因为Reflect.set(x, x, x)执行了child对象更改a属性的默认行为 即 child无a属性去获得其原型parent对象并调用它的set方法。

问题找到,前边说set方法中接收的参数target和receiver分别代表了key的原始对象和代理对象,当我们访问child.a的时候,child的拦截器中target和receiver分别是originChild和child而parent的拦截器中target和recerver分别是originParent和child。如果能把原始对象和代理对象之间建立某种联系,然后在set方法中验证target和receiver是否对应,若不对应则不触发trigger方法,理论上就能解决了这个问题。

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key, receiver) {
      if (key === 'raw') return target
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      const oldVal = target[key]
      const TYPE = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
      const res = Reflect.set(target, key, newValue, receiver)
      if (receiver.raw === target) {
        if (oldVal !== newValue && (oldVal === oldVal || newValue === newValue)) {
          trigger(target, key, TYPE)
        }
      }
      return res
    },
    has(target, key) {
      track(target, key)
      return Reflect.has(target, key)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
    deleteProperty(target, key, receiver) {
      const res = Reflect.deleteProperty(target, key, receiver)
      trigger(target, key, 'DELETE')
      return res
    }
  })
}

在拦截器get方法内,给receiver手动加一个属性,比如raw,当访问data.raw的时候得到的是原始对象target。然后在set方法内验证receiver的原始对象与target是否一致,如果一致才去触发trigger函数。

5. 深度响应

基于目前的代码,我们也只是做到了浅响应,如果有以下的代码,则副作用函数将不会按照预期的执行。

const data = reactive({
  a: {
    b: 1
  }
})

effect(() => {
  console.log(data.a.b)
})

data.a.b = 2

先来看目前的拦截器get方法代码:

get (target, key, receiver) {
  if (key === 'raw') return target
  track(target, key)
  return Reflect.get(target, key, receiver)
}

当我们访问data.a的时候,得到的是{b: 1}而它并不是一个响应式对象,那么我们把它也变成响应式对象不就可以了嘛。

get (target, key, receiver) {
  if (key === 'raw') return target
  track(target, key)
  if (typeof target[key] === 'object' && target[key] !== null) return reactive(target[key])
  return Reflect.get(target, key, receiver)
},

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我会尽力回答你的问题。首先,我们需要了解Vue3的响应式系统是如何工作的。Vue3使用了一个名为`Reactive`的函数来实现响应式。 `Reactive`函数的作用是将一个普通的JavaScript对象转换成响应式的对象。当响应式对象的属性被修改时,所有依赖该属性的地方都会自动更新。 下面是`Reactive`函数的实现: ```javascript function Reactive(obj) { const handlers = { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); track(target, prop); return isObject(value) ? Reactive(value) : value; }, set(target, prop, value, receiver) { const oldValue = Reflect.get(target, prop, receiver); let result = true; if (oldValue !== value) { result = Reflect.set(target, prop, value, receiver); trigger(target, prop); } return result; }, deleteProperty(target, prop) { const result = Reflect.deleteProperty(target, prop); trigger(target, prop); return result; } }; return new Proxy(obj, handlers); } ``` `Reactive`函数接受一个普通的JavaScript对象作为参数,返回一个响应式的对象。在实现中,我们使用了ES6的Proxy对象来实现响应式。 在`get`处理器中,我们使用了`track`函数来收集依赖。`track`函数的作用是将当前正在执行的计算函数添加到依赖列表中。 在`set`处理器中,我们首先获取旧值,然后判断新值是否与旧值相同。如果不同,我们使用`trigger`函数来触发更新。`trigger`函数的作用是遍历依赖列表,执行所有计算函数。 在`deleteProperty`处理器中,我们使用`trigger`函数来触发更新,因为删除属性也可能导致依赖更新。 在以上代码中,我们还使用了`isObject`函数来判断一个值是否为对象。该函数的实现如下: ```javascript function isObject(value) { return typeof value === 'object' && value !== null; } ``` 这个函数非常简单,它只是判断一个值是否为对象。如果是对象,我们就递归调用`Reactive`函数来将该对象转换成响应式。 总之,这就是Vue3的响应式系统的实现原理。通过`Reactive`函数和Proxy对象,我们可以将一个普通的JavaScript对象转换成响应式的对象,并实现自动更新。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值