全面了解Vue3的 reactive 和相关函数(上)

温故而知新,同样的知识点,再次学习的时候也可以有新的认识,这里把 reactive 和相关函数总体介绍一下。
从 ES6 的 Proxy 开始,到Vue3提供的各种函数,尽量的把自己的理解融入进来,当然目前水平限制,无法做到非常深入的程度。以后有更深的了解之后还会有新的见解。

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 操作,当然还可以拦截其他操作。

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


自己写的 Proxy 实例的运行结果
  • 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)  // 这里有变化,最后一个参数改成 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) 

运行结果:


reactive的打印结果
  • Handler:可以看到 Vue 除重写 set 和 get 外,还重写了deleteProperty、has和ownKeys。

  • Target: 指向一个Object,这是建立reactive实例时的对象。

属性的结构:


reactive的属性打印结果

然后再看一下两个属性的打印结果,因为 contacts 属性是嵌套的对象,所以单独拿出来也是具有响应性的。

而 name 属性由于是 string 类型,所以单独拿出来并不会自动获得响应性,如果单独拿出来还想保持响应性的话,可以使用toRef。

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

reactive 相关函数放在下一篇介绍。

源码:

GitHub总是上不去,所以搬到gitee了。

https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi

在线演示:

https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/

你可能感兴趣的:(全面了解Vue3的 reactive 和相关函数(上))