前文:

①Vue3 使用Vite或@vue/cli 创建项目

②Vue3 性能比Vue2好的原因(diff算法优化、静态提升、事件侦听器缓存)



写下博客主要用来分享知识内容,并便于自我复习和总结。部分图片节选自相关视频。
如有错误之处,请各位大佬指出。


首先,文章的主要内容参考自相关视频,如果各位有需要,可直接前往视频观看:

Vue3快速上手指南-CompositionAPI

Vue3.x+TypeScript 从入门到实战

PS:文中内容和相关用法,全部基于 vue 3.0.0-beta.1 版本。且文中的结论,都通过视频、官方文档、输出结果的测试证明,尽可能保证了正确性。如有错误或相关疑问也可随时提出。如果后续版本变动,导致相关用法略有出入,为正常现象。后续文章也会进一步调整。
在这里插入图片描述


Composition API介绍

不知道各位在之前使用 Vue 开发的时候,是否感觉到在代码量很少的时候,逻辑结构还是蛮清晰的。但是随着代码量越来越大,即使我们把相关功能抽出来变成一个个的功能组件,但是功能组件也是越来越多,整体内容全部放在其中也是会相当臃肿,而且必不可少的还会使用到各种形式的组件间通信。这就导致了,每个功能模块的代码会散落分布在各个位置,让整个项目的内容难以阅读和维护(尤其是团队开发的时候,各种各样的命名和组件间通信,让人摸不着头脑)。

比如最熟知的 Vue 结构是这样的:
在这里插入图片描述
这也导致了 Vue 帮我们设计了很多方式去维护代码,比如 Vuex、插槽 slot、混入 mixins、事件总线 EventBus 等等,从而让我们尽可能的简化代码,实现复用。

而到了 Vue3,这种现象终于改变了。Vue3 的思路就是根据逻辑功能,对代码进行组织划分,把同一个功能的相关代码全都放在一起,或者把它们单独拿出来放在一个函数中,从而解决上述代码臃肿的问题。基于此,Composition API 又被称为基于函数组合的 API。
在这里插入图片描述
为什么要使用这种方式?

1、首先 Composition API 是根据逻辑相关性组织代码的,这样可以提高代码的可读性和可维护性。

2、这种方式可以更好地重用逻辑代码。比如,在 Vue2 中如果想重用逻辑代码,可能会发生命名冲突,以及关系不清。
在这里插入图片描述
同时需要说明的是,在 Vue3 中,Composition API 是可选的,并不是一定要使用这种新方式,也就是说我们依然可以使用以前的结构和用法。


setup函数

接下来就是 Vue3 的使用。其中主要就是学习使用 setup 函数,它是使用 Composition API 的入口。

注意点:
setup 函数是 Vue3 中新增的函数,它是我们在编写组件时,使用 Composition API 的入口。同时它也是 Vue3 中新增的一个生命周期函数,会在 beforeCreate 之前调用。因为此时组件的 data 和 methods 还没有初始化,因此在 setup 中是不能使用 this 的。所以 Vue 为了避免我们错误的使用,它直接将 setup 函数中的 this 修改成了 undefined。并且,我们只能同步使用 setup 函数,不能用 async 将其设为异步。


基本使用方法


setup 中创建数据(ref)

首先,给一个 setup 函数的最基本使用简例:

<template>
  <div id="app">
    <p>{{ name }}</p>
  </div>
</template>

<script>
  // 不要忘记import
  import { ref } from 'vue'
  export default {
    setup(){
      const name = ref('王路飞')
      return { name }
    }
  }
</script>

这里 ref 函数的作用就是创建并返回一个响应式引用。

此时需要注意的是:这个name并不会返回一个字符串类型的值,而是一个响应式对象。再通过 return 返回的对象中的属性,就可以在模板中使用了。

(您也许会好奇这个名词。响应式先不管,这看起来也不像是个对象啊?别着急,慢慢往下看,在下面的 ref 的注意点中,您将会找到这个问题的答案)


setup 中的 methods 方法

如果我们想要在 setup 中使用原本的 methods 用法,我们需要做如下操作:

<template>
  <div id="app">
    <p>{{ name }}</p>
    <p>{{ age }}</p>
    <button @click="addOne">加一</button>
  </div>
</template>
<script>
  import { ref } from 'vue'
  export default {
    setup(){
      const name = ref('王路飞')
      const age = ref(17)
      function addOne(){
        age.value++
      }
      return {name, age, addOne}
    }
  }
</script>

在这里需要注意:前面已经提到了,在 setup 里是无法使用 this 的,并且 age 也不会是一个 number 类型数据,而是一个响应式对象,所以很显然我们无法直接改变它的值(age++)。那么修改方法就是使用 value。同理,如果我们想修改 name 的值,也需要使用 value 的方式。

(而为什么用了 value 就可以对其进行修改,这个问题的答案也在下面的 ref 的注意点中)


setup 中的 computed 方法

如果我们想要在 setup 中使用原本的 computed 用法,我们需要做如下操作:

<template>
  <div id="app">
    <p>姓名:{{ name }}</p>
    <p>年龄:
      <button @click="changeAge(-1)">-</button>
      {{ age }}
      <button @click="changeAge(1)">+</button>
    </p>
    <p>出生年份:{{year}}</p>
  </div>
</template>

<script>
  // 不要忘记import
  import { ref, computed } from 'vue'
  export default {
    setup(){
      const name = ref('王路飞')
      const age = ref(17)
      const year = computed(() => {
        return 2020 - age.value
      })
      function changeAge(val){
        age.value += val
      }
      return {name, age, changeAge, year}
    }
  }
</script>

该用法和以前还是很像的。需要注意的是,显然我们想要改变 year 的返回值,需要去调整 age 的值。如果我们想直接修改 year 的值,肯定无法通过 year.value 的方式获取。在这里,我们可以像以前一样,去使用 getter 和 setter。

<template>
  <div id="app">
    <p>姓名:{{ name }}</p>
    <p>年龄:
      <button @click="changeAge(-1)">-</button>
      {{ age }}
      <button @click="changeAge(1)">+</button>
    </p>
    <p>出生年份:
      <button @click="changeYear(-1)">-</button>
      {{ year }}
      <button @click="changeYear(1)">+</button>
    </p>
  </div>
</template>

<script>
  import { ref, computed } from 'vue'
  export default {
    setup(){
      const name = ref('王路飞')
      const age = ref(17)
      const year = computed({
        get: () => {
          return 2020 - age.value
        },
        set: val => {
          age.value = 2020 - val
        }
      })
      function changeAge(val){
        age.value += val
      }
      function changeYear(val){
        year.value += val
      }
      return {name, age, changeAge, year, changeYear}
    }
  }
</script>

setup 中创建数据(reactive)

现在通过以上的内容我们可以知道,如果想要使用数据,需要使用 ref,获取值需要使用 .value 的方式,而且这些响应式对象还需要通过 return 返回。虽然现在相比以前确实简化了不少,也可以将相关的内容全部放在一起管理,但是到目前为止的操作看起来还是比较繁琐的,不知道各位怎么看。

先不说这样写各位觉得累不累,但至少如果响应式对象很多,那么 return 里需要返回很多的内容。除此以外,从目前来看,ref 函数只能监听简单类型的变化,不能监听复杂类型的变化,比如对象和数组。

在这里插入图片描述
综上所述,Vue3 确实提供了另一种定义响应式对象的方式就是:允许我们定义一个响应式对象,然后把我们想使用的值都放在对象内,当作对象属性。

如果我们想这么用,我们需要先导入一个函数:reactive。它的作用就是创建并返回一个响应式对象。

经过筛减,我们的代码可以改变成这样:

<template>
  <div id="app">
    <p>姓名:{{ data.name }}</p>
    <p>年龄:
      <button @click="changeAge(-1)">-</button>
      {{ data.age }}
      <button @click="changeAge(1)">+</button>
    </p>
    <p>出生年份:
      <button @click="changeYear(-1)">-</button>
      {{ data.year }}
      <button @click="changeYear(1)">+</button>
    </p>
  </div>
</template>

<script>
  import { reactive, computed } from 'vue'
  export default {
    setup(){
      const data = reactive({
        name: '王路飞',
        age: 17,
        year: computed({
          get: () => {
            return 2020 - data.age;
          },
          set: val => {
            data.age = 2020 - val;
          }
        })
      });
      function changeAge(val){
        data.age += val;
      }
      function changeYear(val){
        data.year += val;
      }
      return { data, changeAge, changeYear };
    }
  }
</script>

需要注意的是,之前使用 ref 的时候,使用的是 age.value,而且返回的是一个响应式对象。而目前使用 reactive 后,就有点像是一个普通对象了。我们就可以像之前使用对象那样,对其进行使用 data.age。(不需要使用 .value 了)

这部分一定要注意观察区别。也就是说,用这种方式,除了 methods 方法(function),现在所有的内容都可以放在 data 里了,方便我们的查阅,简化了 return 的数量。

不要忘记在 template 里,用对象的方式去调用:data.age。


reactive 的注意点

reactive 参数必须是对象(json / arr),否则无法实现响应式。

setup(){
    let state = reactive(123);
    function myFn(){
      state = 666;  //由于在创建响应式数据的时候传递的不是一个对象,所以无法实现响应式
      console.log(state); //输出666,但是页面无变化
    }
    return { state, myFn };
}

setup(){
    let state = reactive({
      age: 17
    });
    function myFn(){
      state.age = 666;
      console.log(state); //输出666,页面变化
    }
    return { state, myFn };
}

setup(){
    let state = reactive([1,3,5]);
    function myFn(){
      state[0] = 100;
      console.log(state); // 页面变化
    }
    return { state, myFn };
}

除此以外,在 console.log(state) 后,我们可以发现 Proxy。那也就证明了,Vue3 中的响应式数据是通过 ES6 的 Proxy 来实现的。
在这里插入图片描述
需要注意的是:如果给 reactive 传递了其他对象,默认情况下修改对象,界面不会自动更新。如果想更新,需要通过重新赋值的方式。

setup(){
    let state = reactive({
      time: new Date()
    });
    function myFn(){
      // 直接修改,页面不会更新:
      state.time.setDate(state.time.getDate() + 1 );
      console.log(state.time); // 日期变更,页面无变化

      // 重新赋值
      const newTime = new Date(state.time.getTime());
      newTime.setDate(state.time.getDate() + 1);
      state.time = newTime;
      console.log(state.time); // 日期变更,页面更新
    }
    return { state, myFn };
}

ref 的注意点

虽然刚才提到 ref 用起来比较繁琐,但 reactive 也存在一些问题。

由于刚才证明了 reactive 必须传递一个对象,所以这就导致了在企业开发中,如果我们只想让某个变量实现响应式时会非常麻烦。所以该如何选择使用 reactive 和 ref 就是一个值得深思的问题。

在这里需要注意的就是:ref 只能监听简单类型的变化,不能监听复杂类型的变化,比如对象和数组。

但实际上 ref 底层的本质其实还是 reactive,系统会自动根据我们给 ref 传入的值将它转换成 reactive。 即:ref(xx) => reactive({value:xx})。所以这也就是为什么,ref 是个所谓的响应式对象,以及需要用 value 的原因了。

那既然如此,各位应该还记得,之前我们使用 reactive 的时候,在 template 里需要以对象的形式使用{{data.age}}。那既然 ref 也是 reactive,其中又确实有个 value 属性,我们在 template 里怎么就直接使用 {{age}} 就可以了呢?在这里,不需要使用 {{age.value}} 的原因是 Vue 会自动帮我们添加 .value。

那么 Vue 是如何判断 ref 和 reactive 的呢?判断响应式对象是否为 ref 的方法:其中有一个__v_isRef值,它会记录该数据到底是 ref 还是 reactive。
在这里插入图片描述


setup 中使用 Vue2 的 ref

相信各位在之前 Vue2 的开发过程中,经常会使用到 ref。而现在在 Vue3 中我们用 ref 去创建一个响应式对象,那么在 Vue3 中该如何使用 Vue2 的 ref 呢?

<template>
  <div id="app">
    <div ref="box">我是div</div>
  </div>
</template>

<script>
  export default {
    setup(){
      console.log(this.$refs.box)
    }
  }
</script>

通过上面的代码发现,我们直接在 setup 里像以前这么使用,是不行的。
在这里插入图片描述
而为什么不能这么使用的原因很简单,文章最开始就已经提到:setup 函数是 Vue3 中新增的函数,它是我们在编写组件时,使用 Composition API 的入口。同时它也是 Vue3 中新增的一个生命周期函数,会在 beforeCreate 之前调用。因为此时组件的 data 和 methods 还没有初始化,因此在 setup 中是不能使用 this 的。所以 Vue 为了避免我们错误的使用,它直接将 setup 函数中的 this 修改成了 undefined。因此,很显然这样是用不了的。

既然知道了问题所在,那么想要解决就很简单了。因为这时 this 无法使用,那我们就等到 this 可以使用就解决了。我们在这里可以用 onMounted 来验证一下:

<template>
  <div id="app">
    <div ref="box">我是div</div>
  </div>
</template>

<script>
  import { ref, onMounted } from 'vue'
  export default {
    setup(){
      let box = ref(null);  // reactive({value: null})
      onMounted(()=>{
        console.log('onMounted',box.value); //到了相应生命周期才会执行,结果:<div>我是div<div>
      });
      console.log(box.value);  //虽然放在后侧也会先执行,结果:null
      return { box };
    }
  }
</script>

在这里插入图片描述
综上所述,Vue2 和 Vue3 的 ref 会指向同一个响应式对象噢,而且想要使用时,只要确保在对应生命周期之后去使用即可,也就是不要提前在 setup 中对 ref 做额外操作。


递归监听和非递归监听

在了解了以上内容后,在这里介绍一下 ref 和 reactive 的监听原理。

默认情况下,无论是 ref 还是 reactive 都是递归监听。递归监听就是无论在响应式对象中有多少层内容,每一层它都会去进行 Proxy 监听。因此递归监听的问题就是,如果数据量比较大,它会非常消耗性能。我们可以通过输出看到效果:

let state = reactive({
  a: 'a',
  b: {
    c: 'c',
    d: {
      e: 'e'
    }
  }
})

在这里插入图片描述
除此以外,不要忘记之前在 reactive 的注意点中提到的,如果给 reactive 传递了其他对象,默认情况下修改对象,界面也是不会自动更新的。如果想更新,需要通过重新赋值的方式。


而如果想让它们从递归监听变成非递归监听,我们可以使用 shallowRef 和 shallowReactive:

let state = shallowReactive({
  a: 'a',
  b: {
    c: 'c',
    d: {
      e: 'e'
    }
  }
})

我们可以通过输出看到效果:
在这里插入图片描述
这时我们可以看到,除了第一层以外,都没有了 proxy 监听。


但是对于 ref 有些特殊。我们之前说到过很多次,ref 只能监听简单类型的变化,不能监听复杂类型的变化,比如对象和数组。那么 shallowRef 的存在有些尴尬?首先我们先来看一下使用:

setup(){
    let state = shallowRef({
      a: 'a',
      b: {
        c: 'c',
        d: {
          e: 'e'
        }
      }
    })
    function myFn(){
      state.value.a = '1'
      state.value.b.c = '3'
      state.value.b.d.e = '5'
      console.log(state)
      console.log(state.value)
      console.log(state.value.b)
      console.log(state.value.b.d)
    }
    return { state,myFn };
}

使用的时候依然需要使用 .value。

这是因为之前提到了 ref 和 reactive 的转换关系:ref(xx) => reactive({value:xx})

同理的,shallowRef(xx) => shallowReactive({value:xx})

测试一下可以发现,虽然我们对 state.value.a 进行了修改,但是在页面中是不会发生更新的。难道使用 shallowRef 之后 ref 连第一层都不能监听了?

其实还是老问题,ref 是无法监听复杂类型数据的变化的,所以如果我们想这么用,那就只能通过重新赋新对象的方式了:

setup(){
    let state = shallowRef({
      a: 'a',
      b: {
        c: 'c',
        d: {
          e: 'e'
        }
      }
    })
    function myFn(){
      state.value = {
        a: '1',
        b: {
          c: '3',
          d: {
            e: '5'
          }
        }
      }
      console.log(state)
      console.log(state.value)
      console.log(state.value.b)
      console.log(state.value.b.d)
    }
    return { state,myFn };
}

在这里插入图片描述
首先,shallowRef 确实其效果了,shallow 的值就是用来判断的。其次,通过这种方式就可以发现一个问题,它不仅能修改第一层 a 的值,也能修改之后所有内容的值。所以简单来说,shallowRef 貌似没什么限制效果。因为我们为其设置非递归监听,却还能修改其他层级的数据,确实不够合理。

如果我们真的要这么使用,且需要修改其他层级的数据,那么我们依然只能通过重新赋值的方式来实现吗?针对这个问题,Vue3 为我们提供了一种方式:triggerRef。它会查看之前 state 中发生变化的数据,然后主动帮我们更新。看代码:

<script>
  import { shallowRef, triggerRef } from 'vue'

  export default {
    setup(){
        let state = shallowRef({
          a: 'a',
          b: {
            c: 'c',
            d: {
              e: 'e'
            }
          }
        })
        function myFn(){
          state.value.b.d.e = '5'
          triggerRef(state)
        }
        return { state,myFn };
    }
  }
</script>

这么用就简便的多了。但是这其中最重要的矛盾就是,为什么要使用非递归监听去监听 ref,而且还要让 ref 存储对象或者数组,而不是简单数据类型。而且即使非递归监听,还需要通过重新赋值新对象,或者使用 triggerRef 去让其数据更新,于我而言没发现这种用法的好处。虽然这样能避免递归监听的效率损耗,但在编码上带来的变化也很大。既然是各位大佬最终得到的产物,这样做必然有它的优势,这个优势就需要后续在开发中去体会了。

需要说明的是,虽然之前所有内容都是成对出现的,但这里确实是没有 triggerReactive 的。也就是说,非递归监听下,我们就真没办法修改 reactive 其中任意一层的数据了,这也确实满足非递归监听的本质概念。更何况递归监听下,修改响应式对象的数据是很简单的,不需要这种方式的存在。

所以综上所述,在笔者看来,一般情况下我们使用 ref 和 reactive 即可,只有在需要监听的数据量比较大的时候,才考虑使用 shallowRef 和 shallowReactive。而什么情况下需要使用 shallowRef 仍是需要考量的问题。


toRefs(Vue3 的’模块化’)

Vue3 为了能够帮助我们让代码再简洁一点,还提供了一个 toRefs 函数。这个函数的作用就是将一个响应式函数的对象,转变为普通的对象。但是这个普通的对象里的内容,又都是响应式对象,所以还需要解构,也就是应该这么使用: …toRefs(data)。这一步也可以验证一下,是否 Vue3 真的这么做了。

测试证明如下:

<template>
  <div id="app">
    <p>姓名:{{ name }}</p>
    <p>年龄:
      <button @click="changeAge(-1)">-</button>
      {{ age }}
      <button @click="changeAge(1)">+</button>
    </p>
    <p>出生年份:
      <button @click="changeYear(-1)">-</button>
      {{ year }}
      <button @click="changeYear(1)">+</button>
    </p>
  </div>
</template>

<script>
  import { reactive,computed,toRefs } from 'vue'
  export default {
    setup(){
      const data = reactive({
        name: '王路飞',
        age: 17,
        year: computed({
          get: () => {
            return 2020 - data.age;
          },
          set: val => {
            data.age = 2020 - val;
          }
        })
      });
      function changeAge(val){
        data.age += val;
      }
      function changeYear(val){
        data.year += val;
      }
      console.log('data', data);
      console.log('toRefs', toRefs(data));
      return { ...toRefs(data), changeAge, changeYear };
    }
  }
</script>

在这里插入图片描述
在这里插入图片描述
从结果上来看,使用 toRefs 后,它会将一个响应式函数的对象,转变为一个普通的对象。这个普通对象里的内容,都会是响应式对象。

这么做之后,在 template 里就不需要写 data.age 了。再在此的基础上,我们还能把 data 部分的数据拿出来,写在一个函数中,这样就更方便我们区分各个功能模块了:

<script>
  import { reactive,computed,toRefs } from 'vue'
  export default {
    setup(){
      const { data, changeAge, changeYear } = test();
      return { ...toRefs(data), changeAge, changeYear };
    }
  }
  function test(){
    const data = reactive({
      name: '王路飞',
      age: 17,
      year: computed({
        get: () => {
          return 2020 - data.age;
        },
        set: val => {
          data.age = 2020 - val;
        }
      })
    });
    function changeAge(val){
      data.age += val;
    }
    function changeYear(val){
      data.year += val;
    }
    return { data, changeAge, changeYear };
  }
</script>

那这样一来,我们就还可以把test函数从这个vue组件当中提取出来,放在一个js文件当中集中管理相关功能了。
在这里插入图片描述
这样就达成了所谓 Vue3 的模块化,从而简化了代码,而且把各个功能模块抽分出去,方便管理。


toRef

首先先看个结论:

setup(){
    let obj = {name: 'zs'};
    let state = ref(obj.name);

    function myFn(){
      state.value = 'ls';
      console.log(state.value);  // 输出ls,页面更新
      console.log(obj.name);  // 输出zs
    }
    return { state, myFn };
}

结论就是:如果利用 ref 将某一个对象中的属性变成响应式的数据,那我们修改响应式的数据是不会影响到原始数据的。

但当我们使用 toRef 将某一个对象中的属性变成响应式的数据,那我们修改响应式的数据是会影响到原始数据的。但是此时就不会触发页面的更新了。

setup(){
    let obj = {name: 'zs'};
    let state = toRef(obj, 'name');

    function myFn(){
      state.value = 'ls';
      console.log(state.value); // 输出ls,页面不更新
      console.log(obj.name);    // 输出ls,页面不更新
    }
    return { state, myFn };
}

所以 ref 和 toRef 的区别是:
ref 是对原始数据的复制,修改响应式数据不会影响原始数据,同时数据发生改变,界面就会自动更新。
toRef 是对原始数据的引用,修改响应式数据会影响原始数据,但数据发生改变,界面不会自动更新。

toRef 的应用场景就是:响应式数据和原始数据相关联,但我们又不想更新数据后更新界面,那么就可以使用 toRef。


基于上述内容,如果 obj 中有很多属性,又想使用 toRef,此时我们可以利用 toRefs。因为 toRefs 会将一个响应式函数的对象,转变为普通的对象。但是这个普通的对象里的内容,又都是响应式对象,所以 state 中就可以这样用了:

<template>
  <div id="app">
    <p>{{name}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template>

<script>
  import { toRefs } from 'vue'

  export default {
    setup(){
        let obj = {name: 'zs', age: 17};
        let state = toRefs(obj);

        function myFn(){
          state.name.value = 'ls';
          state.age.value = '16';
        }
        return { ...toRefs(state), myFn };
    }
  }

</script>

customRef

之前我们使用的是 ref 去创建一个响应式对象。如果我们想显式地控制依赖追踪和触发响应,此时我们就可以使用 customRef 去自定义一个 ref。customRef 的作用就是返回一个 ref 对象。相关代码:

<template>
  <div id="app">
    <p>{{age}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template>

<script>
  import { customRef } from 'vue'
  function myRef(value){
    return customRef((track, trigger)=>{
      return{
        get(){
          track();  //告诉Vue这个数据需要追踪变化
          return value;
        },
        set(newValue){
          value = newValue;
          trigger(); //告诉Vue触发界面更新
        }
      }
    });
  }
  export default {
    setup(){
      let age = myRef(18);
      function myFn(){
        age.value += 1;
      }
      return { age,myFn };
    }
  }
</script>

那什么时候考虑选用 customRef 呢?

首先,如果我们想要去获取 json 数据,我们需要这么使用:(自己在public中建了一个data.json)

setup(){
  let state = ref([]);

  fetch('../public/data.json')
    .then((res)=>{
      return res.json();
    })
    .then((data)=>{
      console.log(data);
      state.value = data;
    })
    .catch((err)=>{
      console.log(err);
    })
  return { state };
}

(这里补充一下,我之前使用 @vue/cli 的时候,这个方式去调用 json 数据会报错。但是根据报错信息去检查时,发现自己写的没有问题,所以在这里沉思一段时间,查阅资料也没找到解决方法。后来改用 Vite 创建项目,发现问题直接解决,暂时不知道原因,但之前提到过 Vite 是 Vue 作者开发的一款意图取代 webpack 的工具,也许 Vite 中帮我们处理了 json 数据。因此如果使用 @vue/cli 遇到 json 数据报错,却找不到原因的话,可以考虑使用 Vite 创建项目进行尝试)

言归正传,之前提到过,setup 是不能异步使用的,也就是在这里不能用 async 和 await 去获取 json 数据。那么之后获取更多数据,就要在其中写这么多的回调函数显然不方便管理。这时,我们可以考虑使用 customRef:

<script>
  import { customRef } from 'vue'
  function myRef(value){
    return customRef((track, trigger)=>{
      fetch(value)
              .then((res)=>{
                return res.json();
              })
              .then((data)=>{
                console.log(data);
                value = data;
                trigger();
              })
              .catch((err)=>{
                console.log(err);
              })
      return{
        get(){
          track();  //告诉Vue这个数据需要追踪变化
          return value;
        },
        set(newValue){
          value = newValue;
          trigger(); //告诉Vue触发界面更新
        }
      }
    });
  }
  export default {
    setup(){
      let state = myRef('../public/data.json');
      return { state };
    }
  }
</script>

参数 props 和 context

setup 函数,它其实是可以设置一些参数的,一个叫做 props,一个叫做 context。

这个 props 参数是用来获取在组件中定义的 props 的。如下所示:

  export default {
    props:{
      title: String
    },
    setup(props){
      const data = reactive({
        name: '王路飞',
        age: 17,
        year: computed({
          get: () => {
            return 2020 - data.age;
          },
          set: val => {
            data.age = 2020 - val;
          }
        })
      });
      function changeAge(val){
        data.age += val;
        console.log(props.title)
      }
      function changeYear(val){
        data.year += val;
      }
      return { ...toRefs(data), changeAge, changeYear };
    }
  }

需要注意的是 props.title,因为我们知道通过 props 传递过来的值都只是可读的,无法修改。

除此以外,我们定义的所有 props 都是响应式的,我们可以监听 props 的值,一旦发生变化我们就做出相应。比如我们想监听 title 的值,我们需要使用 watch,如下代码:

<script>
  import { reactive, computed, toRefs, watch } from 'vue'
  export default {
    props:{
      title: String
    },
    setup(props){
      const data = reactive({
        name: '王路飞',
        age: 17,
        year: computed({
          get: () => {
            return 2020 - data.age;
          },
          set: val => {
            data.age = 2020 - val;
          }
        })
      });
      function changeAge(val){
        data.age += val;
        console.log(props.title)
      }
      function changeYear(val){
        data.year += val;
      }
      watch(() => props.title, (newTitle, oldTitle) => {
        console.log(newTitle,oldTitle)
      })
      return { ...toRefs(data), changeAge, changeYear };
    }
  }
</script>

对于第二个参数 context,我们之前说到在 setup 里是不能使用 this 的,但如果我们有些功能需要使用 this,我们就可以使用 context 来获取 attribute,获取插槽,或者发送事件。比如:

setup(props, context){
  const data = reactive({
    name: '王路飞',
    age: 17,
    year: computed({
      get: () => {
        return 2020 - data.age;
      },
      set: val => {
        data.age = 2020 - val;
      }
    })
  });
  function changeAge(val){
    data.age += val;
    console.log(props.title)
  }
  function changeYear(val){
    data.year += val;
  }
  watch(() => props.title, (newTitle, oldTitle) => {
    console.log(newTitle,oldTitle);
    context.emit('title-change');
  })
  return { ...toRefs(data), changeAge, changeYear };
}

context 的使用方法和以前是一样的,比如使用 emit。


toRaw

我们现在知道,在 setup 函数里,只有响应式的数据发生改变,页面才有可能发生更新,也就是在 setup 函数里一个普通对象发生改变,是无论如何都不能引起页面更新的。那现在我们去做这些操作:

setup(){
    let obj = {name: 'zs', age: 17};
    let state = reactive(obj);

    console.log(obj === state); //  false

    function myFn(){
      // 对obj操作无法让页面发生更新,但是会修改obj数据
      obj.name = 'ls'
      console.log(obj);
      // 对state操作才能让页面发生更新,同时会修改obj数据
      // state.name = 'ls';
      // console.log(state);
    }
    return { state,myFn };
}

那么这里的 obj 和 state 是什么关系呢?

它们是引用关系,state 的本质是一个 Proxy 对象,在这个 Proxy 对象中引用了 obj。

在这里插入图片描述
说到 toRaw 的作用,它会从 reactive 或 ref 中得到原始数据(引用)。也就是说:

let obj = {name: 'zs', age: 17};
let state = reactive(obj);
let obj2 = toRaw(state);
// let state = ref(obj);
// let obj2 = toRaw(state.value);

console.log(obj === state); // false
console.log(obj === obj2);  // true

那么这么做有什么用?

因为 ref 和 reactive 每次修改都会被追踪,都会更新 UI 界面,所以如果有一些操作不需要追踪,不需要更新界面,那么这个时候 toRaw 的作用就体现出来了。因为它拿到原始数据,对原始数据进行修改就不会被追踪了,从而让性能提升。

除此以外还有一个 markRaw。如果某数据永远都不想被追踪,就可以使用 markRaw。

let obj = {name: 'zs', age: 17};
obj = markRaw(obj);
let state = reactive(obj);

readonly

readonly 和它的名字一样,数据只能是只读的,并且是递归只读的。也就是它里面所有层级都是只读的。比如下面代码:

<template>
  <div id="app">
    <p>{{state.name}}</p>
    <p>{{state.attr.age}}</p>
    <p>{{state.attr.height}}</p>
    <button @click="myFn">按钮</button>
  </div>
</template>

<script>
  import { readonly, isReadonly, shallowReadonly } from 'vue'

  export default {
    setup(){
      let state = readonly({name: 'ls', attr:{age: 18,height: 1.88}});
      function myFn(){
        state.name = 'zs';
        state.attr.age = 16;
        state.attr.height = 1.66;
      }
      return {state, myFn};
    }
  }
</script>

当我们试图去修改state的数据时,就会出现提示:
在这里插入图片描述


shallowReadonly 就可以让只有第一层是只读的:

setup(){
  let state = shallowReadonly({name: 'ls', attr:{age: 18,height: 1.88}});
  function myFn(){
    state.name = 'zs';
    state.attr.age = 16;
    state.attr.height = 1.66;
	console.log(state);
  }
  return {state, myFn};
}

在这里插入图片描述
虽然设置 shallowReadonly 只有第一层是只读的,但是对其它内容进行修改,也无法让视图发生更新。


isReadonly 就很简单了,用于判定是否为只读类型数据:

function myFn(){
  console.log(isReadonly(state));
}

对于只读类型数据,第一时间就可以想到 const。const 和 readonly 的区别:

我们知道如果 const 声明了一个对象,对象内部数据再重新赋值,依然是可以修改的,那么 const 做到的其实是赋值保护,即不能给变量重新赋值。但 readonly 是属性保护,即不能给属性重新赋值。


补充说明:了解了 Composition API 之后,需要说的是,它的本质其实还是 Vue2.x 的 Option API。setup 函数 return 时,它就会把响应式对象写入到 data 里,把 methods 方法写入到 methods 里,其他功能都是同理。

Logo

前往低代码交流专区

更多推荐