Vue组合API


Composition API

  • 组件逻辑复用
  • 逐步应用
  • Options API共存

为什么要使用Composition API?

在使用Options API时,我们必须反复在不同的配置项中跳跃进行代码阅读。

而使用Composition API后,我们可以将组件的所有代码写在一个setup()函数中,在其中我们可以自由地调整代码位置,让同一功能的代码放在一起。同时我们也可以抽离其中的函数放置在单独的文件中,让组件直接使用抽离出的函数,让组件代码更加简洁。

Composition API的入口——setup()函数:

注意:setup()函数只是提供了响应式组件的替代方式,例如data()computed()methods()等。像静态的components,props,emits等组件配置项,还是使用原来的方式,不过之后还是会有替代的方式。

示例:

组件MessageList.vue文件:

<template>
  <ul>
    <li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
  </ul>
</template>
<script>
export default {
  setup() {
    const messages = [
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ];

    return { messages };
  },
};
</script>

主文件App.vue文件:

<template>
  <main>
    <div class="container">
      <MessageList />
    </div>
  </main>
</template>

<script>
import MessageList from "./components/MessageList.vue";

export default {
  components: {
    MessageList,
  },
  setup() {},
};
</script>

如何在setup()函数中定义响应式数据?

Vue提供了一组用于在setup()函数中定义响应式数据的函数,这些响应性数据也可称为状态

也就是说,随着用户使用,应用的响应式数据会根据事件的发生而发生变化,从而引起组件的刷新,也就是状态的改变。

ref()函数:

ref()函数接收一个参数,可以是任意类型,它会把他们包装成响应式数据,作用就是替代了data()配置项

import {ref} from 'vue';
const num = ref(0);			//数字
const str = ref("字符串");   //字符串
const arr = ref([0,1,2]);	//数组
const obj = ref({a: 1, b: 2});  //对象

注意:如果传给ref()函数的是一个对象,那么对象的所有属性包括嵌套的属性都会转换为响应式的属性。

setup()函数中,必须通过其value属性来访问其包装的响应式数据,但和原始数值不是全相等的,原理以后再说。而在模板中不需要使用value属性来访问。

示例:

<template>
  <ul>
    <li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
    <button @click="messages = []"></button>
  </ul>
</template>
<script>
import {ref} from "vue"
export default {
  setup() {
     // 使用ref()让其他依赖响应式数据的代码进行更新
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);
    console.log(messages.value);

    return { messages };
  },
};
</script>

reactive()函数:

reactive()函数与ref()函数类似,但它只接收一个对象类型的函数作为参数。这里的对象类型是广义的,包括数组。

reactive()函数在setup()函数中可以直接访问,不需要使用value属性。

使用ref()函数时,其内部也会调用reactive()函数,把对象的所有属性转换为响应式数据,之后将转换后的值赋值给value属性。

如何选择ref()函数和reactive()函数呢?

通常情况下直接使用ref()函数,因为它能直接定义基本数据,而且使用ref()函数定义的数据比较分散,所以更容易抽离成可复用的;

reactive()函数则适用于一次性定义多个响应式数据的情况,将它们放置在一个对象中,之后通过该对象修改和访问其中的属性,适合存放组件的配置属性或表单数据。

示例:

<template>
  <div>
    <h2>{{ options.title }}</h2>
    <p>
      用户:{{ options.user.name }},活跃:{{
        options.user.active ? "是" : "否"
      }}
    </p>
    <ul>
      <li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
    </ul>
    <button @click="messages = []">删除全部</button>
    <button @click="options.title = '这是标题'">修改标题</button>
    <button @click="options.user.name = '李四'">修改用户</button>
  </div>
</template>
<script>
import { ref, reactive } from "vue";

export default {
  setup() {
    // const messages = reactive([
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    // const options = ref({
    const options = reactive({
      title: "消息列表",
      user: {
        name: "张三",
        active: true,
      },
    });

    console.log(options);

    return { messages, options };
  },
};
</script>

computed()函数:

示例:

<template>
  <div>
    <!-- v-model 可以直接绑定 ref,与 data 配置项等效 -->
    <input type="text" placeholder="搜索消息" v-model="searchTerm" />
    <ul>
      <li v-for="msg in searchedMessages" :key="msg.id">{{ msg.content }}</li>
    </ul>
  </div>
</template>
<script>
import { ref, computed } from "vue";

export default {
  setup() {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    const searchTerm = ref("");

    const searchedMessages = computed(() => {
      if (searchTerm.value === "") return messages.value;
      return messages.value.filter((msg) => {
        return msg.content.includes(searchTerm.value);
      });
    });

    console.log(searchedMessages.value);

    return { searchTerm, searchedMessages };
  },
};
</script>

watch()函数:

监听响应式数据的变化:
  1. 直接监听响应式数据:

    watch(searchTerm, (newVal, oldVal) => {
         console.log(messages.value);
         console.log("搜索词:", newVal, oldVal);
       });
  2. 监听解剖后的响应式数据:

    // 直接监听 searchTerm 不可以
        watch(searchTerm.value, (newVal, oldVal) => {
          console.log("搜索词:", newVal, oldVal);
        });
        // 需要使用一个函数
        watch(
          () => searchTerm.value,
          (newVal, oldVal) => {
            console.log("搜索词:", newVal, oldVal);
          }
        );
监听对象中基本类型的响应式属性:

示例:

<template>
  <div>
    <h2>{{ options.title }}</h2>
    <p>
      用户:{{ options.user.name }},活跃:{{
        options.user.active ? "是" : "否"
      }}
    </p>
    <ul>
      <li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
    </ul>
    <button @click="options.title = '这是标题'">修改标题</button>
    <button @click="options.user.name = '李四'">修改用户</button>
  </div>
</template>
<script>
import { ref, reactive, computed, watch } from "vue";

export default {
  setup() {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    const options = ref({
      // const options = reactive({
      title: "消息列表",
      user: {
        name: "张三",
        active: true,
      },
    });

    // 监听浅层 Object 属性
    watch(
      () => options.value.title,
      // () => options.title, // reactive
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );

    // 监听深层 Object 属性
    watch(
      () => options.value.user.name,
      // () => options.user.name, // reactive
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );
    return { messages, options };
  },
};
</script>
监听对象类型的响应式属性:

注意:直接监听value属性,watch()函数不会监听到变化。因为在监听整个对象的时候,watch()比较的是对象的引用,每次修改对象的属性都不会创建新的对象,而是在原对象中进行修改,所以watch()函数监听不到。数组同理

  1. 配置deep: true

    缺点:不能访问修改前的值

    示例:

    // with deep true,可以比对对象的属性
        watch(
          () => options.value,
          (newVal, oldVal) => {
            console.log(newVal, oldVal, newVal === oldVal); // 相同的引用
          },
          { deep: true }
        );
  2. 使用扩展运算符...

    示例:

     export default {
      setup() {
        const messages = ref([
          { id: 1, content: "这是一条消息提醒1" },
          { id: 2, content: "这是一条消息提醒2" },
          { id: 3, content: "这是一条消息提醒3" },
          { id: 4, content: "这是一条消息提醒4" },
        ]);
    
        const options = ref({
          // const options = reactive({
          title: "消息列表",
          user: {
            name: "张三",
            active: true,
          },
        });
    	watch(
          () => ({ ...options.value }),
          (newVal, oldVal) => {
            console.log(newVal, oldVal, newVal === oldVal); // 相同的引用
          },
          { deep: true }
        );
       
        return { messages, options };
      },
    };
    </script>

    只使用扩展运算符,并不会监听到子对象的值,如user.name。即使设置了deep: true,监听到的对象也是相同的引用。

    因为...语法创建的对象是浅拷贝,只会复制顶层的属性,子对象会原封不动的把引用传给新的对象。

    可使用JSON相关的方法进行深拷贝:

    () => JSON.parse(JSON.stringify(options.value)); // 对象转为字符串再转为对象
同时监听多个响应性数据:

watch()函数支持使用数组进行监听:

// 同时监听多个响应性数据
   watch(
     [() => options.value.title, () => options.value.user.name],
     (newVals, oldVals) => {
       console.log(newVals, oldVals); // 其中的数据也是数组形式
     }
   );

watchEffect()函数:

watchEffect()watch()的作用基本一样,用于监听响应式数据的变化,并根据变化做一些业务逻辑,例如请求远程服务数据。

区别:

  1. watchEffect()函数不用明确指定标签中的响应式数据,而是会根据回调函数中的代码自动判断。如果代码中用到了响应式数据,无论多少个,只要其中的一个发生了变化,watchEffect()函数就会重新执行一次。
  2. watchEffect()函数无论数据是否发生了变化,都会先执行一次回调函数
  3. watchEffect()不能访问修改前的值,回调函数中的响应式的值都是修改后的

示例:

<template>
  <div>
    <h2>{{ options.title }}</h2>
    <p>
      用户:{{ options.user.name }},活跃:{{
        options.user.active ? "是" : "否"
      }}
    </p>
    <ul>
      <li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
    </ul>
    <button @click="options.title = '这是标题'">修改标题</button>
    <button @click="options.user.name = '李四'">修改用户</button>
  </div>
</template>
<script>
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    const options = ref({
      // const options = reactive({
      title: "消息列表",
      user: {
        name: "张三",
        active: true,
      },
    });

    watchEffect(() => {
      console.log(options.value.title);
      console.log(options.value.user.name);
    });

    return { messages, options };
  },
};
</script>

watch()&watchEffect() 清理收尾工作:

watchEffect()函数:
watchEffect((onInvalidate) => {
      console.log(options.value.title);
      console.log(options.value.user.name);

      onInvalidate(() => {
        console.log("做一些清理操作...");
      });
    });
watch()函数:
watch(
     () => options.value.title,
     (newVal, oldVal, onInvalidate) => {
       console.log(newVal, oldVal);
       onInvalidate(() => {
         console.log("做一些清理操作...");
       });
     }
   );

注意:onInvalidate()函数会在下次监听代码执行前执行

传递和访问Props属性:

如果需要在setup()函数中访问Props属性,需要在setup()函数中设置参数

示例:

子组件MessageList.vue文件:

<template>
  <li>{{ msg }}</li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";

export default {
  props: ["msg"],
  setup(props) {
    console.log(props.msg);

    return {};
  },
};
</script>

父组件MessageListItem.vue文件:

<template>
  <div>
    <ul>
      <MessageListItem
        v-for="msg in messages"
        :key="msg.id"
        :msg="msg.content"
      ></MessageListItem>
    </ul>
  </div>
</template>
<script>
import { ref, watch, watchEffect } from "vue";
import MessageListItem from "./MessageListItem.vue";

export default {
  components: { MessageListItem },
  setup(props) {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    return { messages };
  },
};
</script>

转换非响应性props为响应性:

setup()函数中的props属性整体是响应性的,相当于使用reactive()或使用ref()创建的对象的value属性,故可以使用watch()watchEffect()监听props中的属性变化。

但如果父组件传递给子组件的数据是非响应性的,如果使用解构语法拆解出来,那么是不会被监听到变化的,同样地类似computed()响应式属性也不会生效。

须调用Vue中的toRefs()函数,之后再解构。

示例:

<template>
  <li>{{ msg }}</li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";

export default {
  props: ["msg"],
  setup(props) {
    const { msg } = toRefs(props);
      // const  msg = toRefs(props, 'msg');
    watch(msg, (newMsg) => {
      console.log(newMsg);
    });

    return {};
  },
};
</script>

ref/reactive创建的数据在Props中的响应性:

注意:如果父组件传递的数据类型为JavaScript基本类型,例如字符串、数字、布尔类型等,他们在通过属性传递后就会失去响应性,在子组件中需要使用toRefs()函数进行转换。

换言之,只有对象数组类型的数据在传递的时候会保留其响应性。

使用ref()函数创建的数据在传递时,只会传递其value属性,在子组件中即为Proxy类型。

setup()函数中定义methods:

直接定义function:

示例:

<template>
  <li>{{ msg }} <button @click="removeMessage(id)">删除</button></li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";

export default {
  props: ["msg", "id"],
  setup(props) {
    // 无参数
    // function removeMessage() {
    //   console.log("删除消息");
    // }

    // // 有参数
    function removeMessage(id) {
      console.log("删除消息", id);
    }

    return { removeMessage };
  },
};
</script>

Emit自定义事件:

因为无法从setup()函数中访问this属性,setup()接收第二个参数context,其中一个属性就是emit函数。

context参数本身不是响应性的,所以可以直接使用解构赋值

示例:

子组件MessageListItem.vue文件:

<template>
  <!-- <button @click="removeMessage(id)">删除</button> -->
  <li>{{ msg }} <button @click="this.$emit('remove', id)">删除</button></li>
</template>
<script>
import { ref, watch, watchEffect, toRefs } from "vue";

export default {
  props: ["msg", "id"],
  emits: ["remove"],
  setup(props) {
    // setup(props, { emit }) {
    // 有参数
    // function removeMessage(id) {
    //   emit("remove", id);
    // }

    return {};
    // return { emit };
  },
};
</script>

父组件MessageList.vue文件:

<template>
  <div>
    <ul>
      <MessageListItem
        v-for="msg in messages"
        :key="msg.id"
        :id="msg.id"
        :msg="msg.content"
        @remove="removeMessage"
      ></MessageListItem>
    </ul>
  </div>
</template>
<script>
import { ref, watch, watchEffect } from "vue";
import MessageListItem from "./MessageListItem.vue";

export default {
  components: { MessageListItem },
  setup(props) {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    function removeMessage(id) {
      // console.log(id);
      messages.value = messages.value.filter((msg) => msg.id !== id);
    }

    return { messages, removeMessage };
  },
};
</script>

生命周期钩子:

Options API不同,Composition API中的生命周期钩子需要加上on前缀,后面对应的生命周期首字母大写:

onMounted(()=>{
    //业务代码
})

image-20220904171819752

image-20220904172051743

没有beforeCreate和created对应生命周期钩子的原因是setup()函数本身就是在这两个生命周期期间执行的,所以在这两个生命周期的自定义业务逻辑,直接在setup()函数中编写即可。

示例:

<template>
  <div>
    <div v-if="loading">loading...</div>
    <ul v-else>
      <MessageListItem
        v-for="msg in messages"
        :key="msg.id"
        :msg="msg.content"
      ></MessageListItem>
    </ul>
  </div>
</template>
<script>
import { ref, watch, watchEffect, onMounted } from "vue";
import MessageListItem from "./MessageListItem.vue";

export default {
  components: { MessageListItem },
  setup(props) {
    const messages = ref([]);
    const loading = ref(false);

    onMounted(() => {
      loading.value = true;
      setTimeout(() => {
        messages.value = [
          { id: 1, content: "这是一条消息提醒1" },
          { id: 2, content: "这是一条消息提醒2" },
          { id: 3, content: "这是一条消息提醒3" },
          { id: 4, content: "这是一条消息提醒4" },
        ];
        loading.value = false;
      }, 2000);
    });

    return { messages, loading };
  },
};
</script>

Provide和Inject:

  1. 普通数据:

    provide('propName', value);
    inject('propName');
  2. 响应式数据:

    如果使用provide提供的数据本身是响应性的,那么inject接收的数据就是响应性的;

    如果不是,则可以调用ref()reactive()toRef()toRefs()转换为响应性数据:

    <template>
      <div class="card">
        <!-- <MovieItem :title="movie.title" :description="movie.description" /> -->
        <MovieItem :description="movie.description" />
      </div>
    </template>
    <script>
    import MovieItem from "./MovieItem.vue";
    import { ref, provide, toRef } from "vue";
    export default {
      components: {
        MovieItem,
      },
      setup() {
        const movie = ref({
          title: "电影",
          description: "这是一段电影的描述",
        });
    
        provide("movie", movie);
    
        // provide("title", toRef(movie.value, "title"));
    
        setTimeout(() => {
          movie.value.title = "电影-修改";
        }, 1500);
    
        return { movie };
      },
    };
    </script>

获取Template Ref:

即相当于在Options API中获取DOM和Vue组件实例:

示例:

<template>
  <input type="text" v-model="inputText" ref="inputControl" />
</template>

<script>
import { onMounted, ref } from "vue";

export default {
  setup() {
    const inputText = ref("");
    const inputControl = ref(null);

    onMounted(() => {
      inputControl.value.focus();
    });

    return { inputText, inputControl };
  },
};
</script>

获取非props属性:

使用attrs可以获取在props中未明确定义的属性:

示例:

子组件MessageList.vue文件:

<template>
  <div>
    <ul>
      <MessageListItem
        v-for="msg in messages"
        :key="msg.id"
        :msg="msg.content"
      ></MessageListItem>
    </ul>
  </div>
</template>
<script>
import { ref, watch, watchEffect, isRef } from "vue";
import MessageListItem from "./MessageListItem.vue";

export default {
  components: { MessageListItem },
  setup(props, { attrs }) {
    const messages = ref([
      { id: 1, content: "这是一条消息提醒1" },
      { id: 2, content: "这是一条消息提醒2" },
      { id: 3, content: "这是一条消息提醒3" },
      { id: 4, content: "这是一条消息提醒4" },
    ]);

    console.log(attrs);
    console.log(attrs.class);
    console.log(attrs["data-title"]);

    // 拆解出来,不再具有响应性
    // const { test } = attrs;

    // watchEffect(() => {
    //   console.log(test, " in MessageList.vue");
    // });

    watchEffect(() => {
      console.log(attrs.test, " in MessageList.vue");
    });

    // console.log(attrs);
    // console.log(attrs.class);

    return { messages };
  },
};
</script>

父组件App.vue文件:

<template>
  <main>
    <div class="container">
      <MessageList class="messageList" :test="test" data-title="消息列表" />
    </div>
  </main>
</template>

<script>
import MessageList from "./components/MessageList.vue";
import { ref } from "vue";

export default {
  components: {
    MessageList,
  },
  setup() {
    const test = ref("test");

    setTimeout(() => {
      test.value = "changed";
    }, 2000);

    return { test };
  },
};
</script>

使用script setup进一步简化组件代码:

使用配置:

<script setup>

</script>
  • script setup中定义的函数及变量等,可以直接在模板中使用,无需手动return

  • script setup可以使用import导入库或其他组件,并在模板中直接使用,无需配置。

  • script setup中如果需要使用props,则可以调用defineProps( [ "propsName"] )函数。

  • script setup中如果需要使用emits,则可以调用defineEmits( [ "emitsName"] )函数。

  • script setup中如果需要使用slots和attrs:

    import { useSlots, useAttrs} from 'vue';
    const slots = useSlots();
    const attrs = useAttrs();

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录