Skip to content

一、Vue3快速上手

image-20220925205443793

1.Vue3简介

2.Vue3带来了什么

2.1 性能的提升

  • 打包大小减少41%
  • 初次渲染快55%, 更新渲染快133%
  • 内存减少54% …

2.2 源码的升级

  • 使用Proxy代替defineProperty实现响应式
  • 重写虚拟DOM的实现和Tree-Shaking …

2.3 拥抱TypeScript

  • Vue3可以更好的支持TypeScript

2.4 新的特性

  1. Composition API(组合API)
  • setup配置

  • ref与reactive

  • watch与watchEffect

  • provide与inject

  1. 新的内置组件
  • Fragment

  • Teleport

  • Suspense

  1. 其他改变
  • 新的生命周期钩子

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

  • 移除keyCode支持作为 v-on 的修饰符

二、创建Vue3工程

1.使用 vue-cli 创建

官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

bash
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue_test
## 启动
cd vue_test
npm run serve

2.使用 vite 创建

官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite

vite官网:https://vitejs.cn

  • 什么是vite?—— 新一代前端构建工具。

  • 优势如下:

    • 开发环境中,无需打包操作,可快速的冷启动。

    • 轻量快速的热重载(HMR)。

    • 真正的按需编译,不再等待整个应用编译完成。

传统构建 与 vite构建对比图

注:他们创建的时间都是差不多的,只是在运行启动时快慢不同!

(1)传统构建

![在这里插入图片描述](file://D:/downloads/typora%20picture/af5f8d4493f3423087d6b9e6c5e60fa1tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.webp?lastModify=1664112561)

传统构建模式,是将所有资源都打包好,再上线

(2)vite构建

![在这里插入图片描述](file://D:/downloads/typora%20picture/c57d4d695fe64014b78610ff2a5cd2b6tplv-k3u1fbpfcp-zoom-in-crop-mark3024000.webp?lastModify=1664112583)

而Vite是按需加载

接下来我们就用Vite来创建一个Vue3的项目

bash
## 创建工程,前面三个是不可变的,最后一个是工程的名字
npm init vite-app <project-name>
# same as :npm会自动转换为如下命令执行:
# npx create-vite-app <project-name>
## 进入工程的目录
cd <project-name>
## 安装依赖!!!这个是特殊之处,不会自动安装的
npm install # npm i也可以
## 运行
npm run dev

3.关闭eslint语法检查

  • 根目录新建vue.config.js
js
module.exports={
   
    lintOnSave:false  //关闭语法检查
}

这样有一些错误就不会在web页面报错,只是效果出不来而已(但是开发时一般不用关)

4.分析文件目录

4.1 main.js

Vue2项目的main.js

javascript
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

我们再来看看Vue3项目中的main.js

特点:更加的简洁,用工厂函数创建app对象,而不是直接使用new Vue()的方式,把vue2的逻辑封装了起来更加方便

并且这里创建出来的app对象不完全是vm对象了,app类似于之前Vue2中的vm,但app比vm更“轻”,即没有那么多冗余的属性和方法

并且vue3不需要这句话:Vue.config.productionTip = false

这句话的意思:阻止显示处于生产模式的消息(因为这个消息没什么意义,我们开发的时候肯定是处于生产模式,如果出现会显得比较冗余杂乱)

image-20221119143958687

js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

分析一下

js
// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”,即没有那么多冗余的属性和方法)
const app = createApp(App)
// console.log(app)
// 挂载:(这样页面上才会出东西)
app.mount('#app')

// 卸载:(这样页面上就没东西了)
// setTimeout(() => {
//     app.unmount("#app"); //用unmount函数卸载
// },2000)

注意

我们在vue3工程中,书写main.js时如果再使用原来vue2那套是行不通的!

4.2 .vue组件

我们再来看看组件

template标签里可以没有根标签了(但是也可以有)

以App.vue为例:

html
<template>
    <!-- Vue3组件中的模板结构可以没有根标签,即可以没有<div id="xxx"></div>-->
	<img alt="Vue logo" src="./assets/logo.png">
	<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

5.Vue3.0中的响应式原理

5.1 vue2.x的响应式

底层:

js
let person = {
    name: '张三',
    age: 18
};

//模拟vue2中的响应式
//创建一个空对象p
let p = {};
//给p对象身上追加属性name(在原型上加属性和方法)
Object.defineProperty(p, 'name', {
    get(){
        //get name
        return person.name;
    },
    set(value){
        //set name
        console.log('name changed, must change the page'); //这里其实有深层的逻辑,说白了就是发现数据被修改了,要更新页面!
        person.name = value;
    }
})
//给p对象身上追加属性age
Object.defineProperty(p, 'age', {
    configurable: true,
    get(){
        //get age
        return person.age;
    },
    set(value){
        //set age
        console.log('age changed, must change the page');
        person.age = value;
    }
})

总结:

  • 实现原理:

    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(==数据劫持==)。

    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

      js
      Object.defineProperty(对象, '属性', {
          get () {}, 
          set () {}
      })
  • 存在问题:

    • 只能拦截到读取和修改属性,所以后面再新增和删除属性, 界面不会更新。——>最大的缺点!

      (1)新增:

      需要使用this.$set(this.对象, ‘属性’, ‘属性值’)或者 Vue.set(this.对象, ‘属性’, ‘属性值’)来进行新增

      (2)删除:

      需要使用this.$delete(this.对象, ‘属性’)或者 Vue.set(this.对象, ‘属性’)来进行删除

    • 直接通过下标修改数组, 界面不会自动更新。

      (1)修改:

      使用this.$set(this.数组, 索引, ‘值’)或者 Vue.set(this.数组, 索引, ‘值’)来进行修改

      (2)修改、新增或删除:

      使用this.数组.splice(索引, 个数, ‘值’)

      新增就把个数写为0

      删除就不用指定值

5.2 Vue3.0的响应式

注:这里说的是Proxy对象的方式实现响应式,Vue3中的ref函数操作基本类型属性依然用的是vue2中的get,set方法!

底层:

js
let person = {
    name: '张三',
    age: 18
};

//模拟vue3中的响应式
//Proxy代理:让p对象去映射对于person的操作,其实和Object.defineProperty方法是类似的!
const p = new Proxy(person, {
    //捕获数据的修改:所有的属性共用一套get,set!非常轻便!
    //读取
    get(target, propName) {
        //person对象 ==> target
        //person对象的属性 ==> propName
        console.log(`有人读取了p身上的${propName}属性`);
        //return target[propName] //直接改,和下面效果等价
        return Reflect.get(target, propName); //这个get和我们定义的get不是一个概念!
    },
    //修改/增加
    set(target, propName, value) {
        // 修改和增加属性都调用set
        console.log('修改,更新页面');
        Reflect.set(target, propName, value);
    },
    //删除
    deleteProperty(target, propName) {
        console.log(`删除,更新页面`);
        return Reflect.deleteProperty(target, propName);
    }
});

Reflect原理:

Reflect.defineProperty相比Object.defineProperty的优势:会有返回值,可以判断执行失败还是成功了,不需要捕获异常,更加健壮!

js
let obj = {
    a:1,
    b:2
}

const x1 = Reflect.defineProperty(obj, 'c',{
    get(){
        return 3;
    }
});
console.log(x1); //true 成功
const x2 = Reflect.defineProperty(obj, 'c',{ //这里如果是Object.defineProperty就会报错,但是这里会返回false!这是有优势的!
    get(){
        return 4;
    }
});
if(x2){ //可以进行判断捕获
    console.log(x2); //false 否
}
Reflect.get(obj,'a');

总结:

  • 实现原理:

    • 通过Proxy(代理对象): 拦截对象中任意属性的任意变化, 包括:属性值的读写、属性的添加、属性的删除等。

      所以说不存在vue2中的添加、删除属性不能实现响应式的问题!

      不需要$set了!

    • 通过Reflect(反射对象): 对源对象的属性进行操作,并且有返回值表示执行的状态。

    • MDN文档中描述的Proxy与Reflect:

这样就实现了利用对Proxy代理对象进行更新,拦截了更新的动作,然后利用Reflect反射对象对原来的

数据进行更新,从而实现页面绑定的数据的更新,从而实现响应式!

三、常用 Composition API

Composition API:组合式API

官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html

1.拉开序幕的setup

vue
<template>
    <!--vue3的组件模版结构可以没有根标签-->
    <h1>我是app组件</h1>
    <h1>我叫{{ name }}, {{ age }}岁</h1>
    <button @click="sayHello">hello!!</button>
    <h3>msg: {{ vue2 }}</h3>
    <p>数据冲突该怎么办?{{ a }}</p>
    <button @click="test1">测试一下在vue2中去读取vue3的配置</button>
    <button @click="test2">测试一下在vue3的setup中去读取vue2的配置</button>
</template>

<script>
    // import { h } from 'vue';
    export default {
        name: "App",
        //测试使用vue2的内容
        data() {
            return {
                vue2: "still can use vue2 in vue3 code",
                a: 1,
            };
        },
        methods: {
            //vue2配置方法的方式
            test1() {
                console.log(this.vue2);
                console.log(this.name);
                console.log(this.sayHello);
                this.sayHello();
            },
        },
        //此处只是测试setup,暂时先不考虑响应式的问题
        //响应式:就是数据绑定,数据更改了就进行相应的改变
        setup() {
            //表演的舞台
            //准备数据 data
            let name = "py";
            let age = 21;
            let a = 300;

            //方法 methods,读取vue3的配置:可以读取!
            function sayHello() {
                alert(`My name is ${name}, ${age} ${age === 1 ? "year" : "years"} old`);
            }

            //在vue3的配置里去读取vue2配置的属性:读取不到!
            function test2() {
                console.log(name);
                console.log(age);
                console.log(sayHello);
                console.log(this.vue2);
                console.log(this.test1);
            }

            //选择1:返回一个对象
            return {
                name,
                age,
                sayHello,
                test2,
                a,
            };

            //选择2:返回一个渲染函数
            //这是直接将你在这里渲染的东西替换到template中
            //第一个参数:要把哪个元素放到页面 //第二个参数:元素里面的内容
            // return () => h('h1', 'hello');
            //这样模板template里面写什么都没有用了,就以渲染函数为主
            //所以页面上只会出现大大的hello
        },
    };
</script>

总结:

  1. 理解:Vue3.0中一个新的配置项,值为一个函数。
  2. setup是所有Composition API(组合API)“ 表演的舞台 ”,也就是说主要是用来写组合式API的!
  3. 组件中所用到的:数据、方法等,均要配置在setup中(也就是说不用写data和methods等配置项了),当然组合式API也要写在setup中!
  4. setup函数的两种返回值:
  • 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)

  • 若返回一个渲染函数:则可以自定义渲染内容。(了解)

  1. 注意点:
  • ==setup尽量不要与Vue2.x配置混用==

    • Vue2.x的配置项依然可以用
    • Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
    • 但==在setup中不能访问到Vue2.x配置(data、methos、computed…)中的属性、方法==。
    • 如果有重名的, 以setup内的为主。
  • ==setup不能是一个async函数==,因为返回值不再是return原本的对象, 而是被promise包裹的对象, 模板就看不到return的对象中的属性了(因为原本的对象被包裹住了)。

    注:后期也可以返回一个Promise实例,但需要Suspense组件和defineAsyncComponent异步引入的配合使用才行,所以这样setup也可以是async函数了,同时还可以配合await使用!

2.setup的两个注意点

注:vue2中的两个补充点

(1)Vue2中的this.$attrs:这个就是用来兜底的,在props属性中没有进行声明的,但是外部传进来的,就会出现在这个attrs属性中

(2)Vue2中的this.$solts:这个是用来存插槽接收到的内容,如果没有在子组件中写<solt></solt>,那么内容不会在页面上显示,但是父组件的子组件标签内的内容(以虚拟dom的形式)会存储到这个solts属性中,并不会丢失(不管声明不声明<solt></solt>都会出现在solts中)

setup的两个注意点

Demo.vue

vue
<template>
  <!--vue3的组件模版结构可以没有根标签-->
  <h1>我是app组件</h1>
  <h1>我叫{{ person.name }}, {{ person.age }}岁</h1>
  <button @click="test">测试触发一次demo的自定义事件</button>
</template>

<script>
import { reactive } from 'vue';
export default {
  name: 'Demo',
  beforeCreate() {
    console.log('----@bc');
  },
  props: ['msg', 'school'], //还是一样要声明
  emits:['hello'], //要声明触发的事件
  setup(props, context){
    // console.log('----setup');
    // console.log(this); ///undefined
    console.log(props); //props: 外部给组件丢的参数 => 响应式(Proxy实例)

    let person = reactive({
      name: 'py',
      age: 21,
    });

    // console.log(context, context.attrs); 相当于vue2中的$attrs
    // console.log(context,context.slots); 插槽

    //返回一个对象
    return {
      person,
      test(){
        context.emit('hello', 666); //触发自定义事件
      }
    }
  }
}
</script>

App.vue

vue
<template>
  <!--vue3的组件模版结构可以没有根标签-->
  <Demo @hello="showHelloMsg" msg="hello" school="wust">
    <!--注意:现在slot="qwe"这种写法弃用了,在vue3中不要这么写!-->
    <template v-slot:qwe>
      没事
    </template>
    <template v-slot:asd>
      love you
    </template>
  </Demo>
</template>

<script>
import Demo from "./components/Demo";
export default {
  name: 'App',
  components: {Demo},
  setup(){
    return {
      showHelloMsg(value){
        alert(`你好,${value}`);
      }
    }
  }
}
</script>

总结:

  • 1.setup执行的时机

    • 在beforeCreate(最先执行的钩子)之前执行一次,并且this是undefined(因为vm还没创建出来)。
    • 所以说setup函数里面不需要使用this,因为根本没法使用,所以说setup里面所有函数都可以用箭头函数!
  • 2.setup的参数

    • (1)props:父组件传递的数据响应式集合对象,依然需要props:[]接收,才能在setup函数内打印出props参数

      注意:但是这里props声明的和传入的必须一样,如果有出入会报警告,并且这次的有出入的参数会存到context对象的attrs对象中;和vue2不一样,vue2不会有警告,并且是存在vm的$attrs内

      其余的context对象的slots、emit对象同理!

    • (2)context:上下文对象

      • 1)context.attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于vue2的 this.$attrs。

      • 2)context.slots: 收到的插槽内容,相当于vue2的 this.$slots。

        注意:现在slot="name"这种写法弃用了,统一使用v-slot:name这样的写法!

        还有简写的方式,可以写#name,就相当于v-slot:name

      • 3)context.emit: 分发(触发)自定义事件的函数, 相当于vue2的 this.$emit。

        • 在vue2中子组件触发事件并向父组件传值,this.$emit(‘test’,1)

          在vue3中第一步要定义**emits:[“test”]**声明将要触发父组件的事件名,第二步在合适的时机进行触发事件 context.emit(‘test’,1)

3.ref函数

我们发现如果还是用去取setup中的数据,那数据改变页面不会变了!

原因:直接在setup里面定义的数据根本就不是响应式数据,vue检测不到!

ref函数应用场景:针对setup中的非响应式数据,即数据会变但页面不会变的情况

vue
<template>
  <!--vue3的组件模版结构可以没有根标签-->
  <h1>我是app组件</h1>
  <h1>我叫{{ name }}, {{ age }}岁</h1>
  <h3>职位:{{ job.type }}</h3>
  <h3>薪水:{{ job.salary }}</h3>
  <button @click="changeInfo">修改人的信息</button>
</template>

<script>
//引用ref函数
import { ref } from 'vue';
    
export default {
  name: 'App',
  setup(){
    //表演的舞台(setup)
    //准备数据 data
    //ref实现响应式(基本类型):也是采用Object.definedProperty()来实现的,即getter和setter方法
    let name = ref('py'); //创建ref引用对象:把数据交给ref函数加工一下作为一个变量!
    let age = ref(21);
    //ref实现响应式(对象类型):reactive函数实现,本质上是采用ES6中的Proxy对象来实现的
    let job = ref({
      type: 'frontend developer',
      salary: '30'
    });

    function changeInfo(){
      name.value = '李四'; //要用.value来进行修改,否则依然不行,因为ref对象里面的value才是我们想要的属性!
      age.value = 42;
      job.value.type = 'UI developer'; //取对象里面的属性就不需要再次.value了!
      console.log(name, age); //不是响应式的
    }

    //返回一个对象
    return {
      name,
      age,
      job,
      changeInfo
    }
  }
}
</script>

总结:

  • 解释:这里的ref是一个函数,与vue2中的不同,vue2中的ref只是标签的一个属性,作用类似于id,而这里不同!

  • 作用: 定义一个响应式的数据

  • 语法: const xxx = ref(initValue)

    • 创建一个包含响应式数据的引用对象(RefImpl对象,又称reference对象,全称为‘引用实现的实例对象’,简称ref对象),ref对象.value就是我们想要的数据

    • 在JS中操作数据: xxx.value

    • 模板中读取数据: 不需要 变量名.value,直接:<div></div>

  • 备注:

    • 接收的数据可以是:基本类型、也可以是对象类型。

    • 基本类型的数据:响应式依然是靠Object.defineProperty()的getter与setter完成的(数据劫持)。

    • 对象类型的数据:对象内部的属性的响应式 “ 求助 ” 了Vue3.0中的一个新函数 —— reactive函数,本质上是采用ES6中的Proxy对象来实现的。

4.reactive函数

vue
<template>
  <!--vue3的组件模版结构可以没有根标签-->
  <h1>我是app组件</h1>
  <h1>我叫{{ person.name }}, {{ person.age }}岁</h1>
  <h3>职位:{{ person.type }}</h3>
  <h3>薪水:{{ person.salary }}</h3>
  <h3>爱好:{{ person.hobbies }}</h3>
  <h4>测试的数据c:{{ person.a.b.c }}</h4>
  <button @click="changeInfo">修改人的信息</button>
</template>

<script>
import { reactive } from 'vue';
export default {
  name: 'App',
  setup(){
    //表演的舞台(setup)
    //准备数据 data
    //ref实现响应式(基本类型)也是采用Object.definedProperty()来实现的 getter和setter
    // let name = ref('py'); //ref引用对象(RefImpl对象)实例
    // let age = ref(21);
    //ref实现响应式(对象类型)也是采用Proxy来实现(proxy) 这里如果就算是用ref也是借助了reactive
    let person = reactive({
      name: 'py',
      age: 21,
      type: 'frontend developer',
      salary: '30',
      hobbies: ['抽烟', '喝酒', '烫头'],
      a:{
        b:{
          c: 666
        }
      }
    });

    //注:注意这里即便是得到对象也不需要.value了,得到对象的属性就更加不需要.value了!
    function changeInfo(){
      //基本类型不能响应式,因为reactive处理不了,套路:我们把基本类型作为属性封装为对象!
      person.name = '李四';
      person.age = 42;
      //原来的:job.value.type = 'xxx'
      person.type = 'UI developer';
      //测试reactive能否监测深层次变化:可以监测!
      person.a.b.c = 100;
      //测试reactive能否处理数组:可以!
      person.hobbies[0] = 'play tennis';
    }

    //返回一个对象
    return {
      person,
      changeInfo
    }
  }
}
</script>

总结:

  • 作用: 定义一个对象类型的响应式数据(==基本类型不要用它,要用ref函数==)

  • 语法:const 代理对象= reactive(源对象)

    接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称Proxy对象)

  • reactive定义的响应式数据是“深层次的”,对象的属性也会成为响应式的。

  • 内部基于 ES6 的 Proxy 实现,Proxy是响应式的,由Object——>Proxy,通过代理对象操作源对象内部数据进行操作(数据劫持)。

  • 一般的定义方式:

js
let data = reactive({
	person:{},
    student:{}
});

5.reactive对比ref

  • 从定义数据角度对比:

    • ref用来定义:基本类型数据

    • reactive用来定义:对象(或数组)类型数据

    • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象

  • 从原理角度对比:

    • ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)。

    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。

  • 从使用角度对比:

    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value。

    • reactive定义的数据:操作数据与读取数据,均不需要.value。——> 用的更多一点

6.计算属性与监视

6.1 computed函数

vue
<template>
  <h1>一个人的信息</h1>
  姓:<input type="text" v-model="person.firstName"/>
  <br/>
  名:<input type="text" v-model="person.lastName"/>
  <p>姓名:{{ person.fullName }}</p>
  <br/>
  修改全名:<input type="text" v-model="person.fullName"/>
</template>

<script>
import { reactive, computed } from 'vue'; //引入计算属性
export default {
  name: 'Demo',
  //vue2的写法:
  // computed:{
  //   fullName(){
  //     //vue2看得到vue3,要注意
  //     return this.person.firstName + '-' + this.person.lastName;
  //   }
  // },
    
  //vue3的写法:
  setup(){
    let person = reactive({
      firstName: 'pan',
      lastName: 'yue',
      age: 21,
    });
      
    //计算属性(简写,没有考虑计算属性被修改的情况)
    // 注1:可以直接给person添加一个fullName计算属性!保证是响应式的
    // 注2:可以使用箭头函数,因为setup函数里面不需要使用this,因为setup的this是undefined的!!!
    // person.fullName = computed(() => {
    //   return person.firstName + '-' + person.lastName;
    // })

    //计算属性(完整写法,既考虑了读也考虑了改)
    person.fullName = computed({
      get(){
        return person.firstName + '-' + person.lastName
      },
      //修改
      set(name){
        //解构赋值,这里也可以写let a = ...,然后下面用a[0]、a[1]去接收
        let [ fn, ln ] = name.split('-');
        //响应式
        person.firstName = fn;
        person.lastName = ln;
      }
    })

    //返回一个对象
    return {
      person,
    }
  }
}
</script>

总结:

  • 与Vue2.x中computed配置功能一致

  • 写法

    js
    import {reactive,computed} from 'vue'
    
    setup(){
        let person = reactive({
            firstName:'',
            lastName:''
        })
    	//计算属性——简写
        person.fullName = computed(()=>{
            return person.firstName + '-' + person.lastName
        })
        //计算属性——完整
        person.fullName = computed({
            get(){
                return person.firstName + '-' + person.lastName
            },
            set(value){
                const nameArr = value.split('-')
                person.firstName = nameArr[0]
                person.lastName = nameArr[1]
            }
        })
    }

6.2 watch函数

vue
<template>
   <h1>当前求和为:{{ sum }}</h1>
   <button @click="sum++">点我加一</button>
   <hr/>

   <h2>当前的信息为:{{ msg }}</h2>
   <button @click="msg += '!'">修改信息</button> <!--每次多加一个叹号!-->
   <hr/>

   <h2>姓名:{{ person.name }}</h2>
   <h2>年龄:{{ person.age }}</h2>
   <h2>薪资:{{ person.job.j1.salary }}K</h2>
  <button @click="person.name = person.name + '~'">修改姓名</button>
  <button @click="person.age++">增长年龄</button>
  <button @click="person.job.j1.salary++">增长薪资</button>
</template>

<script>
import { ref, reactive, watch } from 'vue';
export default {
  name: 'Demo',
    
  //vue2中的watch写法:
  // watch:{
  //   //简单写法
  //   // sum(nv,ov){
  //   //   console.log('sum的值发生变化了');
  //   //   console.log(`newValue:${nv}, oldValue:${ov}`);
  //   // }
  //
  //   //完整写法
  //   sum:{
  //     deep: true, //深度监视
  //     immediate: true, //一开始就监视一下
  //     handler(nv,ov){
  //         console.log('sum的值发生变化了');
  //         console.log(`newValue:${nv}, oldValue:${ov}`);
  //     }
  //   }
  // },
    
  //vue3中的watch写法:
  setup(){

      let sum = ref(0);
      let msg = ref('你好');
      let person = reactive({
          name: '张三',
          age: 18,
          job:{
              j1:{
                  salary: 20
              }
          }
      })
      let person2 = ref({
          name: '张三',
          age: 18,
          job:{
              j1:{
                  salary: 20
              }
          }
      })

    //情况一: 监视ref所定义的响应式数据
    // 注意:这里不用.value,否则报错,因为存的是基本类型数据,不能直接监视数据值,而应该监视一个变量或者对象!
    // watch(sum, (nv, ov) => {
    //   //这里我并不需要this,所以箭头函数、普通函数都可以
    //   console.log('sum的值发生变化了');
    //   console.log(`newValue:${nv}, oldValue:${ov}`);
    // }, {
    //   //监视的配置
    //   immediate: true //一上来就更新
    // });
      
    //注意:如何监视ref定义的对象?
    // 方式一:这里需要.value,否则无效,因为属性在ref对象的person2对象的里面,属性改变不会造成person2的地址值改变,所以监测不到改变。如果.value就得到了一个Proxy对象,默认deep的对象
    // watch(person2.value, (nv, ov) => {
    //   console.log(`newValue:${nv}, oldValue:${ov}`);
    // });
      
    // 方式二:这里不需要.value,开启deep即可,这样person2里面的属性变化就可以监视到了
    // watch(person2, (nv, ov) => {
    //   console.log(`newValue:${nv}, oldValue:${ov}`);
    // }, {
    //   deep: true
    // });

    //情况二: 监视ref所定义的多个响应式数据
    // 注意:这里不用.value,否则报错
    // watch([sum, msg], (nv, ov) => {
    //   //此时nv和ov都是被监视属性值的数组
    //   // console.log(Array.isArray(ov)); //true
    //   console.log('sum的值或者msg的值发生变化了');
    //   console.log(`newValue:${nv}, oldValue:${ov}`);
    // },{
    //   immediate: true
    // });

    /**
     * 情况三:监视reactive所定义的一个响应式数据(ref定义的对象也一样,因为底层还是reactive)
     * reactive定义的数据在进行监视时的两个坑:
     *	 1.此处无法获取正确的ov(oldValue) ——> 目前还没有解决的bug,但是oldValue本来就不重要
     *   2.强制开启了深度监视
     */
    // watch(person, (nv, ov) => {
    //   console.log('person变化了');
    //   console.log(nv, ov);
    // }, {
    //   deep: false //此处的deep配置是无效的,会强制为true
    // });

    //情况四:监视reactive所定义的响应式数据中的某一个属性
    // 属性要写成函数,作为其返回值,() => person.age
    // watch(() => person.age, (nv, ov) => {
    //   console.log('person的age变了', nv, ov);
    // })

    //情况五:监视reactive所定义的响应式中的多个属性
    // watch([() => person.age, () => person.name], (nv, ov) => {
    //   //此时nv和ov都是数组
    //   console.log('person的age或name发生改变了',nv, ov);
    // });

    //特殊情况:监视reactive所定义的对象里面的对象
    // watch(() => person.job, (nv, ov) => {
    //   //这里依然无法拿到正确的ov,因为依然监视的是对象
    //   console.log('person的job信息发生改变了',nv, ov);
    // }, {
    //   //这里必须要加deep:true 注意!
    //   deep: true //此处因为监视的是reactive所定义的响应式对象的一个属性(这个属性的值它依然是一个对象),所以deep配置就有效了,且默认为false
    // })

    //返回一个对象
    return {
      sum,
      msg,
      person
    }
  }
}
</script>

总结

  • 与Vue2.x中watch配置、功能一致
img
  • 监视reactive定义的响应式数据时,三个小“坑”:

    • oldValue无法正确获取

    • 默认强制开启了深度监视(deep:false关闭配置失效)。

    • 监视reactive定义的响应式数据中某个属性时:deep配置有效。

6.3 watchEffect函数

vue
<template>
   <h1>当前求和为:{{ sum }}</h1>
   <button @click="sum++">点我加一</button>
   <hr/>

   <h2>当前的信息为:{{ msg }}</h2>
   <button @click="msg += '!'">修改信息</button>
   <hr/>

   <h2>姓名:{{ person.name }}</h2>
   <h2>年龄:{{ person.age }}</h2>
   <h2>薪资:{{ person.job.j1.salary }}K</h2>
  <button @click="person.name = person.name + '~'">修改姓名</button>
  <button @click="person.age++">增长年龄</button>
  <button @click="person.job.j1.salary++">增长薪资</button>
</template>

<script>
import {reactive, ref, watch, watchEffect} from 'vue';
export default {
  name: 'Demo',
  setup(){

    let sum = ref(0);
    let msg = ref('你好');
    let person = reactive({
      name: '张三',
      age: 18,
      job:{
        j1:{
          salary: 20
        }
      }
    });

    //监测的不是一个值,而是一个保存值的结构(不能写成sum.value) ,不能监视一个具体的值 注意
    watch(sum, (nv, ov) => {
       console.log(nv, ov);
    },{
      immediate: true
    });

    //watchEffect函数
    //特点:不确定监视对象,用谁就监视谁,并且默认开启了immediate:true
    //好处:实现了业务逻辑的复用,也就是说多个属性可以共用一套监视逻辑!
    watchEffect(() => {
      console.log(`watch effect指定的回调执行了!!!`)
      //依赖收集,你用到了谁它就监视谁!!
      //这里用到sum,person.job.j1.salary了,所以可以被监视到(只要它们发生变化就重新执行watchEffect,重新进行一系列的逻辑)
      //与computed有点类似,依赖收集(即回调函数里面依赖的数据发生变化就重新运行)。但是侧重点不一致,即它们的回调函数的内容侧重不同,watchEffect注重过程的执行(业务逻辑的运行),而computed注重计算函数的返回值(属性的动态计算)
      const x1 = sum.value;
      const x2 = person.job.j1.salary;
      //不需要return
    })

    //返回一个对象
    return {
      sum,
      msg,
      person
    }
  }
}
</script>

总结:

  • watch的套路是:既要指明监视的属性,也要指明监视的回调。

  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。只要有变化就会执行回调

  • watchEffect有点像computed:

    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。

    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。

      js
      //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
      watchEffect(()=>{
          const x1 = sum.value
          const x2 = person.age
          console.log('watchEffect配置的回调执行了')
      })

7.生命周期onMounted()

vue2.x的生命周期

image-20220926163909306

vue3.0的生命周期

image-20220926164200789

  • Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:销毁变成了卸载

    即Vue2中的钩子函数在Vue3中依然适用!

    • beforeDestroy改名为 beforeUnmount
    • destroyed改名为 unmounted

    控制组件的挂载和卸载=>使用v-if

  • Vue3.0也提供了 Composition API 形式的生命周期钩子(写在setup函数里面的),与Vue2.x中钩子对应关系如下:只变名,不变功能

    • beforeCreate ==>setup()本身
    • created ==>setup()本身
    • beforeMount ==>onBeforeMount
    • mounted ==>onMounted
    • beforeUpdate ==>onBeforeUpdate
    • updated ==>onUpdated
    • beforeUnmount ==>onBeforeUnmount
    • unmounted ==>onUnmounted
    • 注:对应的两者最好不要同时出现,当同时出现时,带on的要先于不带的!即组合式Api的优先级更高一点!

实战:

vue
<template>
   <h1>当前求和为:{{ sum }}</h1>
   <button @click="sum++">点我加一</button>
   <hr/>
</template>

<script>
//组合式API的理解:可以import引入很多函数,然后组合到setup函数中去!
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';
export default {
  name: 'Demo',
  setup(){
    let sum = ref(0);

    //1.通过 组合式api 的形式去使用生命周期钩子 ——> 在setup里面写!
    //setup()相当于beforeCreate()和created()
    onBeforeMount(() => {  console.log('----beforeMount----'); });
    onMounted(() => {  console.log('-----mounted-----'); });
    onBeforeUpdate(() => {  console.log('-----beforeUpdate-----') });
    onUpdated(() => { console.log('-----updated-----'); });
    onBeforeUnmount(() => { console.log('-----beforeUnmount----'); });
    onUnmounted(() => { console.log('-----unmounted----'); })

    console.log('-----setup----')

    //返回一个对象
    return {
      sum,
    }
  },
  //2.使用 配置项 的形式使用生命周期钩子 ——> 在setup外面写!
  // beforeCreate() {
  //   console.log('----beforeCreate!!----');
  // },
  // created() {
  //   console.log('----created!!----');
  // },
  // beforeMount() {
  //   console.log('----beforeMount----');
  // },
  // mounted() {
  //   console.log('-----mounted-----');
  // },
  // beforeUpdate() {
  //   console.log('-----beforeUpdate-----')
  // },
  // updated() {
  //   console.log('-----updated-----');
  // },
  // beforeUnmount() {
  //   console.log('-----beforeUnmount----');
  // },
  // unmounted() {
  //   console.log('-----unmounted----');
  // }
}

Vue2的钩子函数和Vue3中的Composition API 形式的生命周期钩子的关系图示:

image-20220926164322455

8.自定义hook函数

其实就相当于工具类的感觉

../hooks/usePoint.js

数据,方法,生命周期钩子

hook文件的命名一般都是:useXxx

js
//得到鼠标点坐标的api

import { reactive, onMounted, onBeforeUnmount } from "vue";

//export default function usePoint(){
export default function(){ //简写:可以没有名字,叫什么名字是根据引入的人来决定的
    //1.响应式数据
    let point = reactive({
        x: 0,
        y: 0
    });

    //2.方法
    const savePoint = event => {
        console.log(event.pageX, event.pageY);
        point.x = event.pageX;
        point.y = event.pageY;
    };

    //3.生命周期
    //挂载时就绑定事件监听
    onMounted(() => {
        window.addEventListener('click', savePoint)
    });

    //卸载之前移除事件监听
    onBeforeUnmount(() => {
        //在卸载之前取消事件的监听,因为组件如果不用了,事件也不应该监听了
        window.removeEventListener('click', savePoint);
    });

    return point;
}

Demo.vue

vue
<template>
   <h1>当前求和为:{{ sum }}</h1>
   <button @click="sum++">点我加一</button>
   <hr/>
   <h2>当前点击时鼠标的坐标为x:{{ point.x }}, y:{{ point.y }}</h2>
</template>

<script>
import {ref} from 'vue';
import usePoint from "../hooks/usePoint";
export default {
  name: 'Demo',
  setup(){
    let sum = ref(0);

    //复用自定义hooks,调用hook函数,得到数据、方法、生命周期钩子
    const point = usePoint();

    //返回一个对象
    return {
      sum,
      point
    }
  },
}
</script>

Test.vue

vue
<template>
   <h2>我是test组件</h2>
   <h3>{{ point }}</h3>
</template>

<script>
import usePoint from "../hooks/usePoint";

export default {
  name: "Test",
  setup(){
    const point = usePoint(); //复用打点hook(封装数据以及其要用的生命周期钩子),这也是组合式api的好处
    return {
      point
    }
  }
}
</script>

总结

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,里面有响应式数据和一系列的逻辑。

    所以Composition API,即组合式API的理解:包括ref、reactive、computed、watch、生命周期函数等,里面有数据和逻辑

  • 类似于vue2.x中的mixin。

  • 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。

操作步骤

  1. 在src文件夹下创建hooks文件夹
  2. 在hooks文件夹下创建usePoint.js文件(常以use开头)
  3. 定义方法并return出去point,然后再其他组件中既可以调用使用return的值

9.toRef和toRefs

Demo.vue

目的:实现模板中可以直接引用属性,不需要 对象.属性

vue
<template>
  <h4>{{ person }}</h4>
  <h2>姓名:{{ name }}</h2> <!--之前是person.name-->
  <h2>年龄:{{ age }}</h2>
  <h2>薪资:{{ salary }}K</h2>
  <button @click="name = name + '~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="salary++">增长薪资</button>
</template>

<script>
import { ref, reactive, toRef, toRefs } from 'vue';
export default {
  name: 'Demo',
  setup(){
    let person = reactive({
      name: '张三',
      age: 18,
      job:{
        j1:{
          salary: 20
        }
      }
    });

    const name1 = person.name; //普通的赋值操作
    console.log('@@@@@', name1); //注意 这里输出的是字符串,并不是响应式的数据
      
    const name2 = toRef(person,name); //返回的是RefImpl对象,这里的name2与person.name是完全一模一样的(你改这里的name2与你改上面的person.name是一码事),且数据还是响应式的
    console.log('####', name2);

    const x = toRefs(person);
    console.log(x);

    //toRef就是返回一个RefImpl对象(toRef是引用 name就是person.name且为响应式)
    //toRef处理一个,而toRefs处理一群
    //大白话:toRef(s)就是方便我们把响应式数据(ref或者reactive生成的)展开丢出去,方便在模版中直接应用(就不用对象.属性了,直接写属性即可)
    return {
      person,
      //普通写法:toRef
      // name: toRef(person, "name"),
      // age: toRef(person, "age"),
      // salary: toRef(person.job.j1, "salary")
      //简便写法:toRefs
      ...toRefs(person), //将所有属性转换为ref对象,包裹成为一个大对象,ES6的...进行对象的拆解
      //注意:但是toRefs只会转换第一层的属性,我们拿不到深层的属性,所以还需要单独使用toRef
      salary: toRef(person.job.j1, 'salary')  //toRef可以与toRefs连用,更加方便,可以取到深层的属性!
    };

    //注意千万不能这样写!
    //一旦这样写就与元数据分离了,虽然页面数据会变,但是改name不会引起原来的person.name的变化(因为ref把name值包装成了一个新的refImpl对象,并没有指向原来的person的属性,与原来的person就没有关系了!是两个不同的数据!)
    // return {
    //   person,
    //   name: ref(person.name),
    //   age: ref(person.age),
    //   salary: ref(person.job.j1.salary)
    // };
      
    //这样写就更不行了,因为person.name就相当于一个普通的字符串,一个普通的变量,与元数据没有任何关系
    // return {
    //   person,
    //   name: person.name,
    //   age: person.age,
    //   salary: person.job.j1.salary
    // };
  }
}
</script>

总结:

  • 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
  • 语法:const name = toRef(person,'name'),html中就可以直接使用name。
  • 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
  • 扩展:toRefs与toRef功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person),相当于一个对象

创建的新的ref对象指向的还是原来的那个属性:所以说toRef就相当于一个桥接的作用!

img

toRef写法:

img

toRefs写法:

img

四、其它 Composition API

1.shallowReactive 与 shallowRef

vue
<template>
  <h2>当前的y是:{{ x.y }}</h2>
  <button @click="x = {y : 888}">点我替换x</button>
  <button @click="x.y++">点我y+1</button>
  <hr/>
  <h4>{{ person }}</h4>
  <h2>姓名:{{ name }}</h2>
  <h2>年龄:{{ age }}</h2>
  <h2>薪资:{{ job.j1.salary }}K</h2>
  <button @click="name = name + '~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="job.j1.salary++">增长薪资</button>
</template>

<script>
import {ref,reactive, toRefs, shallowReactive, shallowRef} from 'vue';
export default {
  name: 'Demo',
  setup(){
    // shallowReactive只考虑对象类型的第一层数据响应式:浅层次的reactive
    // 所以这里salary就不是响应式的了
    // let person = shallowReactive({
    //   name: '张三',
    //   age: 18,
    //   job:{
    //     j1:{
    //       salary: 20
    //     }
    //   }
    // });

    let person = reactive({
      name: '张三',
      age: 18,
      job:{
        j1:{
          salary: 20
        }
      }
    });

    // let x = ref(0);
    //对于基本类型来说,ref与shallowRef基本是没什么区别的
    // let x = shallowRef(0);
      
    //但注意对于对象类型来说,shallowRef只能处理对象类型的第一层,而ref可以处理对象类型全部层次的响应式
    //但注意不管是shallowRef还是非shallow, 第一层都是响应式的(比如下面的x是响应式的数据),只不过shallowRef的对象里面的属性不是响应式的
    //所以x是可以进行替换的,但是里面的属性不能修改!
    let x = shallowRef({ y: 0 });
    console.log(x);

    // let x = ref({ y: 0 });
    // console.log(x);

    return {
      person,
      ...toRefs(person),
      x,
    };

  }
}
</script>

总结:

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式),如果对象第一层属性也是对象就不会响应式。

  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理(只能处理对象类型的第一层)。

  • 什么时候使用?

    • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。

    • 如果有一个对象数据,后续功能不会修改该对象中的属性(常量对象),而是生成新的对象来替换 ===> shallowRef。

2.readonly 与 shallowReadonly

vue
<template>
  <h2>当前求和为:{{ sum }}</h2>
  <button @click="sum++">sum+1</button>
  <hr/>
  <h2>姓名:{{ name }}</h2>
  <h2>年龄:{{ age }}</h2>
  <h2>薪资:{{ job.j1.salary }}K</h2>
  <button @click="name = name + '~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="job.j1.salary++">增长薪资</button>
</template>

<script>
import {ref,reactive, toRefs, readonly, shallowReadonly} from 'vue';
export default {
  name: 'Demo',
  setup(){

    let person = reactive({
      name: '张三',
      age: 18,
      job:{
        j1:{
          salary: 20
        }
      }
    });

    // person = readonly(person); //此时person里面的属性值都不允许修改
    // person = shallowReadonly(person); //第一层不能改(name,age), 但j1和salary仍然可以改动

    let sum = ref(0);
    // sum = readonly(sum); //同理
    // sum = shallowReadonly(sum) //对于基本类型来说readonly和shallowReadonly没区别

    return {
      sum,
      ...toRefs(person),
    };

  }
}
</script>

总结:

  • readonly: 让一个响应式数据变为只读的(深只读)。person = readonly( person )

  • shallowReadonly:让一个响应式数据变为只读的(浅只读),对象里的第一层不能改,但深层的数据可以改。

  • 应用场景: 当不希望数据被修改时,保证安全。

    因为响应式的数据可能不是我们自己定义的,是别人定义的,我们不想修改。

    或者有的用户无权限进行修改,而有的有权限。

3.toRaw 与 markRaw

vue
<template>
  <h2>当前求和为:{{ sum }}</h2>
  <button @click="sum++">sum+1</button>
  <hr/>
  <h2>姓名:{{ name }}</h2>
  <h2>年龄:{{ age }}</h2>
  <h2>薪资:{{ job.j1.salary }}K</h2>
  <h3 v-show="person.car">座驾信息:{{ person.car }}</h3>
  <button @click="name = name + '~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="job.j1.salary++">增长薪资</button>
  <button @click="showRawPerson">输出最原始的person</button>
  <button @click="addCar">给人添加一台车</button>
  <button @click="person.car && (person.car.name +='!') ">换车名</button>
  <button @click="changePrice">换价格</button>
</template>

<script>
import {markRaw, reactive, ref, toRaw, toRefs} from 'vue';

export default {
  name: 'Demo',
  setup(){

    let sum = ref(0);

    let person = reactive({
      name: '张三',
      age: 18,
      job:{
        j1:{
          salary: 20
        }
      }
    });

    //toRaw用于消除数据的响应式
    const showRawPerson = () => {
       const p = toRaw(person);
       // console.log(person); //proxy代理对象 Proxy {....}
       // console.log(p); //原始对象数据  {....}
       p.age++; //注意此时页面不会再发生变化了,只是普普通通的对象,不是响应式
       
      // const sum  = toRaw(sum);
      // console.log(sum); //undefined //这条路走不通,toRaw只能处理reactive生成的Proxy对象
    }

    //markRaw用于剥夺数据的响应式权利
    const addCar = () => {
      person.car = markRaw({
         name: 'benz',
         price: 40
       }); //在响应式的对象身上添加任何属性都是响应式的,经过markRaw一包装就变成最原始的数据,就不会再做响应
    }


    const changePrice = () => {
      //?.是js语法中的可选链,只有当car存在时才会去访问price属性!
      //下面的意思就是:只有当car存在时,才会把price进行++,也就是点击那个按钮才有用
      person.car?.price && person.car.price++;
      console.log(person?.car?.price);
    }

    return {
      sum,
      person,
      ...toRefs(person),
      showRawPerson,
      addCar,
      changePrice
    };

  }
}
</script>

总结:

  • toRaw:

    • 作用:将一个由reactive生成的响应式对象转为普通对象,就是普通的person对象。

    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

  • markRaw:

    • 作用:标记一个对象,使其永远不会再成为响应式对象

    • 应用场景:

      • 有些值不应被设置为响应式的,例如复杂的第三方类库等(有时需要把工具库放在对象身上)。
      • 当渲染具有不可变数据源的大列表时(没有进行修改的需求),跳过响应式转换可以提高性能。——> 性能极致化

4.customRef(自定义ref)

vue
<template>
  <input v-model="keyWord"/>
  <h3>{{ keyWord }}</h3>
</template>

<script>
import { ref, customRef } from 'vue';
export default {
  name: 'App',
  setup(){
    // let keyWord = ref('hello'); //使用vue提供的内置ref,
    let keyWord = myRef('hello', 500); //使用自定义ref

    //自定义ref(customRef):手动挡,可玩性更大
    function myRef(value, delay){
      return customRef((track, trigger) => {
         let timer;
         return {
           get(){
              console.log(`从myRef这个容器读取数据,data:${value}`);
              track(); //track通知vue追踪一下value的变化(跟getter商量一下让它明确你这个新的value是有用的)
              return value; //读取的时候就会调用get
           },
           set(newValue){
             console.log(`myRef容器中的数据被修改,data改为${newValue}`);
             clearTimeout(timer); //清楚之前的定时器,重新开启,防抖
             timer = setTimeout(() => { //定时器,延时修改
               value = newValue; //把修改后的值赋值给传入的value
               trigger(); //trigger通知vue重新解析模版 //防抖,只有在定时器结束,最终运行到trigger时才会重新解析模板
             },delay);
           }
         }
      });
    }

    return {
      keyWord,
    }
  }
}
</script>

修改input的数据时触发的逻辑:

image-20220926232909645

总结:

  • 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

  • 实现防抖效果:

    vue
    <template>
    <input type="text" v-model="keyword">
    <h3>{{keyword}}</h3>
    </template>
    
    <script>
    	import {ref,customRef} from 'vue'
    	export default {
    		name:'Demo',
    		setup(){
    			// let keyword = ref('hello') //使用Vue准备好的内置ref
    			//自定义一个myRef
    			function myRef(value,delay){
    				let timer
    				//通过customRef去实现自定义
    				return customRef((track,trigger)=>{
    					return{
    						get(){
    							track() //告诉Vue这个value值是需要被“追踪”的
    							return value
    						},
    						set(newValue){ //防抖效果
    							clearTimeout(timer)
    							timer = setTimeout(()=>{
    								value = newValue
    								trigger() //告诉Vue去更新界面
    							},delay)
    						}
    					}
    				})
    			}
    			let keyword = myRef('hello',500) //使用程序员自定义的ref
    			return {
    				keyword
    			}
    		}
    	}
    </script>

5.provide 与 inject

Child.vue

vue
<template>
  <div class="child">
    <h2>child(子)</h2>
    <Son/>
  </div>
</template>

<script>
import Son from "./Son";
import { inject } from "vue";

export default {
  name: "Child",
  components: {Son},
  setup() {
    console.log('@@@@ Car ', inject('car'))
  }
}
</script>

Son.vue

vue
<template>
  <div class="son">
    <h2>son(孙)</h2>
    <h3>从app组件获取的数据:{{ car.name }} --- {{ car.price }}</h3>
  </div>
</template>

<script>
import { inject } from "vue";
export default {
  name: "Son",
  setup(){
    let car = inject('car'); //获取爷爷辈的数据
    console.log(car); //响应式数据
    return {
      car
    }
  }
}
</script>

App.vue

vue
<template>
  <div class="app">
    <h2>app(祖) {{ name }}---{{ price }}</h2>
    <Child/>
  </div>
</template>

<script>
import Child from "./components/Child";
import {reactive, toRefs, provide} from "vue";
export default {
  name: 'App',
  components: {Child},
  setup(){
    let car = reactive({
      name: 'benz',
      price: 40
    });
    provide('car', car); //给自己的后代组件递数据,只要是后代都可以(后代的后代也可以)
    return {
      ...toRefs(car)
    }
  }
}
</script>

总结:

  • 作用:实现祖代与后代组件间通信(多级组件的深入通讯)

  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据

  • 具体写法:

    • 祖组件中:
    js
    setup(){
    	......
        let car = reactive({name:'奔驰',price:'40万'})
        provide('car',car)
        ......
    }
    • 后代组件中:
    js
    setup(props, context){
    	......
        const car = inject('car')
        return {car}
    	......
    }

6.响应式数据的判断

vue
<template>
  <div class="app">
  </div>
</template>

<script>
import {reactive, readonly, ref, toRefs, isRef, isReactive, isReadonly, isProxy} from "vue";
export default {
  name: 'App',
  setup(){

    let car = reactive({
      name: '宝马',
      price: 40
    });

    let sum = ref(0);

    let car2 = readonly(car); ///readonly依然返回代理类型的对象只不过它不能再改而已

    console.log(isRef(sum), isReactive(car), isReadonly(car2), isProxy(car), isProxy(car2), isProxy(sum));

  }
}
</script>

总结:

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

五、Composition API 的优势

1.Options API 存在的问题

Options API:Vue2中的一套,是配置式的API

使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改。

因为不是按照功能分类的,而是按照模块分的,所以在修改某一个功能的内容时,需要去这些不同模块中去修改,比较麻烦!

image-20220926234656230image-20220926234720246

2.Composition API 的优势

我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。

这里是按照功能进行分类的,一个功能相关的数据、方法、计算属性等都是在一起的!

所以组合式API的最大精髓要通过自定义hook来体现,同样的功能的相关代码就放到一个hook里面!

六、新的组件

1.Fragment(片段、碎片)

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 原因就是内部会将多个标签包含在一个Fragment虚拟元素中, 最终不参与渲染
  • 好处: 减少标签层级, 减小内存占用

2.Teleport(传送、瞬间移动)

App.vue

vue
<template>
  <div class="app">
    <h2>app</h2>
    <Child/>
  </div>
</template>

<script>
import Child from "./components/Child";
import {reactive, toRefs, provide} from "vue";
export default {
  name: 'App',
  components: {Child},
}
</script>

Child.vue

vue
<template>
  <div class="child">
    <h2>child(子)</h2>
    <Son/>
  </div>
</template>

<script>
import Son from "./Son";
export default {
  name: "Child",
  components: {Son},
  setup() {
    // console.log('@@@@ Car ', inject('car'))
  }
}
</script>

Son.vue

vue
<template>
  <div class="son">
    <h2>son(孙)</h2>
    <Dialog/>
  </div>
</template>

<script>
import Dialog from "./Dialog";
export default {
  name: "Son",
  components: {Dialog}
}
</script>

Dialog.vue 重孙组件

vue
<template>
  <div>
    <button @click="isShow = true">点我弹个窗</button>
    <!--如果不用teleport,那么弹窗出现的时候会把外面的三层都撑大,影响效果--> 
    <!--我们希望弹窗可以出现在屏幕的正中央,而不依赖于任何一个组件的内部!-->
      <teleport to="body"> <!--挂载到index.html文件的<body>标签上,会在</body>的上面,这样就可以参照body去写定位了,更好处理定位-->
      <div class="mask" v-if="isShow"> <!--遮罩效果弹窗的实现-->
        <div class="dialog">
          <h3>我是一个弹窗</h3>
          <h4>一些内容</h4>
          <h4>一些内容</h4>
          <h4>一些内容</h4>
          <h4>一些内容</h4>
          <button @click="isShow = false">关闭弹窗</button>
        </div>
      </div>
    </teleport>
  </div>
</template>

<script>
import { ref } from "vue";
export default {
  name: "Dialog",
  setup(){
    let isShow = ref(false);
    return {
      isShow
    }
  }
}
</script>

<style scoped>
  .mask{ /*遮罩层*/
    position: absolute;
    top:0;
    bottom: 0;
    left: 0;
    right:0;
    background: rgba(0,0,0,.5); /*半透明*/
    text-align: center;
  }
  .dialog{ /*居中的效果*/
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 300px;
    height: 300px;
    background: #42b983;
  }
</style>

总结:

  • 什么是Teleport?—— Teleport 是一种能够将我们的组件的html结构移动到指定位置的技术。

  • to = “body” 或者 to = “#id”

    语法:<teleport to="移动位置">

    vue
    <teleport to="移动位置">
    	<div v-if="isShow" class="mask">
    		<div class="dialog">
    			<h3>我是一个弹窗</h3>
    			<button @click="isShow = false">关闭弹窗</button>
    		</div>
    	</div>
    </teleport>

3.Suspense(悬念、悬而未决)

Child.vue

vue
<template>
  <div class="child">
    <h2>child(子)</h2>
    {{ sum }}
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  name: "Child",
  //注:这里主要就是为了模拟一个慢的网速,无特殊意义(也可以用浏览器的3g网络模拟)
  async setup() { //async是异步的,与await搭配使用
    let sum = ref(0);

    let p = new Promise((resolve, reject) => { //返回Promise对象值
      setTimeout(() => {
        resolve({
          sum
        });
      },5000) //等待5s
    })

    return await p; //await含义:等p成功才返回结果
  }
}
</script>

App.vue

vue
<template>
  <div class="app">
    <h2>app</h2>
    <!--Suspense的底层是插槽,default用于展示真正的组件,fallback用于展示组件没出来时的效果-->
    <Suspense>
      <template v-slot:default>
        <Child/>
      </template>
      <template v-slot:fallback>
        <!--fallback:退路-->
        <h3>loading...</h3>
      </template>
    </Suspense>
  </div>
</template>

<script>
// import Child from "./components/Child"; 静态引入组件
import { defineAsyncComponent } from "vue"; //定义异步组件
const Child = defineAsyncComponent(() => import('./components/Child')); //动态引入组件(异步)
export default {
  name: 'App',
  components: {Child},
}
</script>

总结:

  • Suspense的作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验(比如加载圈等)

  • 静态引入:父组件和子组件会一起渲染(如果子组件没加载出来,父组件也没办法加载)——>这是一般的情况

  • 动态引入(异步引入):父组件会先加载,再加载子组件 ——>这是使用了defineAsyncComponent的效果(定义异步组件)

  • 使用步骤:

    • 异步引入组件

      js
      import {defineAsyncComponent} from 'vue'
      const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
    • 使用Suspense包裹组件,并配置好defaultfallback两个插槽(固定配置)

      vue
      <template>
      	<div class="app">
      		<h3>我是App组件</h3>
      		<Suspense> <!--标签里边是异步组件-->
      			<template v-slot:default>
      				<Child/>
      			</template>
      			<template v-slot:fallback> <!--Child组件未加载出来时显示这个-->
      				<h3>加载中.....</h3>
      			</template>
      		</Suspense>
      	</div>
      </template>

七、其他

1.全局API的转移

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

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

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

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

      对应关系:

image-20220925212401848

注:

1.vue3里面在开发环境时默认把生产提示关闭了,并且移除了这个配置项!

因为现在的脚手架可以分辨出来我们所处的环境npm run serve/build,所以就不会有这个提示了

2.现在依然能用全局属性(所有vc对象都可以获取到的),但是方法变成了globalProperties

2.其他改变

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

  • 过度类名的更改:

    • Vue2.x写法

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

      在来的时候加了一个-from

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

    (1)移除keyCode 即不支持@keyup.13,不支持按键编码绑定事件,因为这个兼容性比较差

    (2)移除config.keyCodes 即不支持按键别名,因为这个也要依赖按键编码

  • 移除v-on.native修饰符

    在组件标签上使用自定义事件时,使用.native标志这个事件是一个原生事件

    @click.native="hello"

    那vue3中该如何标志原生事件呢?

    • 父组件中绑定事件

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

      vue
      <script>
        export default {
          emits: ['close'] // 声明指定自定义事件,制定了的就是自定义事件
          //那么没声明的就是原生事件,所以click默认就是原生事件
        }
      </script>
  • 移除过滤器(filter)

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

推荐用计算属性或者普通方法去实现!

3.vue3.2更新内容

1、不需要再return即可使用变量和方法,可以直接使用组件名

vue
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
  <MyComponent />
</template>
<!--动态组件-->
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>

2、在css样式中可以使用v-bind绑定变量

vue
<style scoped>
button {
   
  color: v-bind(color);
}
</style>

3、新增 defineCustomElement方法(自定义html元素)

js
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
   
  // normal Vue component options here
})

// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)

4、新的指令 v-memo 比如一些不改变的数据跳过更新

原文章:https://blog.csdn.net/weixin_43848576/article/details/121075593?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166411039316800180667531%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166411039316800180667531&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-121075593-null-null.142^v50^control,201^v3^control_1&utm_term=vue3尚硅谷笔记&spm=1018.2226.3001.4187