手拉手带你了解Vue3的reactive和相关函数(一)

简介: 网上介绍 reactive 的各种资料也是非常多了,这里按照官网的介绍,对reactive以及相关的几个函数,做一个综合介绍,另外加一些扩展和稍微深入一点的内容。

网上介绍 reactive 的各种资料也是非常多了,这里按照官网的介绍,对reactive以及相关的几个函数,做一个综合介绍,另外加一些扩展和稍微深入一点的内容。


ES6的Proxy

Proxy是ES6提供的一个可以拦截对象基础操作的代理。因为 reactive 采用Proxy代理的方式,实现引用类型的响应性,所以我们先看看 Proxy 的基础使用方法,以便于我理解 reactive 的结构。我们先来定义一个函数,了解一下 Proxy 的基本使用方式:

// 定义一个函数,传入对象原型,然后创建一个Proxy的代理
const myProxy = (_target) => {
  // 定义一个 Proxy 的实例
  const proxy = new Proxy(_target, {
    // 拦截 get 操作
    get: function (target, key, receiver) {
      console.log(`getting ${key}!`, target[key])
      // 用 Reflect 调用原型方法
      return Reflect.get(target, key, receiver)
    },
    // 拦截 set 操作
    set: function (target, key, value, receiver) {
      console.log(`setting ${key}:${value}!`)
      // 用 Reflect 调用原型方法
      return Reflect.set(target, key, value, receiver)
    }
  })
  // 返回实例
  return proxy
}
// 使用方法,是不是和reactive有点像?
const testProxy = myProxy({
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})
console.log('自己定义的Proxy实例:')
console.log(testProxy)
// 测试拦截情况
testProxy.name = '新的名字' // set操作 
console.log(testProxy.name) // get 操作

Proxy有两个参数target和handle。* target:要代理的对象,也可以是数组,但是不能是基础类型。* handler:设置要拦截的操作,这里拦截了set和get操作,当然还可以拦截其他操作。

我们先来看一下运行结果:

28.png


  • Handler 可以看到我们写的拦截函数 get 和 set;
  • Target 可以看到对象原型。

注意:这里只是实现了 get 和 set 的拦截,并没有实现数据的双向绑定,模板也不会自动更新内容,Vue内部做了很多操作才实现了模板的自动更新功能。


用 Proxy 给 reactive 套个娃,会怎么样?

有个奇怪的地方,既然Proxy可以实现对set等操作的拦截,那么 reactive 为啥不返回一个可以监听的钩子呢?为啥要用watch来实现监听的工作呢?为啥会这么想?看看Vuex4.0的设计,明明已经把state整体自动变成了 reactive 的形式,那么为啥还非得在 mutations 里写函数,实现 set 操作呢?好麻烦的样子。外部直接对 reactive 进行操作,然Vuex内部监听一下,这样大家不就都省事了吗?要实现插件功能,还是跟踪功能,不都是可以自动实现了嘛。所以我觉得还是可以套个娃的。


实现模板的自动刷新本来以为上面那个 myProxy 函数,传入 一个 reactive 之后,就可以自动实现更新模板的功能了,结果模板没理我。这不对呀,我只是监听了一下,不是又交给reactive了吗?为啥模板不鸟我?经过各种折腾,终于找到了原因,于是函数改成了这样:

/**
   * 用 Proxy定义一个 reactive 的套娃,实现可以监听任意属性变化的目的。(不包含嵌套对象的属性)
   * @param {*} _target  要拦截的目标
   * @param {*} callback 属性变化后的回调函数
   */
  const myReactive = (_target, callback) => {
    let _change = (key, value) => {console.log('内部函数')}
    const proxy = new Proxy(_target, {
      get: function (target, key, receiver) {
        if (typeof key !== 'symbol') {
          console.log(`getting ${key}!`, target[key])
        } else {
          console.log('getting symbol:', key, target[key])
        }
        // 调用原型方法
        return Reflect.get(target, key, receiver)
      },
      set: function (target, key, value, receiver) {
        console.log(`setting ${key}:${value}!`)
        // 源头监听
        if (typeof callback === 'function') {
          callback(key, value)
        }
        // 任意位置监听
        if (typeof _target.__watch === 'function') {
          _change(key, value)
        }
        // 调用原型方法
        return Reflect.set(target, key, value, target)
      }
    })
    // 实现任意位置的监听,
    proxy.__watch = (callback) => {
      if (typeof callback === 'function') {
        _change = callback
      }
    }
    // 返回实例
    return proxy
  }

代码稍微多了一些,我们一块一块看。

  • get 这里要做一下 symbol 的判断,否则会报错。好吧,其实我们似乎不需要 console.log。
  • set 这里改了一下最后一个参数,这样模板就可以自己更新了。
  • 设置 callback 函数,实现源头监听 设置一个回调函数,才能在拦截到set操作的时候,通知外部的调用者。只是这样只适合于定义实例的地方。那么接收参数的地方怎么办呢?

调用方法如下:

// 定义一个拦截reactive的Proxy
    // 并且实现源头的监听
    const myProxyReactive = myReactive(retObject,
      ((key, value) =>{
        console.log(`ret外部获得通知:${key}:${value}`)
      })
    )

这样我们就可以在回调函数里面得到修改的属性名称,以及属性值。这样我们做状态管理的时候,是不是就不用特意去写 mutations 里面的函数了呢?

  • 内部设置一个钩子函数 设置一个 _change() 钩子函数,这样接收参数的地方,可以通过这个钩子来得到变化的通知。

调用方法如下:

// 任意位置的监听
    myProxyReactive.__watch((key, value) => {
      console.log(`任意位置的监听:${key}:${value}`)
    })

只是好像哪里不对的样子。首先这个钩子没找到合适的地方放,目前放在了原型对象上面,就是说破坏了原型对象的结构,这个似乎会有些影响。然后,接收参数的地方,不是可以直接得到修改的情况吗?是否还需要做这样的监听?

最后,好像没有watch的deep监听来的方法,那么问题又来了,为啥Vuex不用watch呢?或者悄悄的用了?


深层响应式代理:reactive

说了半天,终于进入正题了。reactive 会返回对象的响应式代理,这种响应式转换是深层的,可以影响所有的嵌套对象。

注意:返回的是 object 的代理,他们的地址是相同的,并没有对object进行clone(克隆),所以修改代理的属性值,也会影响原object的属性值;同时,修改原object的属性值,也会影响reactive返回的代理的属性值,只是代理无法拦截直接对原object的操作,所以模板不会有变化。

这个问题并不明显,因为我们一般不会先定义一个object,然后再套上reactive,而是直接定义一个 reactive,这样也就“不存在”原 object 了,但是我们要了解一下原理。我们先定义一个 reactive 实例,然后运行看结果。

// js对象
const person = {
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
}
// person 的 reactive 代理 (验证地址是否相同)
const personReactive = reactive(person)
// js 对象 的 reactive 代理 (一般用法)
const objectReactive = reactive({
  name: 'jykReactive',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})
// 查看 reactive 实例结构
console.log('reactive', objectReactive )
// 获取嵌套对象属性
const contacts = objectReactive .contacts
// 因为深层响应,所以依然有响应性
console.log('contacts属性:', contacts)
// 获取简单类型的属性
let name = objectReactive.name 
// name属性是简单类型的,所以失去响应性
console.log('name属性:', name)

运行结果:


29.png


  • Handler:可以看到Vue除重写set和get外,还重写了deleteProperty、has和ownKeys。
  • Target:指向一个Object,这是建立reactive实例时的对象。

属性的结构:

30.png


然后再看一下两个属性的打印结果,因为contacts属性是嵌套的对象,所以单独拿出来也是具有响应性的。而name属性由于是string类型,所以单独拿出来并不会自动获得响应性,如果单独拿出来还想保持响应性的话,可以使用toRef。

注意:如果在模板里面使用{{personReactive.name}}的话,那么也是有响应性的,因为这种用法是获得对象的属性值,可以被Proxy代理拦截,所以并不需要使用toRef。如果想在模板里面直接使用{{name}}并且要具有响应性,这时才需要使用toRef。


相关文章
|
2天前
|
JavaScript 前端开发 CDN
vue3速览
vue3速览
11 0
|
2天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(中)
Vue3 官方文档速通(中)
9 0
|
2天前
|
缓存 JavaScript 前端开发
Vue3 官方文档速通(上)
Vue3 官方文档速通(上)
9 0
|
2天前
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
Vue3+Vite+Pinia+Naive后台管理系统搭建之五:Pinia 状态管理
7 1
|
3天前
|
缓存 监控 JavaScript
探讨优化Vue应用性能和加载速度的策略
【5月更文挑战第17天】本文探讨了优化Vue应用性能和加载速度的策略:1) 精简代码和组件拆分以减少冗余;2) 使用计算属性和侦听器、懒加载、预加载和预获取优化路由;3) 数据懒加载和防抖节流处理高频事件;4) 图片压缩和选择合适格式,使用CDN加速资源加载;5) 利用浏览器缓存和组件缓存提高效率;6) 使用Vue Devtools和性能分析工具监控及调试。通过这些方法,可提升用户在复杂应用中的体验。
10 0
|
3天前
|
JavaScript 前端开发
vue(1),小白看完都会了
vue(1),小白看完都会了
|
3天前
|
JavaScript 数据库
ant design vue日期组件怎么清空 取消默认当天日期
ant design vue日期组件怎么清空 取消默认当天日期
|
3天前
|
JavaScript C++
vue高亮显示组件--转载
vue高亮显示组件--转载
8 0
|
2天前
|
JavaScript 开发工具 git
Vue 入门系列:.env 环境变量
Vue 入门系列:.env 环境变量
9 1