查看原文
其他

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

自然框架 脚本之家 2022-05-10
 关注
“脚本之家
”,与百万开发者在一起

作者 | 自然框架
出品 | 脚本之家(ID:jb51net)
网上介绍 reactive 的各种资料也是非常多了,这里按照官网的介绍,对reactive以及相关的几个函数,做一个综合介绍,另外加一些扩展和稍微深入一点的内容。

1

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

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

  • Handler 可以看到我们写的拦截函数 get 和 set;

  • Target 可以看到对象原型。

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



2

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

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

3

实现模板的自动刷新
本来以为上面那个 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呢?或者悄悄的用了?


4

深层响应式代理: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) 
运行结果:

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

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

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



5

浅层响应式代理:shallowReactive

有的时候,我们并不需要嵌套属性也具有响应性,这时可以使用shallowReactive 来获得浅层的响应式代理,这种方式只拦截自己的属性的操作,不涉及嵌套的对象属性的操作。
const personShallowReactive = shallowReactive({
  name: 'jykShallowReactive',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})

// 查看 shallowReactive 实例结构
console.log('shallowReactive', objectShallowReactive)

// 获取嵌套对象属性
const contacts = objectShallowReactive.contacts
// 因为浅层代理,所以没有响应性
console.log('contacts属性:', contacts)

// 获取简单类型的属性
let name = objectShallowReactive.name 
// 因为浅层代理且简单类型,所以失去响应性
console.log('name属性:', name) 
shallowReactive的打印结果
shallowReactive 也是用 Proxy 实现响应性的,而单独使用contacts属性并没有响应性,因为 shallowReactive 是浅层代理,所以不会让嵌套对象获得响应性。

注意:objectShallowReactive.contacts.QQ = 123 ,这样修改属性也是没有响应性的。

单独使用的属性的形式:

shallowReactive的属性

嵌套对象和name属性,都没有变成响应式。

6

做一个不允许响应的标记:markRaw

有的时候我们不希望js对象变成响应式的,这时我们可以用markRaw 做一个标记,这样即使使用 reactive 也不会变成响应式。

如果确定某些数据是不会变化的,那么也就不用变成响应式,这样可以节省一些不必要的性能开销。

// 标记js对象
const object = markRaw({
  name: 'jyk',
  age: 18,
  contacts: {
    QQ: 11111,
    phone: 123456789
  }
})

// 试图对标记的对象做相应性代理
const retObject2 = reactive(object)
// 使用对象的属性做相应性代理
const retObject1 = reactive({
  name: object.name
})
console.log('作为初始值:', retObject1) // 无法变成响应性代理
console.log('无法变成响应式:', retObject2) // 可以变成响应性代理

运行结果:

markRaw的打印结果
做标记后的js对象作为参数,不会变成响应式,但是使用属性值作为参数,还是可以变成响应式。
那么哪些地方可以用到呢?我们可以在给组件设置(引用类型的)属性的时候使用,默认情况下组件的属性都是自带响应性的,但是如果父组件里设置给子组件的属性值永远不会发生变化,那么还变成响应式的话,就有点浪费性能的嫌疑了。
如果想节约一下的话,可以在父组件设置属性的时候加上markRaw标记。


7

深层只读响应式代理:readonly

有的时候虽然我们想得到一个响应式的代理,但是只想被读取,而不希望被修改(比如组件的props,组件内部不希望被修改),那么这时候我们可以用readonly。
readonly可以返回object、reactive或者ref的深层只读代理,我们来分别测试一下:
// object的只读响应代理
const objectReadonly = readonly(person)
// reactive 的只读响应代理
const reactiveReadonly = readonly(objectReactive)
// 查看 readonly 实例结构
console.log('object 的readonly', objectReadonly)
console.log('reactive 的readonly', reactiveReadonly)

// 获取嵌套对象属性
const contacts = reactiveReadonly.contacts
console.log('contacts属性:', contacts) // 因为深层响应,所以依然有响应性

// 获取简单类型的属性
let name = reactiveReadonly.name 
console.log('name属性:', name) // 属性是简单类型的,所以失去响应性

运行结果:

object的readonly
  • Handler,明显拦截的函数变少了,set的参数也变少了,点进去看源码,也仅仅只有一行返回警告的代码,这样实现拦截设置属性的操作。

  • Target,指向object。

运行结果:
reactive的readonly
  • Handler,这部分是一样的。

  • Target,指向的不是object,而是一个Proxy代理,也就是reactive。

8

浅层只读响应代理:shallowReadonly

和readonly相对应,shallowReadonly是浅层的只读响应代理,和readonly的使用方式一样,只是不会限制嵌套对象只读。
// object 的浅层只读代理
const objectShallowReadonly = shallowReadonly(person)
// reactive 的浅层只读代理
const reactiveShallowReadonly = shallowReadonly(objectReactive)
shallowReadonly的结构和 readonly 的一致,就不贴截图了。


9

获取原型:toRaw

toRaw 可以获取 Vue 建立的代理的原型对象,但是不能获取我们自己定义的Proxy的实例的原型。
toRaw大多是在Vue内部使用,目前只发现在向indexedDB里面写入数据的时候,需要先用 toRaw 取原型,否则会报错。
// 获取reactive、shallowReactive、readonly、shallowReadonly的原型
console.log('深层响应的原型', toRaw(objectReactive))
console.log('浅层响应的原型', toRaw(objectShallowReactive))
console.log('深层只读的原型', toRaw(objectReadonly))
console.log('浅层只读的原型', toRaw(objectShallowReadonly))
运行结果都是普通的object,就不贴截图了。


10

类型判断

Vue提供了三个用于判断类型的函数:
* isProxy:判断对象是否是Vue建立的Proxy代理,包含reactive、readonly、shallowReactive和shallowReadonly创建的代理,但是不会判断自己写的Proxy代理。
  • isReactive:判断是否是reactive创建的代理。如果readonly的原型是reactive,那么也会返回true。
* isReadonly:判断是否是readonly、shallowReadonly创建的代理。这个最简单,只看代理不看target。
我们用这三个函数判断一下我们上面定义的这些Proxy代理,看看结果如何。
我们写点代码对比一下:
    const myProxyObject = myProxy({title:'222', __v_isReactive: false})
    console.log('myProxyObject', myProxyObject)
    const myProxyReactive = myProxy(objectReactive)
    console.log('myProxyReactive', myProxyReactive)

    // 试一试 __v_isReadonly
    console.log('objectReactive', objectReactive)
    console.log('__v_isReadonly'
      , objectReactive.__v_isReadonly
      , objectReactive.__v_isReactive
      )

    return {
      obj: { // js对象
        check1: isProxy(person),
        check2: isReactive(person),
        check3: isReadonly(person)
      },
      myproxy: { // 自己定义的Proxy object
        check1: isProxy(myProxyObject),
        check2: isReactive(myProxyObject),
        check3: isReadonly(myProxyObject)
      },
      myproxyReactive: { // 自己定义的Proxy reactive
        check1: isProxy(myProxyReactive),
        check2: isReactive(myProxyReactive),
        check3: isReadonly(myProxyReactive)
      },
      // 深层响应  reactive(object)
      reto: { // reactive(object)
        check1: isProxy(objectReactive),
        check2: isReactive(objectReactive),
        check3: isReadonly(objectReactive)
      },
      // 浅层响应 参数:object
      shallowRetObj: {
        check1: isProxy(objectShallowReactive),
        check2: isReactive(objectShallowReactive),
        check3: isReadonly(objectShallowReactive)
      },
      // 浅层响应 参数:reactive
      shallowRetRet: {
        check1: isProxy(objectShallowReactive),
        check2: isReactive(objectShallowReactive),
        check3: isReadonly(objectShallowReactive)
      },

      // 深层只读,参数 object =======================
      readObj: { // readonly object
        check1: isProxy(objectReadonly),
        check2: isReactive(objectReadonly),
        check3: isReadonly(objectReadonly)
      },
      // 深层只读,参数 reactive
      readRet: { // readonly reactive
        check1: isProxy(reactiveReadonly),
        check2: isReactive(reactiveReadonly),
        check3: isReadonly(reactiveReadonly)
      },
      // 浅层只读 参数:object
      shallowReadObj: {
        check1: isProxy(objectShallowReadonly),
        check2: isReactive(objectShallowReadonly),
        check3: isReadonly(objectShallowReadonly)
      },
      // 浅层只读 参数:reactive
      shallowReadRet: {
        check1: isProxy(reactiveShallowReadonly),
        check2: isReactive(reactiveShallowReadonly),
        check3: isReadonly(reactiveShallowReadonly)
      },
      person
    }
对比结果:
验证类型的对比测试

总结一下:

  • isReadonly 最简单,只有readonly、shallowReadonly建立的代理才会返回 true,其他的都是 false。

  • isProxy也比较简单,Vue建立的代理才会返回true,如果是自己定义的Proxy,要看原型是谁,如果原型是 reactive(包括其他三个)的话,也会返回true。

  • isReactive就有点复杂,reactive 建立的代理会返回 true,其他的代理(包含自己写的)还要看一下原型,如果是 reactive 的话,也会返回true。



11

判断依据

那么这三个函数是依据什么判断的呢?自己做的 Proxy 无意中监控到了“__v_isReactive”,难道是隐藏属性?测试了一下,果然是这样。

myProxy({title:'测试隐藏属性', __v_isReactive: true}),这样定义一个实例,也会返回true。



12

reactive直接赋值的方法

使用的时候我们会发现一个问题,如果直接给 reactive 的实例赋值的话,就会“失去”响应性,这个并不是因为 reactive 失效了,而是因为 setup 只会运行一次,return也只有一次给模板提供数据(地址)的机会,模板只能得到一开始提供的 reactive 的地址,如果后续直接对 reactive 的实例赋值操作,会覆盖原有的地址,产生一个新的Proxy代理地址,然而模板并不会得到这个新地址,还在使用“旧”地址,因为无法获知新地址的存在,所以模板不会有变化。
那么就不能直接赋值了吗?其实还是有方法的,只需要保证地址不会发生变化即可。
## 对象的整体赋值的方法。
有请 ES6 的 Object.assign 登场,这个方法是用来合并两个或者多个对象的属性的,如果属性名称相同后面的属性会覆盖前面的属性。所以大家在使用的时候要谨慎使用,确保两个对象的属性就兼容的,不会冲突。
代码如下:
Object.assign(objectReactive, {name: '合并', age: 20, newProp: '新属性'})


13

数组的整体赋值的方法

数组就方便多了,可以先清空再 push 的方式,代码如下:

// retArray.length = 0 // 这里清空的话,容易照成闪烁,所以不要急
setTimeout(() => {
  const newArray = [
    { name: '11', age: 18 },
    { name: '22', age: 18 }
  ]
  // 等到这里再清空,就不闪烁了。
  retArray.length = 0
  retArray.push(...newArray)
}, 1000)


14

var 和 let、const

ES6 新增了 let 和 const,那么我们应该如何选择呢?简单的说,var不必继续使用了。
let 和 const 的最大区别就是,前者是定义“变量”的,后者是定义“常量”的。
可能你会觉得奇怪,上面的代码都是用const定义的,但是后续代码都是各种改呀,怎么就常量了?其实const判断的是,地址是否改变,只要地址不变就可以。
对于基础类型,值变了地址就变了;而对于引用类型来说,改属性值的话,对象地址是不会发生变化的。
而 const 的这个特点正好可以用于保护 reactive 的实例。由Vue的机制决定,reactive的实例的地址是不可以改变的,变了的话模板就不会自动更新,const可以确保地址不变,变了会报错(开发阶段需要eslint支持)。
于是const和reactive(包括 ref 等)就成了绝配。

源码:

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

在线演示:

GitHub:https://naturefwvue.github.io/nf-vue-cnd/cnd/project-compositionapi/
gitee:https://naturefw.gitee.io/nf-vue-cdn/cnd/project-compositionapi/

本文作者:自然框架

个人网址:jyk.cnblogs.com

声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。

写的不错?赞赏一下

长按扫码赞赏我


●  你所在的行业,工作多少年,工资能赶上程序员?

●  脚本之家粉丝福利,请查看!

●  好用到爆!80%的人竟然不知道?

● 卧槽!电脑装了它,流畅到起飞

● 吹爆这款APP!毫秒启动!无广告,重点还免费?!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存