Vue3新特性

84 阅读2分钟

本文是Vue3学习记录,适合Vue3初学者,大牛可以跳过。

目前完成部分,持续更新。完整代码见 代码仓库

Vue3相比Vue2的优势

  • 更快 Proxy实现响应式;diff算法优化,添加PatchFlag;hoistStatic静态提升;cacheHandler事件监听缓存;SSR优化

  • 更小 移除一些不常用的API;tree-shaking,仅打包需要的

  • 更好 组合式API,提高代码组织、逻辑复用、可读性、维护性;TypeScript支持;Fragments、Teleport、Suspense等

选项式和组合式API

Vue2 是选项API(Options API),选项包括data、props、methods、computed、watch、生命周期钩子等,一个逻辑的代码分散在各处,导致代码的可读性变差。

Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性。

生命周期

setup函数

  • 在组件中使用组合式 API 的入口

  • 只会在组件初始化的时候执行一次

  • 在beforeCreate之前执行

在这里插入图片描述

生命周期

  • Vue3中可以使用Vue2的生命周期,但beforeDestroy改为beforeUnmount,destroyed改为 unmounted
  • Vue3大部分生命周期名称是在Vue2的前面加 + “on”
vue2vue3
beforeCreate不需要
created不需要
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

screenshots.gif

OptionsDemo.vue:

<template>
  <div>
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
export default {
  props: ["type"],
  data(props) {
    return {
      msg: `${props.type} API`,
    };
  },
  beforeCreate() {
    console.log(this.type, "beforeCreate");
  },
  created() {
    console.log(this.type, "created");
  },
  beforeMount() {
    console.log(this.type, "beforeMount");
  },
  mounted() {
    console.log(this.type, "mounted");
    // 1s后触发组件更新
    setTimeout(() => {
      this.msg += " 生命周期";
    }, 1000);
  },
  beforeUpdate() {
    console.log(this.type, "beforeUpdate");
  },
  updated() {
    console.log(this.type, "updated");
  },
  // beforeDestroy 改名
  beforeUnmount() {
    console.log(this.type, "beforeUnmount");
  },
  // destroyed 改名
  unmounted() {
    console.log(this.type, "unmounted");
  },
};
</script>

<style lang="scss" scoped>
</style>

CompositionDemo.vue:

<template>
  <div>
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";

export default {
  props: ["type"],
  setup(props) {
    const type = props.type
    const msg = ref(`${type} API`);

    // beforeCreate和created之前执行
    console.log(type, "setup");

    onBeforeMount(() => {
      console.log(type, "onBeforeMount");
    });
    onMounted(() => {
      console.log(type, "onMounted");
      // 1s后触发组件更新
      setTimeout(() => {
        msg.value += " 生命周期";
      }, 1000);
    });
    onBeforeUpdate(() => {
      console.log(type, "onBeforeUpdate");
    });
    onUpdated(() => {
      console.log(type, "onUpdated");
    });
    onBeforeUnmount(() => {
      console.log(type, "onBeforeUnmount");
    });
    onUnmounted(() => {
      console.log(type, "onUnmounted");
    });

    return {
      msg,
      type,
    };
  },
  beforeCreate() {
    console.log("composition beforeCreate");
  },
  created() {
    console.log("composition created");
  },
};
</script>

<style lang="scss" scoped>
</style>

上面两个的父组件IndexDemo:

<template>
  <div>
    <OptionsDemo type='options' />
    <CompositionDemo type='composition' />
  </div>
</template>

<script>
import OptionsDemo from "./OptionsDemo.vue";
import CompositionDemo from "./CompositionDemo.vue";
export default {
  name: "Api-style",
  components: {
    OptionsDemo,
    CompositionDemo,
  },
};
</script>

<style lang="scss" scoped>
</style>

实例化

Vue2是new Vue({}),Vue3是createApp()

其他

  • Vue3移除了filter
  • setup中使用getCurrentInstance获取this, 使用getCurrentInstance().appContext.config.globalProperties获取全局属性
  • 获取全局app上的属性通过app.config.globalProperties
  • 很多接口由Vue.XXX变成了app.XXX,比如注册组件 Vue.component变成app.component,自定义指令Vue.directive变成app.directive

响应式

ref

  • 生成值类型的响应式数据
  • 除了在模板和reactive中使用外,其他地方都要用.value读取、修改值

RefDemo.vue

<template>
  <div>
    <h2>{{ msg }}</h2>
    count: {{ count }} <button @click="countClickHandler">+1</button>
  </div>
</template>

<script>
// import { ref } from "vue";
// export default {
//   setup() {
//     const msg = ref("Ref Demo");
//     const count = ref(0);

//     const countClickHandler = () => {
//       count.value++;
//     };
//     return {
//       msg,
//       count,
//       countClickHandler,
//     };
//   },
// };
//
</script>

<script setup>
import { ref } from "vue";

const msg = ref("Ref Demo");
const count = ref(0);

const countClickHandler = () => {
  count.value++;
};
</script>

<style lang="scss" scoped>
</style>

reactive

  • 生成对象、数组类型的响应式数据
  • 解构后的值,不具有响应式

ReactiveDemo.vue:

<template>
  <div>
    <h2>{{ msg }}</h2>
    <p>
      {{ user.name }} {{ user.age }} {{ user.gender === "m" ? "男" : "女" }}
    </p>
  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
const msg = ref("Reactive Demo");
const ageRef = ref(20);
const user = reactive({
  name: "jim",
  gender: "m",
  age: ageRef,
});

setTimeout(() => {
  console.log("age++");
  ageRef.value++;
}, 2000);
</script>

<style lang="scss" scoped>
</style>

toRef

  • 针对一个响应式对象的prop,创建一个ref,具有响应式
  • 该ref和响应式对象的prop保持引用关系

ToRefDemo.vue:

<template>
  <div>
    <h2>{{ msg }}</h2>
    <p>
      {{ user.name }} {{ user.age }} {{ user.gender === "m" ? "男" : "女" }}
    </p>
    <p>age+1:{{ AgePlusOne }}</p>
  </div>
</template>

<script setup>
import { ref, reactive, toRef, computed, watch, watchEffect } from "vue";
const msg = ref("ToRef Demo");
const user = reactive({
  name: "jim",
  gender: "m",
  age: 20,
});

const age = toRef(user, "age");
setTimeout(() => {
  console.log("age++");
  age.value++;
}, 2000);

const AgePlusOne = computed(() => {
  return user.age + 1;
});

watch(() => user.age, (val, oldVal) => {
  console.log("watch age triggered.", val, oldVal);
});

watchEffect(() => {
  console.log("watchEffect age triggered.", user.age);
});
</script>

<style lang="scss" scoped>
</style>

toRefs

  • 针对响应式对象的每个prop都创建对应ref,具有响应式
  • 保持引用关系

ToRefsDemo.vue:

<template>
  <div>
    <h2>{{ msg }}</h2>
    <p>{{ name }} {{ age }} {{ gender === "m" ? "男" : "女" }}</p>
  </div>
</template>

<script setup>
import { ref, reactive, toRefs } from "vue";
const msg = ref("ToRefs Demo");
const user = reactive({
  name: "jim",
  gender: "m",
  age: 20,
});

setTimeout(() => {
  console.log("age++");
  user.age++;
  user.name += '又老了一岁'
}, 2000);

// 注意:直接解构响应式对象得到的属性不是响应式的,以下写法2s后age++页面不更新
// const { name, age, gender } = user;

const { name, age, gender } = toRefs(user);
</script>

<style lang="scss" scoped>
</style>

screenshots.gif

v-model参数

vue2中v-model原理和实现

ChildComp.vue:

<template>
  <input :value="name" @input="emit('update:name', $event.target.value)" />
  <input
    type="number"
    min="0"
    max="200"
    :value="age"
    @input="emit('update:age', $event.target.value)"
  />
</template>

<script setup>
import { defineProps, defineEmits, toRefs } from "vue";

const props = defineProps({
  name: {
    type: String,
    default: "",
  },
  age: {
    type: [Number, String],
    default: 20,
  },
});

const emit = defineEmits({
  "update:name": null,
  // age校验
  "update:age": (val) => {
    if (Number(val) >= 0 && Number(val) <= 200) {
      return true;
    }
    setTimeout(() => {
      alert('age check failed!!!');
    }, 200);
    return false;
  },
});

const { name, age } = toRefs(props);
</script>

<style lang="scss" scoped>
</style>

上面组件的父组件IndexDemo.vue:

<template>
  <h2>{{ msg }}</h2>
  <p>name: {{ name }} age: {{ age }}  <button @click="resetAge">修改age</button></p>
  <!-- 两种写法等价 -->
  <ChildComp v-model:name="name" v-model:age="age"></ChildComp>&nbsp;
  <ChildComp
    v-model:name="name"
    :age="age"
    @update:age="age = $event"
  ></ChildComp>
</template>

<script setup>
import { reactive, toRefs, ref } from "vue";
import ChildComp from "./ChildComp.vue";
const user = reactive({
  name: "chang bai",
  age: 20,
});

const msg = ref("v-model参数");
const { name, age } = toRefs(user);

const resetAge = () => {
  age.value = 20;
};
</script>
<script>
export default {
  components: {
    ChildComp,
  },
};
</script>

screenshots.gif

接口相关

screenshots.gif

Fragment

Vue2模板必须有个根节点,Vue3不强求,模板可以有多个根节点(或称他们为兄弟节点,没有父节点)。

FragmentDemo.vue:

<template>
  <h2>{{ msg }}</h2>
  <p>name: {{ name }} age: {{ age }}</p>
</template>

<script setup>
import { ref, reactive, toRefs } from "vue";

const msg = ref("Fragment Demo");
const user = reactive({
  name: "chang bai",
  age: 20,
});

const { name, age } = toRefs(user);
</script>

<style lang="scss" scoped>
</style>

Teleport

传送门,可以将当前组件内部分DOM节点移动到指定位置,比如body,Dialog就是这种。

TeleportDemo.vue:

<template>
  <h2>{{ msg }}</h2>
  <teleport to="body" v-if="isOpen">
    <div class="mask" @click="toggleOpen">
      <div class="center" @click.stop="">
        <div class="header">这是header</div>
        <div class="main">点击mask关闭</div>
        <div class="footer">这是footer</div>
      </div>
    </div>
  </teleport>
  <p>
    isOpen: {{ isOpen }}
    <button @click="toggleOpen">{{ isOpen ? "关闭" : "打开" }}</button>
  </p>
</template>

<script setup>
import { ref, reactive, toRefs } from "vue";

const msg = ref("Teleport Demo");
const isOpen = ref(false);

const toggleOpen = () => {
  isOpen.value = !isOpen.value;
};
</script>

<style scoped>
.mask {
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.6);
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}
.center {
  width: 300px;
  height: 200px;
  border-radius: 10px;
  background: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.header,
.footer {
  padding: 20px;
  width: 100%;
  box-sizing: border-box;
  text-align: center;
}
.main {
  flex: 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

Suspense

异步组件,配合defineAsyncComponent使用,可以在加载完成前渲染自定义内容,比如loading。

FetchDataDemo.vue:

<template>
  <div>{{ mockData }}</div>
</template>

<script>
export default {
  async setup(props) {
    const fn = async () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("获取数据成功!");
        }, 2000);
      });
    };
    const mockData = await fn()
    return {
      mockData,
    };
  },
};
</script>

<style lang="scss" scoped>
</style>

上面组件的父组件SuspenseDemo.vue:

<template>
  <h2>{{ msg }}</h2>
  <Suspense>
    <!-- 加载完成渲染的内容 -->
    <template #default>
      <DialogDemo></DialogDemo>
    </template>
    <!-- 加载中显示的内容 -->
    <template #fallback>
      <span>{{ loadingInfo }}</span>
    </template>
  </Suspense>
</template>

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

const DialogDemo = defineAsyncComponent(() => import("./FetchDataDemo.vue"));
const msg = ref("Suspense Demo");
const loadingInfo = ref("加载中...");
</script>

<style lang="scss" scoped>
</style>

代码仓库