学习Vue3(供自我学习)

157 阅读13分钟

根据b站视频整理文档:www.bilibili.com/video/BV18K…

一. 认识Vue3

1. 认识Vue3

1 了解相关信息

  • Vuejs 3.0 "One Piece'" 正式版在 2020 年 9 月份发布
  • 2年多开发, 100+位贡献者, 2600+次提交, 600+次 PR
  • Vue3 支持 vue2 的大多数特性
  • 更好的支持 Typescript

2 性能提升

  • 打包大小诚少 41%
  • 初次泻染快 55%, 更新泻染快 133%
  • 内存诚少 54%
  • 使用 Proxy 代蔡 defineproperty 实现数据响应式
  • 重写虚拟 DOM 的实现和 Tree-Shaking

3 新增特性

  • Composition (组合) AP1
  • setup
    • ref和reactive
    • computed 和 watch
    • 新的生命周期函数
    • provide与 inject
    • ...
  • 新组件
    • Fragment - 文档碎片
    • Teleport - 瞬移组件的位置
    • Suspense - 异步加载组件的 loading 界面
  • 其它API更新
    • 全局 APl 的修改
    • 将原来的全局 API 转移到应用对象
    • 模板语法变化

2. 创建 vue3 项目

1 使用 vue-cli 创建

文档: cli.vuejs.org/zh/guide/cr…

## 安装或者升级
npm install -g @vue/cli
## 保证 vue cli 版本在 4.5.0 以上
vue --version
## 创建项目
Vue create my-project

然后的步骤

  • Please pick a preset - 选择 *Manually select features*
  • Check the features needed for your project - 选择上 *TypeScript* ,特别注意点空格是选择,点回车是下一步
  • Choose a version of Vuejs that you want to start the project with - 选择 *3.X (Preview)*
  • Use class-style component syntax - 是否使用class类的方式创建组件? 这里我们不使用,输入n
  • Use Babel alongside TypeScript - 直接回车
  • Pick a linter / formatter config - 直接回车
  • Use history mode for router? - 直接回车
  • Pick a linter / formatter config -直接回车
  • Pick additional lint features - 直接回车
  • Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
  • Save this as a preset for future projects? - 直接回车

2 使用 vite 创建

  • 文档: v3.cn.vuejs.org/guide/insta…
  • vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,
  • 它做到了 本地快捷开发启动 在生产环境下基于 Rollup 打包。
  • 具体见文档啦....

二. Composition API

1. Composition API(常用部分)

文档:composition-api.vuejs.org/zh/api.html

Setup

我们可以跟以前一样定义data和methods,但是在vue3中我们更推荐使用setup函数

  • setup是一个函数, 只在初始化时执行一次
    • 以后大部分代码都是在setup函数中写
  • 返回一个对象, 对象中的属性或方法, 模板中可以直接使用
  • setup返回的数据会和data和methods进行合并,setup优先级更高
  • setup函数中没有this
    • 以后开发都不使用this了
  • setup不要写成async函数

因为setup函数必须返回一个json对象供模板使用,如果setup是一个async函数,返回的将是一个promise对象
如果setup是一个async函数,那该组件就成了一个异步组件,需要配合Suspense组件才能使用,关于Suspense组件我们后面会学到

<template>
  <!-- 不再需要根元素 -->
  <h1>首页</h1>
  <div>{{ a }}-{{ b }}-{{ c }}</div>
  <button @click="handle">按钮</button>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  // defineComponent 就是定义一个组件的意思
  name: "Home",
  data() {
    return {
      a: 1,
      b: 2,
      c: 3,
    };
  },
  methods: {
    handle() {
      console.log("handle");
    },
  },
  setup() {
    let a = 1;
    let b = 2;
    const handle = () => {
      // 直接操作a b变量,页面不会响应,需要借助ref
      a++;
      b++;
      console.log("handle1");
    };
    return {
      a,
      b,
      handle,
    };
  },
});
</script>

ref

前面我们在setup函数中返回了一些数据,但是如果我们直接修改这些数据,我们可以发现并不是响应式的。
如何创建响应式的数据,这时候我们就要借助ref了 。

  setup() {
    let a = 1;
    let b = 2;
    const update = () => {
      a++;
      b++;
      console.log(a, b); // 能打印出新值,但是页面无变化,不是响应式
    };
    return { a, b, update };
  },
  • 作用: 定义响应式数据了
  • 语法: const xxx = ref(initValue):
    • 创建一个包含响应式数据的引用(reference)对象
    • js 中修改数据: xxx.value = otherValue
    • 模板中显示数据:不需要.value,直接使用 {{xxx}}
  • 一般用来定义一个原始类型的响应式数据
<template>
  <h2>{{ count }}-{{ boo }}-{{ str }}-{{ obj.a }}</h2>
  <button @click="update">更新</button>
</template>

<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup() {
    // 一般定义原始类型的数据
    const count = ref(1);
    const str = ref("abc");
    const boo = ref(true);
    // 当然也可以定义对象,只不过定义对象有更好的方法:reactive
    const obj = ref({ a: 1 });

    const update = () => {
      // 修改数需要使用:xxx.value = otherValue
      count.value++;
      boo.value = !boo.value;
      str.value = str.value.split("").reverse().join("");
      obj.value.a++;
    };

    return { count, str, boo, obj, update };
  },
});
</script>

reactive

  • 作用: 定义对象格式的响应式数据

    如果用 ref 定义对象/数组, 内部会自动将对象/数组转换为 reactive 的对象

  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • js中修改数据不需要操作 .value
  • 一般用来定义一个引用类型的响应式数据
<template>
  <h2>{{ state.count }}-{{ state.boo }}-{{ state.str }}-{{ state.obj.a }}</h2>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
      str: "abc",
      boo: true,
      obj: { a: 1 },
    });
    const update = () => {
      state.count++;
      state.boo = !state.boo;
      state.str = state.str.split("").reverse().join("");
      state.obj.a++;
    };
    return { state, update };
  },
});
</script>

toRefs

  • 将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
  • 应用:对reactive定义的对象进行toRefs包装,包装之后的对象中每个属性都是响应式的。
<template>
  <h2>{{ count }}-{{ boo }}-{{ str }}-{{ obj.a }}</h2>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
      str: "abc",
      boo: true,
      obj: { a: 1 },
    });
    const update = () => {
      state.count++;
      state.boo = !state.boo;
      state.str = state.str.split("").reverse().join("");
      state.obj.a++;
    };

    return { ...toRefs(state), update };
  },
});
</script>

比较 Vue2 与 Vue3 的响应式(重要)

vue2中的问题

  1. 对象直接添加新的属性或删除已有属性,界面不会自动更新,不是响应式
  2. 直接通过下标修改元素(arr[1] = xxx)或更新数组的length,界面不会自动更新,不是响应式
<template>
  <div>
    <div v-for="(value, key) of person" :key="key">{{ key }} - {{ value }}</div>
    <button @click="add">增加对象属性</button>
    <hr />
    <div v-for="(item, index) of arr" :key="index">{{ item }}</div>
    <button @click="modify">修改数组元素</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      person: {
        name: "张三",
        age: 20,
      },
      arr: ["a", "b", "c"],
    };
  },
  methods: {
    add() {
      this.person.sex = "男";
      this.person.hobby = ["吃饭", "睡觉", "打豆豆"];
    },
    modify() {
      this.arr[0] = "A";
    },
  },
};
</script>

vue3中不存在vue2的问题

上面的代码在vue3中没有问题

vue2 的响应式

  • 核心:
    • 对象:通过defineProperty对对象的已有的属性值的读取和修改进行劫持(监视/拦截)
    • 数组:通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持

    数组的push、pop、splice等方法之所以能正常使用,其实是因为被vue重写了

/* const vm = new Vue({
  el: "#app",
  data: {
    name: "John",
    age: 12,
  },
});
*/

// 假设vm是我们的vue实例
const vm = {};
// data数据
const data = {
  name: "John",
  age: 12,
};
// 遍历data,将data属性绑定到vm上,对属性的读取和修改进行拦截
Object.entries(data).forEach(([prop, value]) => {
  let initValue = value;
  Object.defineProperty(vm, prop, {
    get() {
      console.log("执行get");
      return initValue;
    },
    set(newValue) {
      console.log("执行set");
      initValue = newValue;
    },
  });
});
// 读取属性值
console.log(vm.name); // '执行get' 'John'
// 修改属性值
vm.name = "bob"; // '执行set'
console.log(vm.name); // '执行get' 'bob'
// 添加属性
vm.sex = "男"; // 不会执行set方法
console.log(vm.sex); // 能打印出'男', 但是不会执行get方法

关于vue2响应式的具体实现可以阅读【ZSEN就是Huzhushan】的文章:手动实现MVVM双向绑定(v-model原理)

// 把push,pop等方法放在一个对象里面
const obj = {
  push() {},
  pop() {},
  shift() {},
  unshift() {},
  splice() {},
  sort() {},
  reverse() {},
};

// 遍历obj,使用defineProperty监听
Object.keys(obj).forEach((key) => {
  Object.defineProperty(obj, key, {
    value: function (...args) {
      return Array.prototype[key].call(this, ...args);
    },
  });
});

const arr = [];
arr.__proto__ = obj; // 将数组的隐式原型指向obj
// 我们知道arr.__proto__等于它的构造函数的原型,也就是Array.prototype,所以arr可以执行push、pop等方法,但是现在arr.__proto__又等于obj了,所以arr.push就相当于obj.push了,而obj.push我们用defineProperty进行了监听,执行obj.push()就会执行value函数

// 测试
arr.push(1); // 执行这一句就相当于执行obj.push(1)
console.log(arr);

Vue3 的响应式

const user = {
  name: "John",
  age: 12,
};
// 代理对象
const proxyUser = new Proxy(user, {
  get(target, prop) {
    console.log("劫持get", prop);
    return Reflect.get(target, prop);
  },

  set(target, prop, val) {
    console.log("劫持set", prop, val);
    return Reflect.set(target, prop, val);
  },

  deleteProperty(target, prop) {
    console.log("劫持delete", prop);
    return Reflect.deleteProperty(target, prop);
  },
});
// 读取属性值
console.log(proxyUser === user); // false
console.log(proxyUser.name); // 劫持get name John
// 设置属性值
proxyUser.name = "bob"; // 劫持set name bob
proxyUser.age = 13;
console.log(user);
// 添加属性
proxyUser.sex = "男"; // 劫持set sex 男
console.log(user);
// 删除属性
delete proxyUser.sex; // 劫持delete sex
console.log(user);

现在我们可以利用Proxy手动实现ref和reactive了,关于这个知识请往后看

总结

正式由于vue3是使用Proxy代理的方法拦截对象本身,所以在vue3中添加/删除属性都是响应式的,通过下标修改数组也是响应式的

setup 的参数

  • props - 接收父组件传入的通过 props 声明过的属性
  • context - 是一个对象,解构出来包含:
    • attrs - 接收父组件传入的没有通过 props 声明过的属性, 相当于 this.$attrs
    • slots - 接收父组件传入的插构内容的对象, 相当于 this.$slots
    • emit - 用来分发自定义事件的函数,相当于 this.$emit

父组件:

<template>
  <Child msg="hello world" msg2="hello vue3" @custom-event="handle">
    <template #aaa>我是插槽</template>
  </Child>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import Child from "./Child.vue";
export default defineComponent({
  name: "Parent",
  components: { Child },
  setup() {
    const handle = (val) => {
      console.log("handle", val);
    };
    return { handle };
  },
});
</script>

子组件:

<template>
  <div>msg: {{ msg }}</div>
  <div>msg2: {{ msg2 }}</div>
  <slot name="aaa"></slot>
  <button @click="fn">按钮</button>
</template>
 
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  props: ["msg"],
  emits: ['custom-event'],
  setup(props, { attrs, slots, emit }) {
    // console.log(props, context);
    console.log(props, attrs, slots, emit);
    const fn = () => {
      emit("custom-event", 123);
    };
    return {
      msg2: attrs.msg2,
      fn,
    };
  },
});
</script>

计算属性computed

回顾vue2中的属性

computed: {
  // 只有getter
  fullName() {
    return this.firstName + " " + this.lastName;
  },
  // 有getter 和 setter
  fullName2: {
    get() {
      return this.firstName + " " + this.lastName;
    },
    set(value) {
      const names = value.split(" ");
      this.firstName = names[0];
      this.lastName = names[1];
    },
  },
},

vue3的计算属性

  • computed - 用法跟vue2类似,不过需要先引入computed
<template>
  <div>
    <span>姓:</span>
    <input type="text" v-model="firstName" />
  </div>
  <div>
    <span>名:</span>
    <input type="text" v-model="lastName" />
  </div>
  <div>
    <span>姓名:</span>
    <span>{{ fullName }}</span>
  </div>
  <div>
    <span>姓名2:</span>
    <input type="text" v-model="fullName2" />
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs, computed } from "vue";
export default defineComponent({
  setup() {
    const user = reactive({
      firstName: "zhang",
      lastName: "san",
    });
    const fullName = computed(() => {
      return user.firstName + " " + user.lastName;
    });
    const fullName2 = computed({
      get() {
        return user.firstName + " " + user.lastName;
      },
      set(value: String) {
        const names = value.split(" ");
        user.firstName = names[0];
        user.lastName = names[1];
      },
    });
    return {
      ...toRefs(user),
      fullName,
      fullName2,
    };
  },
});
</script>

侦听属性 watch

回顾vue2的侦听属性

watch: {
  obj(newVal, oldVal) {
    console.log(newVal, oldVal);
  },
  // 立即监听、深度监听
  obj: {
    handler(newVal, oldVal) {
      console.log(newVal, oldVal);
    },
    immediate: true, // 初始化立即执行一次
    deep: true, // 深度监视
  },
  // 监听对象上的属性
  "obj.a"(newVal, oldVal) {
    console.log(newVal, oldVal);
  },
},

vue3的侦听属性: Vue3的侦听属性,

  • watch: 指定监听数据
    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
      • 如果是监听reactive对象中的属性,必须通过函数来指定
      • 监听多个数据,使用数组来指定
    • 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次
    • 通过配置 deep 为 true, 来指定深度监视
  • watchEffect - 不指定监听数据
    • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
    • 默认初始时就会执行第一次

使用时需要先引入watch 和 watchEffect

<template>
  <div>
    <span>姓:</span>
    <input type="text" v-model="firstName" />
  </div>
  <div>
    <span>名:</span>
    <input type="text" v-model="lastName" />
  </div>
  <div>
    <span>姓名:</span>
    <span>{{ fullName }}</span>
  </div>
  <div>
    <span>str:</span>
    <input type="text" v-model="str" />
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  ref,
  reactive,
  toRefs,
  watch,
  watchEffect,
} from "vue";
export default defineComponent({
  setup() {
    const user = reactive({
      firstName: "zhang",
      lastName: "san",
    });
    const str = ref("abc");
    // 监听一个ref属性
    watch(str, (newValue, oldValue) => {
      console.log(newValue, oldValue);
    });
    // 监听一个reactive对象里的属性,必须通过函数来指定
    watch(
      () => user.firstName,
      (newValue, oldValue) => {
        console.log(newValue, oldValue);
      }
    );
    // 监听多个属性,通过数组
    watch([str, () => user.firstName], (newValue, oldValue) => {
      console.log(newValue, oldValue);
    });
    // fullName 使用监听属性实现
    const fullName = ref("");
    watch(
      [() => user.firstName, () => user.lastName],
      (newValue, oldValue) => {
        fullName.value = newValue[0] + " " + newValue[1];
      },
      {
        immediate: true, // 立即监听
        deep: true, // 深度监听
      }
    );
    // watchEffect 使用
    watchEffect(() => {
      fullName.value = user.firstName + " " + user.lastName;
    });
    return {
      ...toRefs(user),
      str,
      fullName,
    };
  },
});
</script>

生命周期

vue2中的生命周期钩子函数依旧可以使用,不过建议使用vue3的钩子函数

  • vue2与vue3生命周期对比
标题
vue2写法vue3写法
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

注意: beforeDestroy 和 destroyed已经被废弃,如果在vue3中想继续使用vue2的写法,对应的api是 beforeUnmount(beforeDestroy) 和 unmounted(destroyed)

<script lang="ts">
import {
  defineComponent,
  ref,
  onBeforeMount,
  onMounted,
  onActivated,
  onDeactivated,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";
export default defineComponent({
  // 在vue3中使用vue2的生命周期钩子
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  activated() {
    console.log("activated");
  },
  deactivated() {
    console.log("deactivated");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  // vue3中已经用 beforeUnmount 代替 beforeDestroy
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  // vue3中已经用 unmounted 代替 destroyed
  unmounted() {
    console.log("unmounted");
  },
  // 以下两个生命周期钩子无法使用
  beforeDestroy() {
    console.log("beforeDestroy");
  },
  destroyed() {
    console.log("destroyed");
  },

  setup() {
    console.log('--setup');
    onBeforeMount(() => {
      console.log("--onBeforeMount");
    });
    onMounted(() => {
      console.log("--onMounted");
    });
    onActivated(() => {
      console.log("--onActivated");
    });
    onDeactivated(() => {
      console.log("--onDeactivated");
    });
    onBeforeUpdate(() => {
      console.log("--onBeforeUpdate");
    });
    onUpdated(() => {
      console.log("--onUpdated");
    });
    onBeforeUnmount(() => {
      console.log("--onBeforeUnmount");
    });
    onUnmounted(() => {
      console.log("--onUnmounted");
    });
    const str = ref("abc");
    return {
      str,
    };
  },
});
</script>

ref 获取元素

我们知道Vue2中是用this.$refs.xxx来获取元素或组件的,但是vue3中没有this的概念,应该如何获取元素呢
这个时候我们可以使用之前学过的ref创建响应式数据的api来获取元素

  1. 使用ref创建响应式数据,假设叫X
  2. 模板中绑定ref属性,值为上面的X
    • 注意不能使用v-bind动态绑定
    • 这时X就是一个dom元素或组件了

示例: 让输入框自动获取焦点

<template>
  <input type="text" ref="inputRef" placeholder="我会自动获取焦点..." />
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, nextTick } from "vue";
export default defineComponent({
  name: "Home",
  setup() {
    // 1. 使用ref创建响应式数据inputRef
    const inputRef = ref<HTMLElement | null>(null); // 为了防止ts报错,需要加上ts类型校验

    // 2. 渲染完成,让inputRef获取焦点,注意要操作 .value
    onMounted(() => {
      inputRef.value && inputRef.value.focus();
    });

    // 也可以使用 nextTick 判断渲染完成
    nextTick(() => {
      inputRef.value && inputRef.value.focus();
    });

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

自定义 hook 函数

用过react的同学对hook函数这个名词应该不陌生,hook函数翻译成中文就是钩子函数 (注意并不只是生命周期钩子函数)

其实我们前面学过的所有api,比如ref、reactive、computed、watch、onBeforeMount等等都是hook函数,只不过他们是vue内部hook函 数,现在我们要学怎么自定义一个hook函数。

  1. 创建一个函数,函数的名称必须use开头
  2. 函数必须return一些数据

需求1: 收集用户鼠标点击页面的坐标

<template>
  <h2>x: {{ x }}, y: {{ y }}</h2>
</template>

<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
function userMousePosition() {
  // 初始化坐标数据
  const x = ref(-1);
  const y = ref(-1);

  // 用于收集点击事件坐标的函数
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX;
    y.value = e.pageY;
  };

  // 挂载后绑定点击监听
  onMounted(() => {
    document.addEventListener("click", updatePosition);
  });

  // 卸载前解绑点击监听
  onUnmounted(() => {
    document.removeEventListener("click", updatePosition);
  });

  return { x, y };
}
export default defineComponent({
  name: "Home",
  setup() {
    const { x, y } = userMousePosition();
    return { x, y };
  },
});
</script>

2. Composition API(其它部分)

1 shallowReactive 与 shallowRef

  • 它们都表示浅响应式

reactive和ref是深响应

  • shallowReactive : 只处理了对象第一层属性的响应式(只响应第一层)
  • shallowRef: 只有重新赋值时才是响应式(不响应内部数据,只响应整体)
<template>
  <h2>shallowReactive 和 shallowRef</h2>

  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>

  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, shallowReactive, shallowRef } from "vue";
export default defineComponent({
  name: "Home",
  setup() {
    const m1 = shallowReactive({ a: 1, b: { c: 2 } });
    const m2: any = shallowRef({ a: 1, b: { c: 2 } });

    const update = () => {
      // m1.b.c += 1; // 无效
      m1.a += 1; // 有效

      // m2.value.a += 1; // 无效
      m2.value = { a: 123 };
    };

    return { m1, m2, update };
  },
});
</script>

2 readonly 与 shallowReadonly

  • 它们表示只读代理对象 工
  • readonly:
    • 深度只读
    • 设置readonly后,修改响应式数据会报错
  • shallowReadonly
    • 浅只读
    • 设置shallowReadonly后,修改响应式数据的第一层属性会报错
  • 应用场景:
    • 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
  <h2>readonly 和 shallowReadonly</h2>

  <h3>state: {{ state }}</h3>

  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, readonly, shallowReadonly, reactive } from "vue";
export default defineComponent({
  name: "Home",
  setup() {
    const state = reactive({ a: 1, b: { c: 2 } });
    const rState1 = readonly(state);
    const rState2 = shallowReadonly(state);
    const update = () => {
      // rState1.a++; // ts报错
      // rState1.b.c++; // ts报错

      // rState2.a++; // ts报错
      rState2.b.c++; // 不报错,但是页面不会变化,但是我测了下,页面会响应呀
    };
    return { state, update };
  },
});
</script>

3 toRaw 与 markRaw

  • 返回 reactive 或 readonly 对象的原始数据。
    • 这是一个还原方法,可用于临时读取,得到的数据不具有响应式。
  • markRaw
    • 标记一个对象,使其不具有响应式
    • 应用场景:
      • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象.
      • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
  <h2>state: {{ state }}</h2>

  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
import { defineComponent, toRaw, markRaw, reactive } from "vue";
export default defineComponent({
  name: "Home",
  setup() {
    const state = reactive<any>({
      name: "John",
      age: 25,
    });

    const testToRaw = () => {
      const user = toRaw(state);
      user.age++; // 页面不会更新
      console.log(user);
      console.log(state);
    };

    const testMarkRaw = () => {
      const likes = ["a", "b"];
      // state.likes= likes;
      state.likes = markRaw(likes); // likes数组就不再是响应式的了
      console.log(state.likes);
      setTimeout(() => {
        state.likes.push("c");
      }, 1000);
    };
    return { state, testToRaw, testMarkRaw };
  },
});
</script>

4 toRef

  • 为响应式对象上的某个属性创建一个ref引用,更新时引用对象会同步更新
  • 区别ref:拷贝了一份新的数据值单独操作,更新时想好不影响
<template>
  <h2>state: {{ state }}</h2>
  <h2>foo: {{ foo }}</h2>
  <h2>foo2: {{ foo2 }}</h2>

  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive, toRef, ref } from "vue";
export default defineComponent({
  name: "Home",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });

    const foo = toRef(state, "foo"); // 创建一个state.foo的引用,现在foo和state.foo是引用关系
    const foo2 = ref(state.foo); // 拷贝,foo2 和 state.foo 没有关系

    const update = () => {
      // state.foo++;
      // foo.value++;

      foo2.value++; // foo 和 state中的数据不会更新
    };

    return { state, foo, foo2, update };
  },
});
</script>

5 customRef

  • 用于自定义一个ref,可以显式地控制依赖追踪和触发响应
  • 接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 get 和 set 属性的对象。
  • 需求:使用 customRef 实现防抖函数

6 provide 与 inject

  • provide 和 inject 提供依赖注入,功能类似 2.x 的provide/inject
  • 实现跨层级组件(祖孙)间通信
<template>
  <h2>父组件</h2>
  <p>color: {{ color }}</p>
  <button @click="color = 'red'"></button>
  <button @click="color = 'yellow'"></button>
  <button @click="color = 'green'">绿</button>

  <hr />
  <Son></Son>
</template>

<script lang="ts">
import { defineComponent, ref, provide } from "vue";
import Son from "./Son.vue";
export default defineComponent({
  name: "Parent",
  components: { Son },
  setup() {
    const color = ref("");

    provide("color", color);

    return { color };
  },
});
</script>
<template>
  <h2>子组件</h2>
  <p>color: {{ color }}</p>

  <hr />
  <GrandSon></GrandSon>
</template>
 
<script lang="ts">
import GrandSon from "./GrandSon.vue";
import { defineComponent, inject } from "vue";
export default defineComponent({
  name: "Son",
  components: { GrandSon },
  setup() {
    const color = inject("color");
    return { color };
  },
});
</script>
<template>
  <h2>孙组件</h2>
  <p>color: {{ color }}</p>
</template>
 
<script lang="ts">
import { defineComponent, inject } from "vue";
export default defineComponent({
  name: "Son",
  setup() {
    const color = inject("color");
    return { color };
  },
});
</script>

7 响应式数据的判断

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

3. 手写组合 API

1 shallowReactive 与 reactive

// 公共对象提取
const handle = {
  get(target, prop) {
    if (prop === '_is_reactive') return true;

    console.log("劫持get", prop);
    return Reflect.get(target, prop);
  },
  set(target, prop, val) {
    console.log("劫持set", prop, val);
    return Reflect.set(target, prop, val);
  },
  deleteProperty(target, prop) {
    console.log("劫持delete", prop);
    return Reflect.defineProperty(target, prop);
  },
};

function reactive(target) {
  if (target && typeof target === "object") {
    Object.entries(target).forEach(([key, value]) => {
      if (typeof value === "object") {
        target[key] = reactive(value);
      }
    });

    return new Proxy(target, handle);
  }
}

function shallowReactive(target) {
  return new Proxy(target, handle);
}

// 测试
const state = reactive({
  a: 1,
  b: 2,
  c: {
    d: 3,
  },
});
const state2 = shallowReactive({
  a: 1,
  b: 2,
  c: {
    d: 3,
  },
});
console.log(state.a); // 劫持get a 1
console.log(state.c.d); // 劫持get c  劫持get d  3
state.a = 2; // 劫持set a 2
state.c.d = 4; // 劫持get c 劫持set d 4

console.log(state2.a); // 劫持get a 1
console.log(state2.c.d); // 劫持get c 3
state2.a = 2; // 劫持set a 2
state2.c.d = 4; // 劫持get c

2 shallowRef 与 ref

function ref(target) {
  // 如果用ref定义对象/数组,内部会自动将对象/数组转为reactive的对象
  if (target && typeof target === "object") {
    target = reactive(target);
  }
  return {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value() {
      console.log("获取ref", this._value);
      return this._value;
    },
    set value(val) {
      console.log("设置ref", val);
      this._value = val;
    },
  };
}

function shallowRef(target) {
  return {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value() {
      console.log("获取ref", this._value);
      return this._value;
    },
    set value(val) {
      console.log("设置ref", val);
      this._value = val;
    },
  };
}

// 测试
const str = ref("abc");
const obj = ref({
  a: 1,
  b: 2,
  c: {
    d: 3,
  },
});

3 shallowReadonly 与 readonly

const readonlyHandler = {
  get(target, key) {
    if (key === "_is_readonly") return true;

    return Reflect.get(target, key);
  },
  set() {
    console.warn("只读,不能修改");
    return true;
  },
  deleteProperty() {
    console.warn("只读,不能删除");
    return true;
  },
};
// 自定义 shallowReadonly
function shallowReadonly(target) {
  return new Proxy(target, readonlyHandler);
}
// 自定义 readonly
function readonly(target) {
  if (target && typeof target === "object") {
    if (target instanceof Array) {
      // 数组
      target.forEach((item, index) => {
        target[index] = readonly(item);
      });
    } else {
      // 对象
      Object.keys(target).forEach((key) => {
        target[key] = readonly(target[key]);
      });

      const proxy = new Proxy(target, readonlyHandler);

      return proxy;
    }
  }
  return target;
}

// 测试
const objReadOnly = readonly({
  a: 1,
  b: {
    c: 2,
  },
});
const objReadOnly2 = shallowReadonly({
  a: 1,
  b: {
    c: 2,
  },
});
objReadOnly.a = 2;
objReadOnly.b.c = 3;
objReadOnly2.a = 2;
objReadOnly2.b.c = 3;

4 isRef , isReactive, isReadonly 和 isProxy

// 判断是否是ref对象
function isRef(obj) {
  return obj && obj._is_ref;
}
// 判断是否是reactive对象
function isReactive(obj) {
  return obj && obj._is_reactive
}
// 判断是否是readonly对象
function isReadonly(obj) {
  return obj && obj._is_readonly;
}
// 是否是reactive或readonly产生的代理对象
function isProxy(obj) {
  return isReactive(obj) || isReadonly(obj)
}

// 测试
console.log(isReactive(reactive({})));
console.log(isRef(ref({})));
console.log(isReadonly(readonly({})));
console.log(isProxy(reactive({})));
console.log(isProxy(readonly({})));

三. 其他新组件和API

1. 新组件

1 Fragment(片段)

  • 在 Vue2 中:组件必须有一个根标签
  • 在 Vue3 中:组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中
  • 好处:减少标签层级,减少内存占用
<template>
  <h2>aaaaa</h2>
  <h2>aaaaa</h2>
</template>

2 Teleport(瞬移)

  • Teleport 提供了一种干净的方法,让组件的html在父组件界面外的特定标签(很可能是body)下插入显示
<template>
  <button @click="modalOpen = true">
    Open full screen modal!(With teleport)
  </button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        I'm teleport modal!(My parent is "body")
        <button @click="modalOpen = false">Close</button>
      </div>
    </div>
  </teleport>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup() {
    const modalOpen = ref(false);

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

<style scoped>
.modal {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.5);
}
.modal > div {
  padding: 40px;
  text-align: center;
  background: #fff;
  margin: 60px;
}
</style>

3 Suspense(不确定的)

Suspense组件是配合异步组件使用的,它可以让异步组件返回数据前渲染一些后备内容(例如:loading状态)
那我们首先要学会创建一个异步组件

创建异步组件:

  • 在setup函数中返回一个promise,就是一个异步组件
  • setup函数写成async函数,也就是一个异步组件

异步组件 AsyncComp.vue:

<template>
  <h2>AsyncComp</h2>
  <p>{{ msg }}</p>
</template>

<script lang="ts">
export default {
  setup() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ msg: "abc" });
      }, 2000);
    });
  },
};
</script>

异步组件 AsyncComp2.vue:

<template>
  <h2>AsyncComp2</h2>
  <p>{{ msg }}</p>
</template>

<script lang="ts">
export default {
  async setup() {
    const result = await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ msg: "abcd" });
      }, 2000);
    });
    return result;
  },
};
</script>

使用异步组件:

<template>
  <Suspense>
    <!-- 显示默认组件 -->
    <template v-slot:default>
      <AsyncComp />
    </template>

    <!-- 显示后备组件 -->
    <template v-slot:fallback>
      <p>AsyncComp LOADING...</p>
    </template>
  </Suspense>
  <hr />
  <Suspense>
    <template v-slot:default>
      <AsyncComp2 />
    </template>

    <template v-slot:fallback>
      <p>AsyncComp2 LOADING...</p>
    </template>
  </Suspense>
</template>

<script>
import AsyncComp from "./AsyncComp.vue";
import AsyncComp2 from "./AsyncComp2.vue";
// 也可以这样引入
// import { defineAsyncComponent } from "vue";
// const AsyncComp = defineAsyncComponent(() => import("./AsyncComp.vue"));
export default {
  components: { AsyncComp, AsyncComp2 },
  setup() {
    return {};
  },
};
</script>

2. 其它新的 API

1 全新的全局API

  • createApp()
  • defineProperty()
  • defineAsyncComponent()
  • nextTick()

2 将原来的全局 API 转移到应用对象

  • app.component()
  • app.config()
  • app.directive()
  • app.mount()
  • app.unmount()
  • app.use()

3 模板语法变化

  • v-model 的本质变化
    • 在表单上使用没有变化
    • 在组件上使用的时候,默认的属性名和事件名发生了变化
      • prop: value -> modelValue
      • event: input -> update:modelValue
      <template>
        <Child v-model="msg" />
      </template>
      
      <script lang="ts">
      import { defineComponent, ref } from "vue";
      import Child from "./Child.vue";
      export default defineComponent({
        components: { Child },
        setup() {
          const msg = ref("abc");
          return { msg };
        },
      });
      </script>
      
      Child.vue
      <template>
        <h2>Child</h2>
        <p>{{ modelValue }}</p>
        <button @click="update">更新</button>
      </template>
      
      <script lang="ts">
      import { defineComponent } from "vue";
      export default defineComponent({
        props: ["modelValue"], // 以前是value
        setup(props, { emit }) {
          const update = () => {
            // 以前是emit('input')
            emit("update:modelValue", props.modelValue + "---");
          };
          return { update };
        },
      });
      </script>
      
      • 可以自定义modelValue的名字
      <Child v-model:str="msg" />
      
      // 触发得改成 update:str
      emit(update:str)
      
      • 可以绑定多个v-model
      <Child v-model:str="msg" v-model:name="username" />
      
  • .sync 修改符已移除,由 v-model 代替
<!-- vue2中 .sync 的用法 -->
<Child :name.sync="username" />
<!-- 在vue3中其实就相当于 -->
<Child v-model:name="username" />
  • v-if 优先 v-for 解析

四. 路由和状态管理

路由

  • useRoute - 获取当前路由对象
import { useRoute } from "vue-router";

setup() {
  const route = useRoute();
  console.log(route);
}
  • useRouter - 获取路由实例,可以进行路由跳转
import { useRouter } from "vue-router";

setup() {
  const router = useRouter();
  const goHome = () => {
    router.push("/home");
  };
  return { goHome };
}

状态管理

  • useStore - 获取 vuex 实例
import { useStore } from 'vuex';

setup() {
  const store = useStore();
  store.dispatch("xxxx");
}

五. Vue3 综合案例 - TodoList

<template>
  <div class="wrap">
    <div class="input">
      <input
        type="text"
        placeholder="请输入任务标题"
        v-model="title"
        @keyup.enter="handleAddTask"
      />
    </div>
    <ul class="list">
      <li
        :class="{ active: item.status === 1 }"
        v-for="item in filterList"
        :key="item.id"
      >
        <div class="left">
          <input type="checkbox" :value="item.id" v-model="checkArr" />
          <span class="orange" v-show="item.status === 1">V</span>
          {{ item.title }}
        </div>
        <div class="right">
          <button class="btn" @click="handleModifyStatus(item)">
            切换为{{ item.status === 1 ? "未完成" : "已完成" }}
          </button>
          <a
            class="close-icon"
            href="javascript:;"
            @click="handleDelete(item.id)"
            >x</a
          >
        </div>
      </li>
    </ul>
    <div class="operate">
      <div class="left">
        <button @click="handleBatchDelete">删除选中项</button>
        <button @click="handleBatchSetStatus(1)">设为已完成</button>
        <button @click="handleBatchSetStatus(0)">设为未完成</button>
      </div>
      <div class="right">
        <button
          :class="{ active: sort === 'all' }"
          @click="handleSwitchSort('all')"
        >
          全部
        </button>
        <button
          :class="{ active: sort === 'done' }"
          @click="handleSwitchSort('done')"
        >
          已完成
        </button>
        <button
          :class="{ active: sort === 'undone' }"
          @click="handleSwitchSort('undone')"
        >
          未完成
        </button>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from "vue";
interface ITask {
  id: number;
  title: string;
  status: number;
}
interface IDataProps {
  title: string;
  list: ITask[];
  checkArr: number[];
  sort: string;
  filterList: any;
  handleModifyStatus: (item: ITask) => void;
  handleDelete: (id: number) => void;
  handleAddTask: () => void;
  handleBatchDelete: () => void;
  handleBatchSetStatus: (status: number) => void;
  handleSwitchSort: (sort: string) => void;
}
export default defineComponent({
  setup() {
    const data: IDataProps = reactive({
      title: "",
      list: [
        {
          id: 1,
          title: "html5+css",
          status: 1,
        },
        {
          id: 2,
          title: "javascript",
          status: 1,
        },
        {
          id: 3,
          title: "nodejs",
          status: 0,
        },
      ],
      checkArr: [],
      sort: "all",
      filterList: computed(() => {
        switch (data.sort) {
          case "all":
            return data.list;
          case "done":
            return data.list.filter((item) => item.status === 1);
          case "undone":
            return data.list.filter((item) => item.status === 0);
          default:
            return data.list;
        }
      }),
      handleModifyStatus(item: ITask) {
        item.status = item.status === 1 ? 0 : 1;
      },
      handleDelete(id: number) {
        const index: number = data.list.findIndex(
          (item: ITask) => item.id === id
        );
        if (index === -1) return;
        data.list.splice(index, 1);
      },
      handleAddTask() {
        const task: ITask = { id: Date.now(), title: data.title, status: 0 };
        data.list.push(task);
        data.title = "";
      },
      handleBatchDelete() {
        data.list = data.list.filter(
          (item: ITask) => !data.checkArr.includes(item.id)
        );
      },
      handleBatchSetStatus(status: number) {
        data.list.forEach((item: ITask) => {
          if (data.checkArr.includes(item.id)) {
            item.status = status;
          }
        });
      },
      handleSwitchSort(sort: string) {
        data.sort = sort;
      },
    });
    return toRefs(data);
  },
});
</script>

<style lang="scss" scoped>
.wrap {
  padding: 20px;
}

.input input {
  width: 100%;
  border: 1px solid #ddd;
  padding: 8px;
  background-color: #fff;
  border-radius: 8px;
  box-sizing: border-box;
}

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  margin-bottom: 10px;
}

.list li {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  background-color: #05e;
  margin-top: 10px;
  color: #fff;
}

.list li.active {
  background-color: #090;
}

.close-icon {
  padding: 0 10px;
}

.left {
  float: left;
}
.right {
  float: right;
}

button.active {
  background-color: #090;
}
</style>