Vue3.js“非原始值”响应式实现基本原理笔记(三)

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第5.1节5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前文:
Vue3.js“非原始值”响应式实现基本原理笔记(一)
Vue3.js“非原始值”响应式实现基本原理笔记(二)

合理触发响应

5.4节讨论了除上一节笔记中其他的合理操作

值没有发生变化

如果执行p.foo=1,那么同样不应该触发副作用函数,所以需要在set中新增一个判断,即旧值和新值是否相等,代码如下:

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的全等对比

  1. NaN === NaN // false
  2. NaN !== NaN // true

所以还需再加一个判断条件,代码如下:

 if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal ))

从原型上继承属性——reactive

在书中的案例中,作者定义了一个reactive函数,对Proxy进行了封装,代码如下:

function reactive(obj) {
  return new Proxy(obj, {
    // 其他逻辑
  })
}

这其实是第一次出现reactive,值得注意

接着书中给了一个案例代码:

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 // 会导致副作用函数重新执行两次

分析一下各个对象的关系

  1. childobj的代理对象
  2. parentproto的代理对象
  3. Object.setPrototypeOf设置parentchild的原型

我们知道原型链,如果访问child.bar没有,那么会沿着原型链找到parent.bar,也可以说是继承

现在来分析一下child.bar会导致副作用函数执行两次的流程:

  1. 读取child.bar,触发child里的get
  2. 执行Reflect.get(obj, 'bar', receiver)
  3. 发现child内没有bar,读取parent.bar
  4. 读取parent.bar时与副作用函数建立联系
  5. 在第一次读取的track(obj,bar)中,child.bar也与副作用函数建立了联系

所以child.barparent.bar都与副作用函数建立了联系

修改child.bar时的流程:

  1. 调用child.set,执行Reflect.set
  2. child没有bar,变为执行parent.set

我们知道执行set就会触发trigger,所以就执行了两次副作用函数

那如何避免两次执行呢?答案是在set中进行区分两次更新

首先看一下child里的set,代码如下:

// child 的 set 拦截函数
set(target, key, value, receiver) {
  // target 是原始对象 obj
  // receiver 是代理对象 child
}

这里的receiver,一般情况就是Proxy的实例本身,在这里也就是child

可以看阮一峰ES6里的示例代码:

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    return true;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true

接着看一下parent里的set,代码如下:

// parent 的 set 拦截函数
set(target, key, value, receiver) {
  // target 是原始对象 proto
  // receiver 仍然是代理对象 child
}

这里我们知道target就是parent被代理对象proto,但为什么receiver还是child呢?

这是因为实际操作的上下文对象child,而不是parent

那么现在可以发现一个特点,就是target是变化的,而receiver是不变的,所以可以通过判断receiver是否是target的代理对象即可

也就是obj的代理对象是child,而proto的代理对象不是child

回到问题本身的解决方案,首先是在get中添加了一个判断条件,代码如下:

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)
    }
    // 省略其他拦截函数
  })  
}

如果访问raw,那么会返回对应的值,例如child.raw,那么返回obj

接着回到set,新增一个语句,代码如下:

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
    }
    // 省略其他拦截函数
  })  
}

set中判断了target是否等于receiver.raw,如果是的话才会调用trigger

这样的话问题就解决了

总结

  1. 使用reactive封装Proxy,为浅响应和深响应做铺垫
  2. 解决了原型链修改属性问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小王hs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值