初探Vue3.0--响应式api理解~

1,050 阅读13分钟

掘金的第一篇文章献给vue3.0,

vue3.0的正式版发布有一定的时间了,忙于其他,虽然好奇但没有进行比较和学习,终于不负自己“期望”,战胜了“懒惰”,学习相关熟悉的技术新版本,其实入手还是比较简单的(坐等打脸),本文主要是笔者自己在学习过程中的探索;

组合api(Composition API)

最大的感受,组合api是将具有独立功能的函数封装组合在一起,提高复用性; 后来实际封装了相关功能,变量较多的情况下,设计高内聚的功能是非常必要的

demo 学生和教师显示

分别用vue2.x以及vue3.x进行了学生列表和老师列表的展示,其代码功能分部图如下图所示

vue2.x 学生和老师展示代码功能图 vue 2.x

vue 3.x! 学生和老师展示代码功能图 其中不同颜色表示页面中两个功能的代码分布;

  • 对于vue2.x而言,主要分为三个部分
    • data中的数据定义
    • methods中的方法定义
    • 视图使用
  • 对于Vue3.x而言
    • 学生、老师中的变量和功能定义
    • setup中引用功能
    • 视图使用 根据颜色而言,其实vue2.x的功能相对而言是比较分散的,而vue3.0中的功能和数据定义可以定义在一个函数中,功能比较集中;因此在进行组合性api的是时候是需要思考如何设计是比较合理的;vue3.x在功能上能够实现高度的一个复用,想到vue.mixin的方法,实现代码的复用;

响应式数据定义

响应式数据是实现mvvm非常重要的一个部分,在vue3.x中,在支持data方式定义响应式的数据外,还提供很多可自定义的api;

在响应式的数据中值得注意的部分 深层次响应还是浅层次响应;

reactive

定义响应式数据的一种方式,通过 Proxy包装传入的变量,借助Proxy提供的set和get方法,实现响应式数据的逻辑

  • vue3中提供实现响应式数据的方法
  • vue2中响应式数据是通过Objec.defineProperty\color{red}{Objec.defineProperty} 来实现的,而在vue3中数据通过es6的Proxy\color{red}{Proxy}来实现的,

reactive使用

  setup(props) {
     let state =reactive({
        msg:{
          name:'mafe',
          age:24, 
        },
        time: new Date(), //只能进行 赋值更新
        list:[
          {name:'mfy'},
          {name:'mfy1'},
          {name:'mfy2'},
        ]
     })
     function changeName(){
       //定义的是对象能够直接修改赋值
       state.msg.name = '333'
        //无法更改state中time的赋值对象 
       // state.time.setDate(state.time.getDate()+1)
       let newDate =new Date(state.time.getTime())
       newDate.setDate(state.time.getDate()+1) 
       state.time = newDate ;//不是对象类型 只能修改数据内容
     }
     return  {
       state,
       changeName
     }
  }

reactive定义数据

reactive 使用注意事项

  • reactive定义的基础类型数据
    • 使用只能通过state.time = XXX 赋值的方式进行更改;
    • 获得到的只能是当前的值,比如time初始使用new Date() 进行赋值,得到的time的原型并非是Date的,再次修改time的值的时候不能通过setDate赋值;
  • reactive定义复杂数据类型
    • 对象的每一层都会被包装成一个Proxy的代理,用于页面的响应式数据构建;
    • 更改对象中的某一个属性的值都会因此视图的响应;

ref

也是定义响应式数据的一种,通常用来定义基础类型数据,以及获取dom元素的内容 reactive 通常用于定义对象,在我们实际的使用过程中是非常不便捷的,我们仅仅是想要某个变量实现响应式,ref是对简单的值进行监听;

ref使用

ref的本质还是reactive,系统会自动根据我们传入的值转换成ref(xxx)->reactive({value:xx})

  setup(props) {
    // 传递一个值 
     let age = ref(33);
     function changeAge(){ 
       age.value +=1;
     }
     return{
       age,
       changeAge
     }
  }

ref 定义的内容:

ref的注意点

  • 在Vue中使用ref的值不用通过value获取;
  • 在js中使用ref的值必须要用value获取;
  • 在setup 中定义的变量是无法在method中获取的;

triggerRef 更新变量

ref定义对象类型时候更新问题 ref定义的对象类型,如果是只想更改某一层的变化 需要调用triggerRef进行触发更新

  // 只想更改ref中某一层的变化
  msg.value.a = 'myy'
  //增加triggerRef 引起视图的更新 
  // Vue3 只提供了triggerRef 没有提供reactRef相关的
  triggerRef(msg)

unRef返回其原始值

如果参数为 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val

let age = ref(33);
console.log(age)
console.log(unref(age))

ref 和reactive的扩展讲解

ref 和reactive的区别

  • 在template中使用ref类型的数据,vue会自动帮我们计算.value
  • 在template中使用reactive类型的数据,vue不会自动帮我们计算.value

vue 是如何决定是否自动添加.value

Vue在解析数据之前,会自动判断这个数据是否是ref类型的,如果是,则自动添加上.value,如果不是,则不会自动添加

vue 是如何判断当前数据是否是ref类型的

  • 通过当前按数据的__v_isRef来进行判断的,
  • 如果有私有的属性,并且取值为true,那么就代表是一个ref类型的数据
 console.log("判断是否是ref类型数据",isRef(age))
 console.log("判断是否是isReactive类型数据",isReactive(age))

监听层级问题

无论是ref还是reactive的都是递归监听的过程,也就是无论我们嵌套了多少层级,其每一层的每个数据都会发生监听;

递归监听

reactive

let state= reactive({
      a:'2',
      gf:{
        b:'b',
        f:'c',
        c:{
          a:3
        }
      }
    })
  // 多个层级 每个层级内容改变的时候,都会改变;
    function changeValue(){
      console.log(state)
      console.log(state.gf)
    }

打印后可以发现在reactive中定义的对象,发现对象中的每一层对象都是通过Proxy进行代理

ref

let msg = ref({
       a:'2',
        gf:{
          b:'b',
          f:'c',
            c:{
            a:3
          }
        }
    }) 
 function changeRef(){
    //直接更改ref 
    console.log(msg.value)
    console.log(msg.value.gf) 
 }

非递归监听

只监听对象的第一层,

  • 只有第一层被包装,第二层未被包装;
  • 第一层的数据发生改变,第一层的ui也会变化
  • 第二层的数据修改,但是不会更新ui
  • shallowRef Vue监听的是.value的变化,并不是第一层的变化 利用shallowRef 和shallowReactive能够改变当前对象的监听层级

shallowReactive

let state = shallowReactive({
      a:'2',
      gf:{
        b:'b',
        f:'c',
        c:{
          a:3
        }
      }
    })
  function changeValue(){
      console.log(state)
      console.log(state.gf)
    }

第二层的对象不会包装成响应式内容

shallowRef

 let msg = shallowRef({
       a:'2',
        gf:{
          b:'b',
          f:'c',
            c:{
            a:3
          }
        }
    })
function changeRef(){ 
      console.log(msg)
      console.log(msg.value)
      console.log(msg.value.gf) 
    }

注意内容:

  • 通过ref定义的内容会被reactive包装成一个.value的对象,因此这个对象的第一层的就是value
  • 当访问msg.value的时候,其实已经访问它的值了,自然是没有内容的;

readonly

只读属性的修改,使用readonly防止更改响应式对象

存在一组对象,我们只能读取,但是不能更改,因此可以使用readonly进行包裹;

  • 对于对象深度包裹,递归到最底层的变量
let state = readonly({name:'mfy',age:34,attr:{weight:45,height:1.88}})
  function changeReadonly(){
      state.age = 2; //修改第一层 
      state.attr.weight = 2; //修改深层
      console.log(state)
      console.log(isReadonly(state)) 
    }

点击进行修改时候

readonly定义的对象和reactive数据结构的区别

  const obj = reactive({name:'33'})
  console.log(obj)
  let state = readonly({name:'mfy',age:34,attr:{weight:45,height:1.88}})
  console.log(state)

  • reactive定义的参数在Proxy函数的set支持传入四个参数
  • readonly只能收到两个参数,在set中直接进行警告提示

shallowReadonly

shallowRef以及shallowReactive的性质一样,只能控制第一层的数据是不可修改的;

let state = shallowReadonly({name:'mfy',age:34,attr:{weight:45,height:1.88}})
console.log(state)
 function changeReadonly(){
   state.age = 2; //修改第一层
   state.attr.weight = 2; //修改第二层
   console.log(state) 
   console.log(state.attr)
}

  • 第二层数据的对象发生变化
  • 视图无法进行更新,因为第二层的数据不是响应式的
  • shllowReadonly 非递归包装,只包装第一层数据,第二层将不会进行包装

不得不比较const

const也是定义的变量不能在修改,但是对于对象除外;

  • const 和 readonly的区别
  • const 赋值保护 不能给变量重新赋值
  • readonly 属性保护,不能给属性重新赋值

toRef

在实际的使用中,我们可能不仅仅是进行一整个属性进行监听,有可能是单个的属性进行监听,因此toRef等给我们提供了固定属性进行监听;

let obj1 = {age:34,name:'mfy'} 
  let state2 = toRef(obj1,'name')
 
  function changeRef(){
     state2.value = 333
     console.log('obj1',obj1)
     console.log('state2',state2) 
   }

toRef修改的值不仅仅使定义的值发生了改变,其原始值也发生了改变;

  • 利用toRef将某一个对象的属性变成响应式的,此时修改是会影响到原始值
  • 如果响应数据是通过toRef变化的,是不会触发页面更新的

ref和toRef

  • ref
    • 复制,修改响应式数据不会影响到以前的数据
    • 数据发生改变,ui会自动更新
  • toRef
    • 引用 修改响应式数据会影响到以前怼数据
    • 数据发生改变,ui不会自动更新

toRefs

将响应式对象转化为普通对象,其中结果对象的每个 property 都是指向原始对象相应 propertyref

定义多个属性的响应式

let state3 = toRefs(obj1,'name','age')   

转化reactive定义的响应式

let obj = {age:34,name:'mfy'} 
 let state =reactive(obj);
 console.log(state) 
 let state2 = toRefs(state)
 console.log(state2) 

打印出来statestate2

state2中的引用和原始数据类型以及通过reactive定义的类型都是属于同一引用关系;即修改objstatestate2中的任何一个元素属性值,都会发生改变;

  let obj = {age:34,name:'mfy'} 
   let state =reactive(obj);
   console.log(state) 
   let state2 = toRefs(state)
   console.log(state2) 
  
   function changeRef(){  
     //修改值 
     obj.age = 232323;
     console.log('state2',state2)
     console.log('state',state)
     console.log('obj',obj)

   }

使用场景

使用reactive定义的对象是不具有响应式的,因此在使用的时候无法进行结构出来使用,而toRefs可以定义每个属性都是响应式的,

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  }) 
  // 返回时转换为ref
  return toRefs(state)
}
export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    const { foo, bar } = useFeatureX() 
    return {
      foo,
      bar
    }
  }
}

总结内容

找到原始数据

被定义的响应式数据,如何找到原始数据呢?

toRaw

reactive或者ref中得到原始数据,主要是做一些不想被监听的事情(提升性能),每次修改都会被追踪,都会被更新UI界面,但是是非常消耗性能的,所以有一些操作我们不需要追踪,不需要更新ui界面,此时可以通过 toRaw方法拿到原始数据,对原始数据进行修改,这样不会被追踪,不会更新ui界面

追踪reactive的特性进行定义响应式

    //默认情况下不是一个响应式数据
   let obj = {age:34,name:'mfy'}
   let state  =reactive(obj); 
   //把创建时候的参数分解出来
   let obj2 = toRaw (state); // 和obj是一个样子
   function changeRef(){ 
     //引用修改的内容,页面不会自动更新 只有通过修改包装的方法才能更新  
      obj2.name = 'myy'  
      //state和obj是引用关系,其中的一个值更改时候,另外一个值也会更改
      console.log(state)
      console.log(obj) 
      console.log(obj2) 
   }

  • toRaw获取到的变量,是一个引用的关系
  • 修改每一个值的是时候,都会影响到所依赖的值

追踪ref定义的数据

对于ref定义的值,必须要准确的告诉toRaw.value 如果通过toRaw 获取ref的数据,必须明确告诉ref.value的值 经过Vue处理之后,.value中保存的是当初创建时传入的那个原始数据;

  let obj = {age:34,name:'mfy'} 
   let state  =ref(obj); 
   //把创建时候的参数分解出来
   let obj2 = toRaw(state.value); // 必须指定定义的ref内部变量的value
   function changeRef(){ 
      obj2.name = 'myy'   
      console.log('state',state)
      console.log('obj',obj) 
      console.log('obj2',obj2) 
   }

markRaw

markRaw 永远不想被追踪

  let obj = {age:34,name:'mfy'}
   //告诉这个对象 永远不能被监听
   obj = markRaw(obj) 
   let state =reactive(obj);  
   function changeRef(){
     state.name ='mfy';//监听到这个的变化
     console.log(state)
   }

通过markRaw定义的属性再次被定义成响应式的时候,无论怎样对state进行赋值,是无法更改其内容的;

Composition API 的入口 (setup)

setupvue3.x中加入的一个新的生命周期,也是组合api的入口,由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有this。这意味着,除了props 之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。

执行时间

setup ->beforeCreate -> created

  • 由于setup在执行的时候是在beforeCreate之前,因此是无法进行获取到this
  • setup中只能执行同步的,不能是异步的
 setup(props) { 
    //定义数据
    let age = ref(33) 
    
     //定义函数
    function changeAge(){
      age.value = 333
    }
    // 必须将变量暴露出去才能进行使用
    return{
      age,
      changeAge
    } 
  }

setup中生命周期

在setup的函数中,其内部提供了很多的可进行兼容的方法

setup响应式数据监控方法

computed

用于使用计算属性,可以直接获取到当前拼接出来的值

let firstName = ref("马");
let lastName = ref("fyy") 
let fullName = computed(()=> firstName.value + lastName.value)
console.log(fullName)  

若想要直接修改computed的变量,可以使用set的方法,将依赖的属性的值进行修改;

 let age = ref(12)
 let ageCom = computed({
      get: () => age.value + 1,
      set: val => {
        age.value = val - 1
      }
 })

watchEffect

在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。

let firstName = ref("马");
let lastName = ref("fyy") 
let fullName = computed(()=> firstName.value + lastName.value) 
watchEffect(() => { 
  console.log('%c 内容发生改变了','color:yellow',fullName.value)
})
function changeName(){
  firstName.value = 'Ming'+Math.random()   
}


每次watchEffect里面的内容发生改变的时候都会触发;

watch

watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch 需要侦听特定的 data 源,并在单独的回调函数中副作用。默认情况下,它也是惰性的——即,回调是仅在侦听源发生更改时调用。 与 watchEffect 比较,watch 允许我们:

  • 惰性地执行副作用;
  • 更具体地说明应触发侦听器重新运行的状态;
  • 访问侦听状态的先前值和当前值。

侦听一个数据源

const state = reactive({ count: 0 }) 
 watch(
   ()=>state.count,
   (count,preCount)=>{
     console.log('%c count的值发生修改了','color:yellow','count='+count,' preCount='+preCount)
   }
 ) 
function changeCount(){
  state.count +=1  
}

侦听多个数据源

 let name = ref('myy')
     let age = ref(22)
     watch([name,age],([name,age],[prevName,prevAge])=>{
       console.log("%c --姓名和年龄更改--",'color:yellow')
       console.log("%c 姓名更改",'color:green','prevName='+prevName,' name='+name )
       console.log("%c 年龄更改",'color:green','prevAge='+prevAge,' age='+age )
     })
     function changeCount(){
       name.value = 'myyyyy'
       age.value +=1  
     }

setup 中定义的参数

基础类型 (最准确的来说 是使用ref定义的基础类型)

  • setup中定义的参数,只能在setup的作用域下进行修改
  • 在外部的method中进行修改,则会报错
  • 在setup暴露出去后,其暴露的就是当前变量的值;
changeValue(){ 
      console.log(this.isShowAddModal) //false
      this.isShowAddModal.value =3
    }

复杂数据类型(使用reactive)

在外部的method中进行修改数据 能够修改成功

 changeValue(){ 
    console.log(this.heorData)
    this.heorData.list=[] 
  }
//setup内部的监听函数是否会触发
  watch(heorData,()=>{
    console.log("watch:: heroData--数据修改了",heorData)
  }) 

调用setup内部暴露的函数,在setup内部是可以被检测到的

  • ref定义的参数,无法在setup外部进行参数的修改
  • reactive定义的参数能够进行修改,并且能够被watch监听到

总结

自己通过文档+视频+实际操作对Vue3.x有了一点点了解,有些内容其实不是不够深入的,比较好奇的点会通过自己想要了解的内容去实际操作一波;

  • Composition API 的使用
  • 响应式数据的定义
    • 引用类型、基础类型定义
    • 递归响应和非递归响应数据的定义
    • 只读响应数据
  • setup函数的使用

参考文档

官方文档api b站vue3.0入门讲解视频