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


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

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 内部结构更简单更易懂.


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 结构移动到指定地方



  
...

作用:让 css 样式更好设置

异步组件 && Suspense

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

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

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

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

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

Suspense 的用法:

使用Suspense包裹两个