vue3基础知识梗概

写在前面:阅读本文需要掌握 vue2,本文不会特别回顾 vue2 中的知识

Composition API

1. setup

  1. 组件中所用到的数据,方法等都需要配置在 setup 中.
  2. setup 函数的两种返回值:
    • 若返回一个对象,则对象中的属性,方法在模板中均可以直接使用
    • 若返回一个渲染函数(h),则可以自定义渲染内容
  3. 注意点
    • 尽量不要与 vue2 混用
    • 在非异步组件中 setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 promise,在模板中就无法使用了

2. ref

  1. 作用: 定义一个响应式数据
  2. 语法: const xxx = ref(value)
    • 创建一个包含响应式数据的引用对象
    • 操作数据需要使用xxx.value
    • 模板中插值表达式不需要使用.value
  3. 备注
    • 接受的数据可以是基本类型也可以是对象.
    • 基本类型数据的响应式仍然是使用的Object.defineProperty()的 set get 完成的
    • 对象类型属性的响应式使用了 reactive 函数,本质是 Proxy

3. reactive 函数

  1. 作用: 专用于定义一个对象类型的响应式数据
  2. 语法: const 代理对象 = reactive(源对象)接受一个对象(数组),返回一个代理对象(Proxy)
  3. reactive 的响应式数据是深层次的.不用再像 vue2 一样要使用Vue.set()来进行更新对象
  4. 是基于 ES6 的 Proxy 实现的,通过代理对象操作源对象内部数据进行操作

4. 响应式原理

  • 通过 Proxy 拦截对象中任意属性的变化.Proxy 可以理解成在目标对象之前架设防火墙,外界对该对象的访问都必须经过防火墙.因此这就提供了过滤功能,这里用来表示由它来代理某些操作,称为代理器.

  • 通过 Reflect 对对象的属性进行操作.Reflect 将 Object 对象的一些明显属于语言内部的方法(如Object.defineProperty)放到 Reflect 对象上.某种意义上来讲,Reflect 就是另一种写法而已.但未来的新方法将之不是在 Reflect 对象上.也就是说,从 Reflect 对象上可以拿到语言内部的方法.

let person = {
  _name: "Json",
  age: 18,
};
let handler = {
  // get捕获读取操作
  get(origin, prop) {
    if (Reflect.has(origin, prop)) {
      return Reflect.get(origin, prop);
    } else {
      throw new ReferenceError("不存在该属性");
    }
  },
  // set捕获赋值操作
  set(origin, prop, value) {
    if (prop[0] === "_") {
      throw new Error("只读属性");
    }
    if (prop === "age") {
      if (!Number.isInteger(value)) throw new TypeError("请输入整数");
      if (value > 200) throw new RangeError("数字太大");
    }
    return Reflect.set(origin, prop, value);
  },
  // deleteProperty捕获删除操作
  deleteProperty(origin, prop) {
    console.log(`删除${prop}属性`);
    return Reflect.deleteProperty(origin, prop);
  },
};
let proxy = new Proxy(person, handler);
console.log(proxy._name); // Json
console.log(delete proxy.age); // true

上面的代码是一个小 demo,示例了下 Proxy 以及 Reflect 的使用.如果想要学习这两个,请查看阮一峰的 ES6 教程

Proxy 教程

Reflect 教程


5. setup 注意点

  • setup 在beforeCreate之前执行,没有this
  • setup 的参数
    • props:值为对象,包含组件外部传递过来且组件内部声明接受了的属性
    • context:上下文对象
      • attrs:值为对象,包含组件外部传递过来但没有在 props 配置中声明的属性,相当于 vue2 中的this.$attrs
      • slots:收到的插槽内容,相当于 vue2 中的this.$slots
      • emit:分发自定义事件的函数,相当于 vue2 中的this.$emit

6. 计算属性以及监视

computed

<script lang="ts" setup>
import { computed, reactive, WritableComputedRef } from "vue";
interface p { // 定义Person要使用的接口
  firstName: string; // 一般字符串类型
  lastName: string; // 一般字符串类型
  fullName: WritableComputedRef<string>; // computed计算属性的数据类型
}
let person: p = reactive({ // 使用reactive实现深度监听
  firstName: "",
  lastName: "",
  fullName: null,
});
// 对象.属性 = computed()的方式实现计算属性 参数可以为对象或者函数,内容与vue2一致
person.fullName = computed({
  set(value: string) { // 捕获更改
    const valueArr: Array<string> = value.split(" ");
    person.firstName = valueArr[0];
    person.lastName = valueArr[1];
  },
  get() {
    if (person.firstName !== "" && person.lastName !== "") {
      return person.firstName + " " + person.lastName;
    }
  },
});
</script>

上面的代码可以实现一个姓+名拼接成全名的表单,并且还能通过更改全名的方式直接更改姓或名

watch

  1. 监听 ref 响应式数据
const a = ref(0);
watch(a, (newVal, oldVal) => {
  console.log("a被改变了");
});
  1. 监听 ref 多个数据
const b = ref(100);
const c = ref(0);
watch([b, c], (newVal, oldVal) => {
  console.log("b或c变化了", newVal, oldVal);
});
  1. 监听 reactive 对象
interface per {
  name: string;
  age: number;
}
const person: per = reactive({
  name: "Tom",
  age: 12,
});
watch(
  person,
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  },
  { deep: false }
); // deep:false不生效

这里需要注意的是,监听 reactive 定义的响应式对象,是无法取消深度监听的,并且你还会发现无法正确获取到老值oldVal

想要获取到oldVal应该使用computed去缓存数据

const person = reactive({
  name: "Json",
  age: 19,
});
let P = computed(() => {
  return JSON.parse(JSON.stringify(person));
});
watch(P, (newVal, oldVal) => {
  console.log(newVal, oldVal); // 能够正确获取到oldVal
});
  1. 监听对象中的单个数据
interface per {
  name: string;
  age: number;
}
const person: per = reactive({
  name: "Tom",
  age: 12,
});
watch(
  () => person.age,
  (newVal, oldVal) => {
    console.log("年龄变化了");
  }
);

第一个参数变成了一个具有返回值的函数,返回值为需要监听的属性,使用箭头函数的写法就显得非常简洁了

  1. 监听对象中的一些数据
interface per {
  name: string;
  age: number;
}
const person: per = reactive({
  name: "Tom",
  age: 12,
});
watch([() => person.name, () => person.age], (newVal, oldVal) => {
  console.log();
});

第一个参数成了数组,数组内部有多个函数返回值,返回值为要监听的属性

  1. 监听 reactive 对象中的普通对象
interface per {
  name: string;
  age: number;
  job: wo;
}
interface wo {
  work: string;
}
const person: per = reactive({
  name: "Sam",
  age: 24,
  job: {
    work: "front-end",
  },
});
watch(
  () => person.job,
  (newVal, oldVal) => {
    console.log("job发生变化");
  },
  { deep: true }
); // deep:true不能去掉

需要注意的是deep:true不能去掉,因为在这里监听的是一个普通的对象,所以 deep 配置有效

watchEffect 函数

  • watch:既要指明监视的属性,也要指明回调.

  • watchEffect:不用直接指明监视的属性,在回调中使用到哪个属性就监视哪个属性,并且在初始化的时候立即执行一次(相当于 watch 里的 immediate:true).

  • watchEffect 与 computed 函数的区别

    • computed 更注重返回的值,computed 函数必须要有返回值.
    • watchEffect 更注重函数的执行,可以没有返回值.
watchEffect(() => {
  const x = sum.value; // 监视ref定义的sum
  const y = person.age; // 监视reactive定义的person对象的age属性\
  //...逻辑
});

7. 生命周期

在 vue3 中可以继续使用 vue2 声明生命周期的方式.但是有两个被更名

  • beforeDestroy() => beforeUnmount()
  • destroyed() => unmounted()

vue3 中也提供了组合式 API 中声明的方式(就是在setup()中写的),与 vue2 中的钩子对应关系如下

  • beforeCreate() = setup() 因此就不需要声明了
  • created() = setup() 因此就不需要声明了
  • beforeMount() = onBeforeMount()
  • mounted() = onMounted()
  • beforeUpdate() = onBeforeUpdate()
  • updated() = onUpdated()
  • beforeUnmount() = onBeforeUnmount()
  • unmounted() = onUnmounted()

8. 自定义 hook 函数

hook 本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装.

类似于 vue2 中的 mixin.

自定义 hook 带来的优势:复用代码,让 setup 内部结构更简单更易懂.

<script setup lang="ts">
  import usePoint from "../hooks/usePoint"; // 引用
  const point = usePoint(); // 使用自定义hooks
</script>

9. toRef && toRefs

引子:

setup(){
  const person = reactive({
    name: "Tom",
    age: 23,
    job: {
      name: "front-end",
      salary: 30,
    },
  });
  return{
    name:person.name,
    age:person.age
  }
}

上面的代码在页面中可以使用插值表达式来表达出name以及age,但是这个是不能更改的,是不具有响应式的.因为person.name的本质就是一个字符串,怎么进行响应式?

这就引出 toRef 了

toRef

作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性.

语法:const name = toRef(obj,"propName")

应用:要将响应式对象中的某个属性单独提供给外部使用时

setup(){
  //...
  return {
    name:toRef(person, "name"),
    age:toRef(person, "age")
  }
}

这样做的好处是,在模板中可以直接写{{name}},{{age}}来获取到值,并且还能对其做出改变.

toRefs

toRef功能一致,但可以批量创建多个 ref 对象

语法:toRefs(obj)

用法:return 的时候直接...toRefs(obj)即可

其他 Composition API

shallowReactive && shallowRef

shallowReactive函数返回的数据只处理对象最外层的响应式(浅响应).

shallowRef函数返回的数据只处理基本数据类型的响应式.

什么时候使用?

  • 如果有一个对象数据,结构很深,但变化时只是外层数据发生变动,此时应该使用shallowReactive.
  • 如果有一个对象数据,后续不会修改里面的属性,而是直接替换整个对象,此时应该使用shallowRef.

readonly && shallowReadonly

readonly接收一个数据,函数返回的数据是只读的,并且是深层次的只读

shallowReadonly接收一个数据,函数返回的数据是浅层只读的,只有最外层的数据是只读的

用法: const xxx = readonly(person)

后续中转变量xxx的属性就不能被更改了,当然原对象person是能更改的,只是xxx不能被更改.

toRaw && markRaw

toRaw将一个由reactive生成的响应式对象转为普通对象

markRaw标记一个对象使其不再做响应式.比如临时往响应式对象中添加新数据,由于新增的数据也是响应式的,可以使用markRaw使其变成非响应式数据.

  • 使用场景:
    • 有些值不应被设置为响应式,例如复杂的第三方库.
    • 当渲染具有不可变数据源的大列表时,跳过响应式以提升性能.

自定义 ref -- customRef

定义:创建一个自定义的 ref,并对其依赖项跟踪(track)和更新触发(trigger)进行显示控制

用法:

setup(){
  function myRef(value){
    // 这个自定义的函数应该返回一个Vue的customRef,因为不可能让开发者自己写一套完整的响应式代码
    return customRef((track,trigger)=>{ // track:跟踪器 trigger:触发器
      // 必须返回一个对象,对象内必须含有getter,setter
      return{
        get(){
          track() // 让Vue跟踪value
          return value
        },
        set(newVal){
          value = newVal // 让旧值等于新值
          trigger() // 触发更新界面
        }
      }
    })
  }
  const x = myRef(100)
  return{
    x
  }
}

provide && inject

作用:实现祖组件与后代组件通信

使用:

  1. 在祖组件中
setup(){
  const something = "string"
  provide("prop",something)
}
  1. 在后代组件中
setup(){
  const something = inject("prop")
  return{
    something
  }
}

响应式数据的判断

  • isRef:检查一个值是否为一个 ref 对象.
  • isReactive:检查一个值是否是由reactive创建的响应式代理.
  • isReadonly:检查一个对象是否是由readonly创建的只读代理.
  • isProxy:检查一个对象是否是由reactive或者readonly方法创建的代理.

Composition API 的优势


新的组件

Fragment

正是因为这个组件,让 template 模板中不在需要一个根元素包裹所有的标签.Fragment 会自动将所有的标签自动包裹.

Teleport

使用 Teleport 组件能够将 HTML 结构移动到指定地方

<!-- to后面跟要移动的位置 -->
<teleport to="body">
  <div>
    ...
  </div>
</teleport>

作用:让 css 样式更好设置

异步组件 && Suspense

Suspense 在等待组件渲染的时候给予用户更好的体验

在使用的时候需要引入异步组件

import { defineAsyncComponent } from "vue";
const Child = defineAsyncComponent(() => import("./components/Child.vue"));

上面的代码中defineAsyncComponent接受一个参数,该参数是一个函数,函数的返回值为一个import函数,import 函数中包含需要异步引入的组件

在异步组件中,setup可以为一个async函数,返回一个 promise.

Suspense 的用法:

使用Suspense包裹两个<template>,两个 template 都需要配置v-slot,并配置好插槽名为defaultfallback

<template>
  <h2>这是子组件</h2>
  <Suspense>
    <template v-slot:default>
      <Child />
    </template>
    <template v-slot:fallback>
      <h3>加载中.....</h3>
    </template>
  </Suspense>
</template>

上面的代码中的v-slot:defaultv-slot:fallback是固定写法,不能改名

default插槽内写加载完成后需要渲染的组件,fallback里写在加载过程中要给用户的提示信息.

其他更改

全局 API 的转移

  • Vue 2.x 有许多全局 API 和配置。

    • 例如:注册全局组件、注册全局指令等。

      //注册全局组件
      Vue.component('MyButton', {
        data: () => ({
          count: 0
        }),
        template: '<button @click="count++">Clicked {{ count }} times.</button>'
      })
      
      //注册全局指令
      Vue.directive('focus', {
        inserted: el => el.focus()
      }
      
  • Vue3.0 中对这些 API 做出了调整:

    • 将全局的 API,即:Vue.xxx调整到应用实例(app)上

      2.x 全局 API(Vue 3.x 实例 API (app)
      Vue.config.xxxx app.config.xxxx
      Vue.config.productionTip 移除
      Vue.component app.component
      Vue.directive app.directive
      Vue.mixin app.mixin
      Vue.use app.use
      Vue.prototype app.config.globalProperties

其他改变

  • data 选项应始终被声明为一个函数。

  • 过度类名的更改:

    • Vue2.x 写法

      .v-enter,
      .v-leave-to {
        opacity: 0;
      }
      .v-leave,
      .v-enter-to {
        opacity: 1;
      }
      
    • Vue3.x 写法

      .v-enter-from,
      .v-leave-to {
        opacity: 0;
      }
      
      .v-leave-from,
      .v-enter-to {
        opacity: 1;
      }
      
  • 移除keyCode 作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除v-on.native修饰符

    • 父组件中绑定事件

      <my-component
        v-on:close="handleComponentEvent"
        v-on:click="handleNativeClickEvent"
      />
      
    • 子组件中声明自定义事件

      <script>
      export default {
        emits: ["close"],
      };
      </script>
      
  • 移除过滤器(filter)

    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

v-model的参数

若子组件有属性值是动态的,而这个值又是父组件给予的,那么采用这种方式将能够很好的处理这种情况.

  • 父组件
<template>
  <input type="text" v-model="value">
  <Child v-model:title="value"></Child>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const value = ref("")
</script>
  • 子组件
<template>
  <h2>{{title}}</h2>
</template>
<script setup>
const props = defineProps(["title"]) // 使用defineProps能获取父组件传过来的值
</script>

上面的代码能够实现子组件的标题是随着父组件中的value值改变而改变

setup 语法糖

格式:

<script setup>
// 代码
</script>

特性:

  1. 不需要再返回声明的变量和函数
  2. 不需要再 export default{}
  3. 不需要再注册组件

当然也是可以获取 context,props,emits 的,这确实是学习成本,但很简单

使用 useAttrs useSlots获取原本 setup(context)里的内容

<script setup>
import { useAttrs, useSlots } from "vue";
const attrs = useAttrs();
const slots = useSlots();
</script>

使用 defineExpose 暴露子组件内部的数据,父组件仍使用ref属性接受

子组件

<script setup>
import { defineExpose } from "vue";
const a = "暴露的内容";
defineExpose({
  a,
});
</script>

父组件

<template>
  <Child ref="chi" />
</template>
<script setup>
import {onMounted, ref} form "vue";
import Child from "./Child.vue";
const chi = ref(null)
onMounted(()=>{
  console.log(chi.value.a)
})
</script>

使用 defineProps 接受父组件传过来的值,仍可以指定接受的类型

父组件

<template>
  <Child :msg="message">
</template>
<script setup>
import Child from "./Child.vue";
const message = "一般的父向子传值"
</script>

子组件

<template>
  <h2>{{ msg }}</h2>
</template>
<script setup>
defineProps(["msg"]);
</script>

使用 defineEmits 子组件像父组件传值

子组件

<template>
  <button @click="toParent">触发向父组件传值事件</button>
</template>
<script setup>
const emit = defineEmits(["child"]); // 相当于告诉了父组件有这么一个东西
function toParent() {
  emit("child", "子向父传值"); // emit("名字","内容")
}
</script>

父组件

<template>
  <Child @child="readChildMsg" />
</template>
<script setup>
import Child from "./Child.vue";
function readChildMsg(msg) {
  console.log(msg);
}
</script>

确实很奇怪,相比较 vue2 的this.$emit,defineEmits还多了一步

posted @ 2021-12-17 10:08  Yune_Neko  阅读(117)  评论(0编辑  收藏  举报