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

简介: Vue3的 reactive 怎么用,原理是什么,官网上和reactive相关的那些函数又都是做什么用处的?这里会一一解答。

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


image.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)  // 这里有变化,最后一个参数改成 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) 
复制代码


运行结果:


image.png


  • Handler:可以看到 Vue 除重写 set 和 get 外,还重写了deleteProperty、has和ownKeys。


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


属性的结构:


image.png


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


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


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


如果想在模板里面直接使用{{name}}并且要具有响应性,这时才需要使用toRef。


浅层响应式代理: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) 
复制代码


image.png


shallowReactive 也是用 Proxy 实现响应性的,而单独使用contacts属性并没有响应性,因为 shallowReactive 是浅层代理,所以不会让嵌套对象获得响应性。


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


单独使用的属性的形式:


image.png


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


做一个不允许响应的标记: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) // 可以变成响应性代理
复制代码


运行结果:


image.png


做标记后的js对象作为参数,不会变成响应式,但是使用属性值作为参数,还是可以变成响应式。


那么哪些地方可以用到呢?我们可以在给组件设置(引用类型的)属性的时候使用,默认情况下组件的属性都是自带响应性的,但是如果父组件里设置给子组件的属性值永远不会发生变化,那么还变成响应式的话,就有点浪费性能的嫌疑了。


如果想节约一下的话,可以在父组件设置属性的时候加上markRaw标记。


深层只读响应式代理: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) // 属性是简单类型的,所以失去响应性
复制代码


运行结果:


image.png


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


  • Target,指向object。


运行结果:


image.png


  • Handler,这部分是一样的。


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


浅层只读响应代理:shallowReadonly



和readonly相对应,shallowReadonly是浅层的只读响应代理,和readonly的使用方式一样,只是不会限制嵌套对象只读。


// object 的浅层只读代理
const objectShallowReadonly = shallowReadonly(person)
// reactive 的浅层只读代理
const reactiveShallowReadonly = shallowReadonly(objectReactive)
复制代码


shallowReadonly的结构和 readonly 的一致,就不贴截图了。


获取原型: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,就不贴截图了。


类型判断



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
    }
复制代码


对比结果:


image.png


总结一下:


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


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


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


判断依据


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


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


reactive直接赋值的方法



使用的时候我们会发现一个问题,如果直接给 reactive 的实例赋值的话,就会“失去”响应性,这个并不是因为 reactive 失效了,而是因为 setup 只会运行一次,return也只有一次给模板提供数据(地址)的机会,模板只能得到一开始提供的 reactive 的地址,如果后续直接对 reactive 的实例赋值操作,会覆盖原有的地址,产生一个新的Proxy代理地址,然而模板并不会得到这个新地址,还在使用“旧”地址,因为无法获知新地址的存在,所以模板不会有变化。


那么就不能直接赋值了吗?其实还是有方法的,只需要保证地址不会发生变化即可。


## 对象的整体赋值的方法。


有请 ES6 的 Object.assign 登场,这个方法是用来合并两个或者多个对象的属性的,如果属性名称相同后面的属性会覆盖前面的属性。所以大家在使用的时候要谨慎使用,确保两个对象的属性就兼容的,不会冲突。


代码如下:


Object.assign(objectReactive, {name: '合并', age: 20, newProp: '新属性'})
复制代码


数组的整体赋值的方法。


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


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


var 和 let、const



ES6 新增了 let 和 const,那么我们应该如何选择呢? 简单的说,var不必继续使用了。


let 和 const 的最大区别就是,前者是定义“变量”的,后者是定义“常量”的。


可能你会觉得奇怪,上面的代码都是用const定义的,但是后续代码都是各种改呀,怎么就常量了?其实const判断的是,地址是否改变,只要地址不变就可以。


对于基础类型,值变了地址就变了;而对于引用类型来说,改属性值的话,对象地址是不会发生变化的。


而 const 的这个特点整合可以用于保护 reactive 的实例。由Vue的机制决定,reactive的实例的地址是不可以改变的,变了的话模板就不会自动更新,const可以确保地址不变,变了会报错(开发阶段需要eslint支持)。


于是const和reactive(包括 ref 等)就成了绝配。


源码:



gitee.com/naturefw/nf…


在线演示:



naturefw.gitee.io/nf-vue-cdn/…



相关文章
|
4天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
16 7
|
5天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
18 3
|
4天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
19 1
|
4天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
21 1
|
6天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
8天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
8天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
27 9
|
7天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
6天前
|
JavaScript
如何在 Vue 中使用具名插槽
【10月更文挑战第25天】通过使用具名插槽,你可以更好地组织和定制组件的模板结构,使组件更具灵活性和可复用性。同时,具名插槽也有助于提高代码的可读性和可维护性。
13 2
|
6天前
|
JavaScript
Vue 中的插槽
【10月更文挑战第25天】插槽的使用可以大大提高组件的复用性和灵活性,使你能够根据具体需求在组件中插入不同的内容,同时保持组件的结构和样式的一致性。
11 2