Vue3 ( 五 ) 完--ComPositionAPI、Setup语法糖

339 阅读2分钟

一.Options API的弊端

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

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

2. 这种代码有一个很大的弊端:

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

二.Composition API

1.认识compositionAPI

Composition API想要做的事情,将同一个逻辑关注点相关的代码收集在一起。也有人把Vue Composition API简称为VCA。

2.setup函数

setup其实就是组件的另外一个选项:

  • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
  • 比如methods、computed、watch、data、生命周期等等
2.1 setup函数的参数

它主要有两个参数:

  1. 第一个参数:props
  2. 第二个参数:context

props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接 通过props参数获取:

  1. 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
  2. 并且在template中依然是可以正常去使用props中的属性,比如message;
  3. 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取);
  4. 因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可;

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:

  1. attrs:所有的非prop的attribute;
  2. slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用);
  3. emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
2.1 setup函数基本使用

1.计数器案例

<template>
  <div class="app">
    <!-- 对应template中ref对象自动解包 -->
    <h2>当前计数:{{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>

  </div>
</template>
<script>
import { ref } from 'vue';
export default {

  setup() {
    // 1.定义counter内容
    // let counter = 100;//默认定义的数据不是响应式的数据
    let counter = ref(100);//ref对原来的数据做一个包裹,返回一个可响应的counter
    const increment = () => {
      counter.value++
    };
    const decrement = () => {
      counter.value--
  }
    // 所有想在template用到的东西,必须在retrun中做一个返回。
    return {
      counter,
      increment,
      decrement
    }
  },

}
</script>
<style scoped></style>

2.抽取计数器案例

image.png

3. setup定义数据

1. Reactive API
  • 如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
 // 2.定义响应式数据
        // 2.1 reactive函数-翻译:响应定义复杂类型的数据:
        const account = reactive({
            username: "www",
            password: 123,
        })
  • 那么这是什么原因呢?为什么就可以变成响应式的呢?
  • 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集
  • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其变成响应式对象的;
2. Ref API
  • reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型
  • 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;

image.png

这个时候Vue3给我们提供了另外一个API:ref API

  • ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用 维护着它内部的值,这就是ref名称的来源;
  • 它内部的值是在ref的 value 属性中被维护的;
    // 2.2 ref函数:定义简单类型的数据(也可以定义复杂类型的数据) 
        // counter定义响应式数据
        const counter = ref(0);//返回的是ref对象

这里有两个注意事项:

  • 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
 <!-- 默认情况下在template中使用ref时,vue会自动对其进行解包(取出其中的value) -->
        <h2>{{ counter }}</h2>
  • 但是在setup函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;

4. ref和reactive的应用场景

1. ractive的应用场景

1.1 条件一:reactive应用于本地的数据

1.2条件二:多个数据之间有关系/联系(聚合的数据,组织在一起有特定的数据)

2.ref的应用场景,其他的场景基本都用ref

2.1 定义本地简单的数据

2.2 定义从网络中获取的数据也使用ref

   setup() {
        // 定义响应式数据:ref/reactive
        // 强调ref也可以定义复杂的数据
        const info = ref({})
        console.log(info.value);

        // 1.ractive的应用场景
        // 1.1 条件一:reactive应用于本地的数据
        // 1.2条件二:多个数据之间有关系/联系(聚合的数据,组织在一起有特定的数据)
        const account = reactive({
            username: "aaa",
            password: 1123,
        })

        // 2.ref的应用场景,其他的场景基本都用ref
        // 2.1 定义本地简单的数据
        const message = ref("hello");
        // 2.2 定义从网络中获取的数据也使用ref
        const musics = ref([]);
        onmounted(() => {
            const servemusics = ["hhh", 'aa', 'wd'];
            musics.value = servemusics;
        })
        return {
            account,
            message,
            
        }
    }

2.4.1 小案例,父传子,子传父

父组件

<template>
    <div>
        <h2>app:{{ info }}</h2>
    </div>

    <show-info :message="info" @showChange="showChangename"></show-info>
</template>
<script>
import showInfo from './showInfo.vue';
import { reactive } from 'vue'
export default {
    components: { showInfo },
    setup() {
        // 本地定义多个数据,都需要传递给子组件、
        const info = reactive({
            name: "wzb",
            age: 19,
            height: 1.88,
        });
        const showChangename = (payload) => {
            info.name = payload;
        }
        return {
            info,
            showChangename
        }
    }
}
</script>
<style scoped></style>

子组件

<template>
    <div>
        <h2>showinfo:{{ message }}</h2>
        <!-- 代码没有错误,但是违背数据原则(单项数据流) -->
        <!-- <button @click="message.name = 'kobe'">修改</button> -->
        <!-- 正确的做法: -->
        <button @click="showinfochange">按钮</button>
    </div>
</template>
<script>
export default {
    props: {
        message: {
            type: Object,
            default: () => ({})//相当于传对象的引用
        }
    },
    emits: ['showChange'],
    setup(props, context) {
        const showinfochange = () => {
            context.emit("showChange", "kobe")
        }
        return {
            showinfochange
        }
    }
}
</script>
<style scoped></style>

5. Setup 其他函数

1.认识readonly
  1. 我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希 望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
  2. Vue3为我们提供了readonly的方法;
  • readonly会返回原始对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修 改);
  1. 在开发中常见的readonly方法会传入三个类型的参数:
  • 类型一:普通对象;
  • 类型二:reactive返回的对象;
  • 类型三:ref的对象;
2.readonly的使用
  1. 在readonly的使用过程中,有如下规则:
  • readonly返回的对象都是不允许修改的;
  • 但是经过readonly处理的原来的对象是允许被修改的;比如 const info = readonly(obj),info对象是不允许被修改的; 当obj被修改时,readonly返回的info对象也会被修改;
  • 但是我们不能去修改readonly返回的对象info;
  1. 其实本质上就是readonly返回的对象的setter方法被劫持了而已;

image.png

3. Reactive判断的API
  1. isProxy
  • 检查对象是否是由 reactive 或 readonly创建的 proxy。
  1. isReactive
  • 检查对象是否是由 reactive创建的响应式代理:
  • 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
  1. isReadonly
  • 检查对象是否是由 readonly 创建的只读代理。
  1. toRaw
  • 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
  1. shallowReactive
  • 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
  1. shallowReadonly
  • 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
4.toRefs
  1. 如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive 返回的state对象,数据都不再是响应式的:
 const info = reactive({
            name: "wwww",
            age: 19,
        })
// reactive被解构后会变成普通的值,失去响应式
        // const { name, age } = info;
  1. 那么有没有办法让我们解构出来的属性是响应式的呢?
  • Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
  // toRefs可以将reactive返回的对象中的属性都转成ref;
  const { name, age } = toRefs(info);
  
 
  • 那么我们再次进行解构出来的 name 和 age 本身都是 ref的;
  1. 这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;

  2. 只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:

 // toRef单独解构一个,一个参数是解构对象,第二个是数据。
        const height = toRef(info, 'height');
5.ref的其他api
  1. unref
  2. 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
  • 如果参数是一个 ref,则返回内部值,否则返回参数本身;
  • 这是 val = isRef(val) ? val.value : val 的语法糖函数;
  1. isRef 判断值是否是一个ref对象。
  2. shallowRef
  • 创建一个浅层的ref对象;
  1. triggerRef
  • 手动触发和 shallowRef 相关联的副作用:

6. setup不可以使用this

官方关于this有这样一段描述

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

三.computed

1. 如何使用computed呢?

  • 方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
 // 1.computed只写一个函数的时候,本质在写get语法
        const fullName = computed(() => {
            return names.firstName + " " + names.lastNmae;
        })
  • 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
 // 2.同时写get和set
        const fullName = computed({
            set: function (newValue) {
                const tempNames = newValue.split(" ")
                names.firstName = tempNames[0];
                names.lastNmae = tempNames[1];
            },

            get: function () {
                return names.firstName + " " + names.lastNmae;
            }
        })
        const change = () => {
            fullName.value = "你好 啊";

四.setup中使用ref

  1. setup中如何使用ref获取元素或者组件,只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
<template>
    <div>
        <!-- 1.获取元素 -->
        <h2 ref="titleRef">我是标题</h2>
        <button ref="btn">按钮</button>
        <button @click="getElement">获取元素</button>
        
        <!-- 2.获取组件实例 -->
        <show-info ref="showRef"></show-info>

    </div>
</template>
<script>
import { ref, onMounted } from 'vue'
import showInfo from './showInfo.vue';
export default {
    components: { showInfo },
    setup() {
        const titleRef = ref();
        const btn = ref();
        const showRef = ref();
        const getElement = () => {
            console.log(titleRef.value);
            // console.log(object);
        }
        // mounted的生命周期函数
        onMounted(() => {
            console.log(titleRef.value);
            console.log(btn.value);
            console.log(showRef.value);
            // 拿到showinfo里面定义的方法,
            showRef.value.showInfofoo();
        })
        return {
            titleRef,
            getElement,
            showRef,
            btn
        }
    }
}
</script>
<style scoped></style>

五.生命周期钩子

  1. setup 可以用来替代 data 、 methods 、 computed 等等这些选项,也可以替代 生命周期钩子。
  2. 那么setup中如何使用生命周期函数呢?可以使用直接导入的 onX 函数注册生命周期钩子; image.png
import { onMounted } from 'vue';
export default {
    setup() {
        // 在执行setup函数,你需要注册别的生命周期函数
        onMounted(() => {
            console.log("onmounted的生命周期");
        })
        return {

        }

六.Provide函数

  1. 我们可以通过 provide来提供数据:
  2. 通过 provide 方法来定义每个 Property;
  3. provide可以传入两个参数:
  • name:提供的属性名称;
  • value:提供的属性值
  1. 在 后代组件中可以通过 inject 来注入需要的属性和对应的值:
  2. inject可以传入两个参数:
  • 要 inject 的 property 的 name;
  • 默认值;
 const name = inject('name');
        const age = inject('age');

image.png

7.侦听数据的变化

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

1. watch:需要手动指定侦听的数据源;

 // 1.定义数据
        const message = ref("hello world");

        const info = reactive({
            name: "why",
            age: 18,
        })
        // 2.侦听数据的变量
        watch(message, (newVlue, oldValue) => {
            console.log(newVlue, oldValue);//你好啊~ hello world
        })
        watch(info, (newVlue, oldValue) => {
            console.log(newVlue, oldValue);//代理对象proxy
            //Proxy {name: 'info改变', age: 18} Proxy {name: 'info改变', age: 18}
        }, {
            immediate: true//深度监听
        })

        // 3.监听reactive数据变化后,获取普通对象
        //()=>({...info})代表第一个参数是一个函数,会执行这个函数,
        //查看有哪些依赖,info发生变化时,会执行后面函数的回调
        watch(() => ({ ...info }), (newVlue, oldValue) => {
            console.log(newVlue, oldValue);
        }, {
            immediate: true,//深度监听
            deep: true,
        })

2. 侦听器还可以使用数组同时侦听多个源:

image.png 3. watchEffect:用于自动收集响应式数据的依赖;

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

image.png

 setup() {
        const counter = ref(0);
        // 当counter发生变化时,回调某一个函数
        // 方法一:
        // watch(counter, (newValue, oldVlaue) => {
        // })
        // 方法二,
        // 1. watchEffecct,传入的函数默认直接被执行,
        // 2. 在执行的过程中,会自动收集依赖(依赖那些响应式的数据),只要依赖发生变化,就会重新执行
        watchEffect(() => {
            console.log("——————",counter.value);
        })
        return {
            counter
        }
    }

3. watchEffect的停止侦听

image.png

4. watch/watchEffect区别:

  • 1.watch必须制定数据源, watchEffect自动收集依赖
  • 2.watch监听到改变, 可以拿到改变前后value
  • 3.watchEffect默认直接执行一次, watch在不设置immediate第一次是不执行

三.Setup-hooks练习

hooks:回调函数,把相同的逻辑抽取到独立的函数里面。

1.将原来逻辑抽成hooks,进行复用,使逻辑看起来更加清晰明了。

image.png

  1. 抽成useCounter.js,内聚性更强,成为独立的逻辑。
import { ref } from 'vue';

export default function useCounter() {
    const counter = ref(0);

    const increment = () => {
        counter.value++;
    }

    const decrement = () => {
        counter.value--;
    }

    return {
        counter,
        increment,
        decrement,
    }
}

image.png

四.修改网页标题

1.普通修改

image.png

2.封装hooks修改

image.png

六.获取window的滚动位置

来完成一个监听界面滚动位置的Hook: image.png

七.Setup语法糖

1. <script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时则推荐该语法。
  • 更少的样板内容,更简洁的代码;
  • 能够使用纯 Typescript 声明 prop 和抛出事件;
  • 更好的运行时性能 ;
  • 更好的 IDE 类型推断性能 ;
2. 使用这个语法,需要将 setup attribute 添加到 <script> 代码块上:
<script setup>
import { ref } from 'vue'
const message = ref("hello world")
</script>
3. 里面的代码会被编译成组件 setup() 函数的内容:
  • 这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同;
  • <script setup> 中的代码会在每次组件实例被创建的时候执行

image.png

4. 顶层的绑定会被暴露给模板:

当使用<script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容)都能在模板中直接使用

   <div>{{ message }}</div>
    <show-info></show-info>
</template>

<script setup>
//组件引入直接使用
import showInfo from './showInfo.vue';
import { ref } from 'vue'
// 所有编写在顶层的代码,都是默认暴露给template使用的
const message = ref("hello world")
</script>
<style scoped></style>

5. 默认在函数作用域中存在 defineProps() 和 defineEmits()

为了在声明 props 和 emits 选项时获得完整的类型推断支持,我们可以使用 defineProps 和 defineEmits API,它们将自动在<script setup>可用。

6. 使用Setup时,
  • 父传子

image.png

  • 子传父

image.png

7. defineExpose()
  1. 使用 <script setup> 的组件是默认关闭的:
  • 通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在
  • 通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的 property:

image.png