vue3_01_composition-API

vue2中Options API的弊端

  • 在Vue2中,我们 编写组件的方式是Options API:

    • Options API的一大特点就是在对应的属性中编写对应的功能模块; p比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子
  • 但是这种代码有一个很大的弊端:

    • 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
    • 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
    • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
  • 下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:

    • 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
    • 并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;

大组件的逻辑分散

在这里插入图片描述

  • 如果我们能将同一个逻辑关注点相关的代码收集在一起会更好。
  • 这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
  • 也有人把Vue Composition API简称为VCA

认识Composition API

  • 那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
    • 为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方
    • 在Vue组件中,这个位置就是 setup 函数
  • setup其实就是组件的另外一个选项:
    • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项
    • 比如methods、computed、watch、data、生命周期等等;
  • 接下来我们一起学习这个函数的使用:
    • 函数的参数
    • 函数的返回值

setup函数的参数

父组件home.vue

<template>
  <div>
    <home message="hahahaha" id="aaa" class="bbbb"></home>
  </div>
</template>

<script>
  import Home from './Home.vue';

  export default {
    components: {
      Home
    }
  }
</script>

子组件Son.vue

<template>
  <div>
    Home Page
    <h2>{{message}}</h2>

    <h2>{{title}}</h2>
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    data() {
      return {
        counter: 100
      }
    },
    /**
     * 参数一: props, 父组件传递过来属性
     */
    // setup函数有哪些参数?
    // setup函数有什么样的返回值
    // setup(props, context) {
    setup(props, {attrs, slots, emit}) {
      console.log(props.message);
      console.log(attrs.id, attrs.class);
      console.log(slots);
      console.log(emit);

      return {
        title: "Hello Home",
        counter: 100
      }
    },
    methods: {
      btnClick() {
        this.$emit("")
      }
    }
  }
</script>
  • 我们先来研究一个setup函数的参数,它主要有两个参数:
    • 第一个参数:props
    • 第二个参数:context
  • props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可
    以直接通过props参数获取:
    • 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
    • 并且在template中依然是可以正常去使用props中的属性,比如message;
    • 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取;
    • 因为props直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可
  • 另外一个参数是context,我们也称之为是一个SetupContext,它里面包含 三个属性
    • attrs:所有的非prop的attribute;
    • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用);
    • emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);

setup函数的返回值

  • setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
    • psetup的返回值可以在模板template中被使用;
    • 也就是说我们可以通过setup的返回值来替代data选项;
  • 甚至是我们可以返回一个执行函数来代替在methods中定义的方法
    在这里插入图片描述
  • 但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
    • 答案是不可以;
    • 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;

setup不可以使用this

  • 官方关于this有这样一段描述
  • 表达的含义是this并没有指向当前组件实例
  • 并且在setup被调用之前,data、computed、methods等都没有被解析;
  • 所以无法在setup中获取this

Reactive API

如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
在这里插入图片描述

那么这是什么原因呢?为什么就可以变成响应式的呢?

  • 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集
  • 数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
<template>
  <div>
    Home Page
    <h2>{{message}}</h2>
    <h2>当前计数: {{state.counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  import { reactive } from 'vue';

  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      const state = reactive({
        counter: 100
      })

      // 局部函数
      const increment = () => {
        state.counter++;
        console.log(state.counter);
      }

      return {
        state,
        increment
      }
    }
  }
</script>

Ref API

  • eactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
  • 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;

在这里插入图片描述

<template>
  <div>
    Home Page
    <h2>{{message}}</h2>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+1</button>

    <show-message :message="counter"></show-message>
  </div>
</template>

<script>
  import { ref } from 'vue';

  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      // counter编程一个ref的可响应式的引用
      // counter = 100;
      let counter = ref(100);

      // 局部函数
      const increment = () => {
        counter.value++;
        console.log(counter.value);
      }

      return {
        counter,
        increment
      }
    }
  }
</script>
  • 这个时候Vue3给我们提供了另外一个API:ref API

    • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
    • 它内部的值是在ref的 value 属性中被维护的;
      在这里插入图片描述
  • 这里有两个注意事项:

    • 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
    • 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
<template>
  <div>
    Home Page
    <h2>{{message}}</h2>
    <!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
    <h2>当前计数: {{counter}}</h2>
    <!-- ref的解包只能是一个浅层解包(info是一个普通的JavaScript对象) -->
    <h2>当前计数: {{info.counter.value}}</h2>
    <!-- 当如果最外层包裹的是一个reactive可响应式对象, 那么内容的ref可以解包 -->
    <h2>当前计数: {{reactiveInfo.counter}}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  import { ref, reactive } from 'vue';

  export default {
    props: {
      message: {
        type: String,
        required: true
      }
    },
    setup() {
      let counter = ref(100);

      const info = {
        counter
      }

      const reactiveInfo = reactive({
        counter
      })

      // 局部函数
      const increment = () => {
        counter.value++;
        console.log(counter.value);
      }

      return {
        counter,
        info,
        reactiveInfo,
        increment
      }
    }
  }
</script>

readonly的使用

  • 在readonly的使用过程中,有如下规则:

    • preadonly返回的对象都是不允许修改的;

    • 但是经过readonly处理的原来的对象是允许被修改的;

      • ✅ 比如 const info = readonly(obj),info对象是不允许被修改的;
      • ✅ 当obj被修改时,readonly返回的info对象也会被修改;
      • ✅ 但是我们不能去修改readonly返回的对象info;
    • p其实本质上就是readonly返回的对象的setter方法被劫持了而已;

    在这里插入图片描述

toRefs

 import { reactive, toRefs, toRef } from 'vue';

如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:
在这里插入图片描述
那么有没有办法让我们解构出来的属性是响应式的呢?

  • Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
  • 那么我们再次进行结构出来的 name 和 age 本身都是 ref的;
 setup() {
      const info = reactive({name: "why", age: 18});
      // 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
      let { name, age } = toRefs(info);
       
      return {
        name,
        age
      }
    }

这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;

toRef

如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
在这里插入图片描述

 setup() {
      const info = reactive({name: "why", age: 18});
   
      let { name } = info;
      let age = toRef(info, "age");

      const changeAge = () => {
        age.value++;
      }

      return {
        name,
        age,
        changeAge
      }
    }

computed

在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理

  • 在前面的Options API中,我们是使用computed选项来完成的;
  • 在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;

如何使用computed呢?

方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
在这里插入图片描述

<template>
  <div>
    <h2>{{fullName}}</h2>
    <button @click="changeName">修改firstName</button>
  </div>
</template>

<script>
  import { ref, computed } from 'vue';

  export default {
    setup() {
      const firstName = ref("Kobe");
      const lastName = ref("Bryant");

      // 1.用法一: 传入一个getter函数
      // computed的返回值是一个ref对象
      const fullName = computed(() => firstName.value + " " + lastName.value);

      // 2.用法二: 传入一个对象, 对象包含getter/setter
      const fullName = computed({
        get: () => firstName.value + " " + lastName.value,
        set(newValue) {
          const names = newValue.split(" ");
          firstName.value = names[0];
          lastName.value = names[1];
        }
      });

      const changeName = () => {
        // firstName.value = "James"
        fullName.value = "coder";
      }

      return {
        fullName,
        changeName
      }
    }
  }
</script>

侦听数据的变化

在前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。

在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;

  • watchEffect用于自动收集响应式数据的依赖
  • watch需要手动指定侦听的数据源

watchEffect

**当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。 **
我们来看一个案例:
首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
在这里插入图片描述

<template>
  <div>
    <h2>{{name}}-{{age}}</h2>
    <button @click="changeName">修改name</button>
    <button @click="changeAge">修改age</button>
  </div>
</template>

<script>
  import { ref, watchEffect } from 'vue';

  export default {
    setup() {
      // watchEffect: 自动收集响应式的依赖
      const name = ref("why");
      const age = ref(18);

      const changeName = () => name.value = "kobe"
      const changeAge = () => age.value++

      watchEffect(() => {
        console.log("name:", name.value, "age:", age.value);
      });

      return {
        name,
        age,
        changeName,
        changeAge
      }
    }
  }
</script>

watchEffect的停止侦听

如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。

  • 比如在上面的案例中,我们age达到20的时候就停止侦听:

在这里插入图片描述

<template>
  <div>
    <h2>{{name}}-{{age}}</h2>
    <button @click="changeName">修改name</button>
    <button @click="changeAge">修改age</button>
  </div>
</template>

<script>
  import { ref, watchEffect } from 'vue';

  export default {
    setup() {
      // watchEffect: 自动收集响应式的依赖
      const name = ref("tom");
      const age = ref(18);

      const stop = watchEffect(() => {
        console.log("name:", name.value, "age:", age.value);
      });

      const changeName = () => name.value = "kobe"
      const changeAge = () => {
        age.value++;
        if (age.value > 25) {
          stop();
        }
      }

      return {
        name,
        age,
        changeName,
        changeAge
      }
    }
  }
</script>


watchEffect清除副作用

什么是清除副作用呢?

  • 比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,

    或者侦听器侦听函数被再次执行了。

  • 那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用;

在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate

  • 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
  • 我们可以在传入的回调函数中,执行一些清楚工作;
    在这里插入图片描述
<template>
  <div>
    <h2>{{name}}-{{age}}</h2>
    <button @click="changeName">修改name</button>
    <button @click="changeAge">修改age</button>
  </div>
</template>

<script>
  import { ref, watchEffect } from 'vue';

  export default {
    setup() {
      // watchEffect: 自动收集响应式的依赖
      const name = ref("tom");
      const age = ref(18);

      const stop = watchEffect((onInvalidate) => {
        const timer = setTimeout(() => {
          console.log("网络请求成功~");
        }, 2000)

        // 根据name和age两个变量发送网络请求
        onInvalidate(() => {
          // 在这个函数中清除额外的副作用
          // request.cancel()
          clearTimeout(timer);
          console.log("onInvalidate");
        })
        console.log("name:", name.value, "age:", age.value);
      });

      const changeName = () => name.value = "kobe"
      const changeAge = () => {
        age.value++;
        if (age.value > 25) {
          stop();
        }
      }

      return {
        name,
        age,
        changeName,
        changeAge
      }
    }
  }
</script>

setup中使用ref

在讲解 watchEffect执行时机之前,我们先补充一个知识:在setup中如何使用ref或者元素或者组件?
其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
在这里插入图片描述

<template>
  <div>
    <h2 ref="title">哈哈哈</h2>
  </div>
</template>

<script>
  import { ref, watchEffect } from 'vue';

  export default {
    setup() {
      const title = ref(null);

      watchEffect(() => {
        console.log(title.value);
      }, {
        flush: "post"
      })

      return {
        title
      }
    }
  }
</script>

watchEffect的执行时机与ref引用

默认情况下,组件的更新会在副作用函数执行之前:
如果我们希望在副作用函数中获取到元素,是否可行呢?
在这里插入图片描述

<template>
  <div>
    <h2 ref="title">哈哈哈</h2>
  </div>
</template>

<script>
  import { ref, watchEffect } from 'vue';

  export default {
    setup() {
      const title = ref(null);

      watchEffect(() => {
        console.log(title.value);
      }, {
        flush: "post"
      })

      return {
        title
      }
    }
  }
</script>

我们会发现打印结果打印了两次:

  • 这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null;
  • 而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素;

调整watchEffect的执行时机

如果我们希望在第一次的时候就打印出来对应的元素呢?

  • 这个时候我们需要改变副作用函数的执行时机;
  • 它的默认值是pre,它会在元素 挂载 或者 更新 之前执行;
  • 所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
    我们可以设置副作用函数的执行时机:
    在这里插入图片描述
    flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。

Watch的使用

  • watch的API完全等同于组件watch选项的Property:
  • watch需要侦听特定的数据源,并在回调函数中执行副作用;
  • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;

与watchEffect的比较,watch允许我们:

  • 懒执行副作用(第一次不会直接执行);
  • 更具体的说明当哪些状态发生变化时,触发侦听器的执行;
  • 访问侦听状态变化前后的值;

侦听单个数据源

watch侦听函数的数据源有两种类型:

  • 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
  • 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
    在这里插入图片描述
<template>
  <div>
    <h2 ref="title">{{info.name}}</h2>
    <button @click="changeData">修改数据</button>
  </div>
</template>

<script>
  import { ref, reactive, watch } from 'vue';

  export default {
    setup() {
      const info = reactive({name: "why", age: 18});

      // 1.侦听watch时,传入一个getter函数
      watch(() => info.name, (newValue, oldValue) => {
        console.log("newValue:", newValue, "oldValue:", oldValue);
      })

      // 2.传入一个可响应式对象: reactive对象/ref对象
      // 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
      // watch(info, (newValue, oldValue) => {
      //   console.log("newValue:", newValue, "oldValue:", oldValue);
      // })
      // 如果希望newValue和oldValue是一个普通的对象
      watch(() => {
        return {...info}
      }, (newValue, oldValue) => {
        console.log("newValue:", newValue, "oldValue:", oldValue);
      })
      // 情况二: ref对象获取newValue和oldValue是value值的本身
      // const name = ref("why");
      // watch(name, (newValue, oldValue) => {
      //   console.log("newValue:", newValue, "oldValue:", oldValue);
      // })

      const changeData = () => {
        info.name = "kobe";
      }

      return {
        changeData,
        info
      }
    }
  }
</script>

侦听多个数据源

侦听器还可以使用数组同时侦听多个源:
在这里插入图片描述

侦听响应式对象

如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
在这里插入图片描述

<template>
  <div>
    <h2 ref="title">{{info.name}}</h2>
    <button @click="changeData">修改数据</button>
  </div>
</template>

<script>
  import { ref, reactive, watch } from 'vue';

  export default {
    setup() {
      // 1.定义可响应式的对象
      const info = reactive({name: "why", age: 18});
      const name = ref("why");

      // 2.侦听器watch
      watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, oldName]) => {
        console.log(newInfo, newName, oldInfo, oldName);
      })

      const changeData = () => {
        info.name = "kobe";
      }

      return {
        changeData,
        info
      }
    }
  }
</script>

watch的选项

如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true:
也可以传入 immediate 立即执行;
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

star@星空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值