Vue3

目录

Vue3简介

优势

1.打包体积减少
2.性能提升
3.占用内存减少
4.支持TS

源码升级

1.使用Proxy代替defineProperty,实现数据响应式
2.重写虚拟DOM的实现和Tree-Shaking
Tree-Shaking:代码摇树,即摇掉无用的代码,比如:我们定义了一个模块,里面有10个方法,但是只用到了3个,那么Tree-Shaking就会把10个方法摇掉,只保留3个方法。

新特性

1.composition API(组合式API)

  • setup配置
  • ref和reactive
  • computed和watch
  • watch与watchEffect
  • provide和inject

2.新的内置组件

  • Fragment
  • Teleport
  • Suspense

3.其他改变

  • 新的声明周期钩子
  • 移除了keyCode值作为键盘事件修饰符,例如,@keyup.13已不支持

创建Vue3应用

前提:
1.node.js version >= 18.3
2.vue-cli version >= 4.5.0 脚手架版本大于4.5.0才能创建Vue3应用

# 查看node版本
➜  vue_demo git:(master) node -v
v23.11.0
# 查看@vue-cli版本
➜  vue_demo git:(master) vue -V
@vue/cli 5.0.8
# 如果小于4.5则重新安装
➜  vue_demo git:(master) npm install -g @vue/cli

脚手架vue-cli创建

使用脚手架创建vue3应用,选择Vue3创建

➜  vue_demo git:(master) vue create vue3-test


Vue CLI v5.0.8
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint) 
  Default ([Vue 2] babel, eslint) 
  Manually select features 

########################################
Vue CLI v5.0.8
✨  Creating project in /Users/cancanliu/Documents/code/vue_demo/vue3-test.
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.22.19
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...

success Saved lockfile.
✨  Done in 8.38s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 4.53s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vue3-test.
👉  Get started with the following commands:

 $ cd vue3-test
 $ yarn serve

安装时错误

error @achrinza/node-ipc@9.2.9: The engine "node" is incompatible

@achrinza/node-ipc和node版本不兼容,解决办法,重新安装node版本为稳定版本,不要安装最新版本


使用vite创建

Vite: 是一种新型前端构建工具,能够显著提升前端开发体验。其作者就是vue的创建者尤雨溪。

优势:
1.在开发环境中无需打包,可快速冷启动
2.轻量快速热重载
3.真正的按需引入,不再等待整个应用编译完成

传统构建和vite构建对比:
传统构建: 构建时需要将所有文件进行打包,然后再进行运行。
vite构建: 运行时需要什么文件就动态导入什么文件,不需要等待所有文件打包完成。
img

# 1.使用vite创建项目 npm init vite-app <项目名>
➜  vue_demo git:(master) npm init vite-app vue3-test-vite
Done. Now run:

  cd vue3-test-vite
  npm install (or `yarn`)
  npm run dev (or `yarn dev`)

# 2.下载依赖包
➜  vue_demo git:(master) ✗ cd vue3-test-vite 
➜  vue3-test-vite git:(master) ✗ npm i
added 305 packages in 9s
44 packages are looking for funding
  run `npm fund` for details
# 3.启动项目
➜  vue3-test-vite git:(master) ✗ npm run dev

> vue3-test-vite@0.0.0 dev
> vite

[vite] Optimizable dependencies detected:
vue

  Dev server running at:
  > Local:    http://localhost:3000/
  > Network:  http://172.21.3.176:3000/

分析工程结构

main.js

1.不再引入Vue构造函数创建vue实例,而是引入了createApp工厂方法创建应用实例对象app

vue2

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

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

vue3

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

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

createApp(App): 创建一个app实例,类似于new Vue()创建的vm实例,但是要比vm实例更简洁轻量

App.vue组件

组件的<teamplate>标签中可以有多个根标签

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

eslint仍然检查语法错误:
img
在vetur扩展配置中,取消勾选
img


拉开序幕的setup函数

1.setup函数: 是一个新的组件选项,是所有Composition API表演的舞台
2.组件中所有资源,包括:datamethodscomputedwatch、生命周期钩子等,都配置在setup函数中
3.setup函数的两种返回值:

  • 若返回一个对象,则对象中的属性、方法,模板中均可以直接使用
  • 若返回一个渲染函数,则可以自定义组件渲染内容

setup返回一个对象:

<template>
  读取setup返回的对象: 姓名: {{name}} 年龄: {{age}} 性别: {{sex}}
  <button @click="sayVue3">说</button>
</template>

<script>
export default {
  name: 'App',
  setup() {
    let name = '张三';
    let age = 18;
    let sex = '男';
    function sayVue3() {
      console.log('Vue3');
    }
    return {
      name,
      age,
      sex,
      sayVue3,
    };
  },
};
</script>

img
img

setup返回一个渲染函数:

返回的渲染函数,渲染函数中指定容器,将完全替换掉组件中<template>标签中的内容

<template>
  读取setup返回的对象: 姓名: {{name}} 年龄: {{age}} 性别: {{sex}}
  <button @click="sayVue3">说</button>
</template>
<script>
export default {
  name: 'App',
  setup() {
    return h=>('h1','Vue3')
  },
};
</script>

img


vue3环境中仍然可以定义datamethdos等属性,并正常使用

<template>
  读取data返回的对象: {{nameVue2}}
  <button @click="sayVue2">说vue2</button>
</template>
<script>
data(){
  return {
    nameVue2: 'vue2数据',
  }
},
methods:{
  sayVue2(){
    console.log('vue2')
  }
},
</script>

img

vue2配置中可以访问setup数据,但是setup中不能访问vue2配置的数据,因此vue2vue3不能混合使用写法

<template>
  <button @click="testV3">vue2配置中读取vue3配置数据</button>
  <button @click="testV2">vue3配置中读取vue2配置数据</button>
</template>

<script>
export default {
  data() {
    return {
      nameVue2: '张三vue2',
    };
  },
  methods: {
    sayVue2() {
      console.log('vue2');
    },
    testV3() {
      console.log('读取vue3数据', this.name);
      console.log('读取vue3方法', this.sayVue3);
    },
  },
  setup() {
    let name = '张三vue3';
    function sayVue3() {
      console.log('Vue3');
    }
    function testV2() {
      console.log('读取vue2数据', this.nameVue2);
      console.log('读取vue2方法', this.sayVue2);
    }
    return {
      name,
      sayVue3,
      testV2,
    };
  },
};
</script>

vue2的配置可以读取vue3的数据和方法,但vue3的配置无法读取vue2的数据和方法
img

总结

1.vue2配置中可以访问setup数据,但是setup中不能访问vue2配置的数据,因此vue2vue3不能混合使用写法
2.setup返回对象中属性会交给vue管理,因此插值语法中可以直接使用,其他配置中通过this访问,而setup没有返回的属性,则不能使用
3.vue2配置的数据如果和setput配置的数据有重名,则优先使用setup配置数据

<template>{{name}}</template>
<script>
export default {
  data() {
    return {
      name: 'vue2',
    };
  },
  setup() {
    let name = 'vue3';
    return {
      name,
    };
  },
};
</script>

img

4.setup函数前不能添加async,一旦添加async那么setup的返回值是一个Promise对象,因此模板中无法使用

// 错误写法
async setup() {
  let name = 'vue3';
  return {
    name,
  };
},

ref函数

ref函数用于创建一个响应式对象,对象中有一个属性valuevalue属性的值就是接收的参数的值。
其接收参数的值可以是基本数据类型,也可以是对象类型

<template>
  <button @click="showRefValue">查看ref函数返回值</button>
  <button @click="changeName">修改name</button>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    let name = ref('vue3');
    function showRefValue() {
      console.log(name);
    };
    return {
      name,
      showRefValue
    };
  },
};
</script>

通过ref函数将属性name包装为一个引用实现的实例,简称为引用对象,其属性值通过代理value属性调用gettersetter方法,从而实现数据代理,
vue2中重新解析模板动作相同,当修改name时,调用setter方法,并触发视图更新
img

在setup方法中通过属性.value获取属性值或修改属性值,而插值模板中直接使用{{属性}}即可获取属性值,插值模板省略了.value
元素指令中的表达式也可以直接获取属性值,不需要.value

<template>
  {{ name }}
  <button @click="showRefValue">查看ref函数返回值</button>
  <button @click="changeName(name)">修改name</button>
</template>

function changeName(nameValue) {
  console.log(nameValue);
  name.value = 'vue3 hello';
}

img


基本数据类型和对象类型实现响应式原理不同

基本数据类型:通过ref函数创建响应式对象,通过gettersetter方法实现响应式(数据劫持)
对象类型: 将ref()函数接收的对象,通过reactive函数转换为响应式对象(Proxy类型),然后再把Proxy类型的数据赋值给RefImplvalue属性

setup() {
  let person = ref({
    hobby: ['篮球', '足球', '羽毛球'],
    age: 18,
  });
  function showObj() {
    console.log(person);
  }
  return {
    showObj,
  };
},

img
img


ref传入ref对象

<template>
  <input type="text" v-model="name" placeholder="姓名" />
  <br />
  <button @click="name+='!'">修改姓名</button>
  <br />
  <span>{{otherName}}</span>
  <br />
  <button @click="otherName+='!'">修改otherName</button>
</template>
<script>
import { ref, customRef } from 'vue';
export default {
  setup() {
    let name = ref('张三');
    let otherName = ref(name);
    console.log(name, otherName);
    console.log(name === otherName);
    return {
      name,
      otherName,
    };
  },
};
</script>

将ref包装过的对象,传入ref函数后会原样返回。也就是nameotherName指向同一个对象,修改一个另一个也会修改
img


总结

作用: 定义响应式数据
语法: const xxx = ref(初始化值)
读取值:
(1): 插值语法中直接使用{{xxx}}
(2): 元素标签指令表达式中直接使用xxx@click="show(xxx)"
(3): js中使用xxx.value,读取对象中属性: xxx.value.属性


reactive函数

作用: 定义对象类型的响应式数据,不能定义基本类型响应式数据

基本类型数据无法响应式

引入reactive函数并使用

import { ref, reactive } from 'vue';
export default {
  setup() {
    let name = reactive('vue3');
    function changeName() {
      console.log(name);
      name = 'reactive';
    }
    return {
      name,
      changeName
    };
  },
};

使用reactive函数定义基本数据类型为响应式数据,虽然页面可使用,但是vue出现已警告,不能用作响应式数据
将基本类型数据传递给reactive函数,返回值仍是传入的基本类型数据,没有任何变化
img

对象类型数据响应式

传入对象类型数据,返回值是一个Proxy对象,不需要.value.属性,可直接.属性使用响应式数据

reactive对象数据: 姓名: {{person.name}} 年龄:{{ person.age }}
<button @click="changePerson">修改person</button>

<script>
import { ref, reactive } from 'vue';
export default {
  setup() {
    let person = reactive({
      name: 'vue3',
      age: 18,
    });
    function changePerson() {
      console.log('person', person);
      person.name = '张三';
      person.age = 20;
    };
    return {
      person,
      changePerson,
    };
  },
};

img


数组类型响应式

reactive可以检测到通过脚标修改的元素,使元素成为响应式数据,而vue2中必须通过破坏性方法修改元素才能响应

function changePerson() {
  person.name = '张三';
  person.age = 20;
  person.hobby[0] = 'football';
};

img


总结

作用: 定义对象类型的响应式数据,不能定义基本类型响应式数据

语法: const 代理对象 = reactive(对象)
通过reactive代理的对象,能够作为响应式数据使用,可以深层代理数据,不需要.value写法获取对象属性,而是直接使用.属性读取对象属性
内部基于ES6的Proxy对象实现,通过Proxy对象代理对象中每个属性,从而实现数据代理
数组数据可以直接通过脚标进行修改元素,也能响应式


Vue3响应式原理

Vue2响应式原理

通过Object.defineProperty对对象中的属性进行劫持,当读取属性时,会调用getter方法,当修改属性时,会调用setter方法,从而实现数据劫持

let person = {
    name: '张三',
    sex: '男',
}
Object.defineProperty(person, 'value', {
    get() {
        console.log('get被调用了');
        return person.name;
    },
    set(newValue) {
        console.log('set被调用了');
        person.name = newValue;
    }
})

存在的问题:
1.新增属性、删除属性不能响应
2.通过下标修改元素不能响应

添加/删除属性进行响应式,应该使用$set/Vue.set$delete/Vue.delete方法,否则不会触发视图更新
对数组的操作通过调用vue包装的数组破坏性方法进行操作,如push/pop/shift/unshift/splice/sort/reverse,否则不会触发视图更新

methods: {
  add(){
    this.$set(this.person, 'hobby', 'football');
  },
  delete(){
    this.$delete(this.person, 'hobby');
  }
  changeArr(){
    this.arr.push(4);
  }
}

Vue3响应式原理

Proxy构造函数

使用Proxy构造函数创建原始对象的代理对象,读取代理对象的属性会调用代理对象的get方法,写入代理对象的属性会调用代理对象的set方法,在这些处理方法中对源数据进行处理。
参数:
1.代理目标对象 必填
2.配置对象 必填 可以使用{}占位

let person = {
  name: "张三",
  age: 18,
};
let proxy = new Proxy(person, {
  get(target, key) {
    console.log("get", key);
    return target[key];
  },
  set(target, key, value) {
    console.log("set", key, value);
    target[key] = value;
  },
});
console.log(proxy.name);
proxy.name = "李四";

proxy.name读取时调用get方法,proxy.name赋值时调用set方法
img

Proxy中的配置对象

代理对象组成:
Handler对象: 代理对象中处理函数
Target对象: 被代理对象
img

get处理函数

调用时机: 读取代理对象属性时调用
作用: 根据该函数的return读取源对象属性值

参数:
1.目标对象
2.被读取的属性名

get(target, key) {
  console.log("get", key);
  return target[key];
}

set处理函数

调用时机: 修改/新增代理对象属性时调用
作用: 修改/新增源对象属性值
返回值: 修改成功返回true, 修改失败返回false

参数:
1.目标对象
2.被修改的属性名
3.修改后的属性值

set(target, key, value) {
  console.log("set", key, value);
  target[key] = value;
},

deleteProperty处理函数

调用时机: 删除代理对象属性时调用
作用: 删除源对象属性

参数:
1.目标对象
2.被删除的属性名

返回值: 删除成功返回true, 删除失败返回false

deleteProperty(target, key) {
  return delete target[key];
},

删除代理对象属性时,调用deleteProperty处理函数,而后删除源对象属性,将删除结果return出去
img

操作代理对象,调用处理函数,处理函数中操作原始对象,从而修改原始对象数据
img


Reflect反射对象

Reflect: Reflect对象也是 ES6 为了操作对象而提供的新 API。
ECMA规范有意将Object中的方法,放到Reflect对象上。
ReflectObject中方法进行优化,像defineProperty方法具有返回值,明确调用definedProperty是否成功,而不关心具体异常的原因
Object调用definedProperty方式时,还要防止抛出异常,因此需要try...catch捕获异常,以免影响程序的正常执行。

例如,使用Object对对象进行操作时,方法出现错误只能使用try...catch捕获,而Reflect对象则提供了更优雅的处理方式。

let person = {
  name: "张三",
  age: 18,
};
try {
  Object.defineProperty(person, "hobby", {
    get() {
      return "football";
    },
  });
  Object.defineProperty(person, "hobby", {
    get() {
      return "football";
    },
  });
} catch (e) {
  console.log(e);
}
console.log("下段逻辑");

Object调用方法时报错,影响后面逻辑执行,只能使用try catch捕获错误
img


let person = {
  name: "张三",
  age: 18,
};
let r1 = Reflect.defineProperty(person, "hobby", {
  get() {
    return "football";
  },
});
let r2 = Reflect.defineProperty(person, "hobby", {
  get() {
    return "football";
  },
});
console.log(r1, r2);
console.log("下段逻辑");

使用Reflect调用defineProperty方法时,如果存在错误则会返回false,不会影响后面逻辑执行
img

Proxy中使用Reflect操作原始数据

let proxy = new Proxy(person, {
  get(target, key) {
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    Reflect.set(target, key, value);
  },
  deleteProperty(target, key) {
    return Reflect.deleteProperty(target, key);
  },
});

img


总结

vue3响应式实现原理:
Proxy(代理构造): 拦截对象属性的任意操作,包括,增删改查
Reflect(反射对象): 在Proxy处理函数中对原始对象进行操作

MDN介绍ProxyReflect

必须在setup函数中返回ref或者reactive函数的返回值才能响应式,否则无法响应式,即使返回对象的属性也会无法响应式

setup() {
  let person = reactive({
    firstName: '张',
    name: '三',
  });
  setInterval(() => {
    console.log('修改响应对象属性值');
    person.firstName = '王';
  }, 1000);
  return {
    firstName: person.firstName,
  };
},

img
img


当setup返回reactive函数返回值时,该返回值才是响应式
img


reactiveref的对比

reactiveref都可以使数据实现响应式

从定义角度对比:

reactive: 定义对象类型数据,基本数据类型无效
ref: 定义基本类型数据

注意:
ref也可以定义对象类型和数组类型数据,对于对象和数组类型,其内部也是借助reactive来实现响应式

从原理角度对比:

reactive: 通过Proxy代理对象来实现响应式(数据劫持),通过Reflect反射对象操作源数据
ref: 基本类型数据通过Object.definePropertygetset来实现响应式(数据劫持),对象和数据组借助reactive

从使用角度对比:

reactive: js操作数据和模板中读取数据都不需要.value
ref: js操作数据时需要.value,在模板中读取数据不需要.value


setup函数注意点

1.setup执行时机优于beforeCreate,因此无法在setup中使用this

setup函数在beforeCreate之前执行,此时组件实例对象还未创建,因此不能通过this访问组件实例对象

setup() {
  console.log('setup');
  console.log('this', this);
},
beforeCreate() {
  console.log('beforeCreate');
  console.log('this', this);
},

img

2.setup函数参数

第一个参数: Proxy类型对象,用于接收props属性中的参数

Proxy类型对象,对象中的属性为props接收的属性。因此需要在子组件中使用props属性接收父组件传递的参数,否则setup第一个参数为空的Proxy对象

export default {
  props: ['name'],
  setup(props) {
    console.log(props);
  },
};

img

当子组件没有使用props属性接收父组件传递的任何参数时,setup第一个参数为空的Proxy对象

export default {
  // props: ['name'],
  setup(props) {
    console.log(props);
  },
};

img


当模板中使用了未通过props接收该参数时,出现警告

<template>
  <div>
    <h2>学生姓名:{{ name }}</h2>
  </div>
</template>
<script>
export default {
  // props: ['name','age'],
};
</script>

name属性没有通过props接收,并且在模板中使用name属性,则出现警告
img


第二个参数: context上下文对象,用于接收attrsslotsemit属性

context上下文对象,对象中的属性为attrsslotsemit属性。

export default {
  setup(props, context) {
    console.log(context);
  },
};

img

attrs属性

vue2中的$attrs属性

attrs属性用于接收父组件传递的未通过props接收的参数

props: ['age']  

父组件传递过来的name属性,未使用props接收时,会存放到context.attrs属性中
img


emit属性

自定义hello事件

<template>
  <student @hello="showValue" name="张三" age="18"></student>
</template>
<script>
import Student from './components/Student.vue';
export default {
  components: { Student },
  setup() {
    function showValue(value) {
      console.log('触发了自定义事件', value);
    }
    return {
      showValue,
    };
  },
};
</script>

触发hello事件

context.emit('hello', '触发事件');

slots属性

vue2中的$slots属性

App组件,必须使用v-slot:插槽名指令指定插槽

<Student @hello="showValue" name="张三" age="18">
  <template v-slot:asd>
    <div>
      <p>这是插槽内容</p>
    </div>
  </template>
</Student>

Student组件

setup(props, context) {
  console.log(context.slots);
},

App组件中设置Student组件的asd插槽内容,Student组件setup的context对象的slots属性中存放了asd插槽内容,

slots存放的是插槽函数,函数名为插槽名,调用插槽函数返回虚拟节点
img
console.log(context.slots.asd()):
img


总结

1.setup优于beforeCreated执行,因此setup中的thisundefined
2.setup函数的参数
(1).props: 父组件传递的属性
(2).context: 上下文对象,包含attrsslotsemit属性
attrs: 值为对象,没有在props配置中声明的属性,会放到该对象中,相当于this.$attrs
slots: 值为对象,存放了插槽函数,调用插槽函数返回虚拟节点,相当于this.$slots
emit: 值为函数,用于触发自定义事件,相当于this.$emit


computed计算属性函数

vue3中仍然可以使用vue2的计算属性配置,vue2通过this获取setup返回的数据

<template>
  学生姓:
  <input type="text" v-model="person.firstName" />
  <br />学生名:
  <input type="text" v-model="person.name" />
  <br />
  学生全名: {{ fullName }}
</template>
<script>
import { reactive } from 'vue';
export default {
  computed: {
    fullName() {
      return this.person.firstName + this.person.name;
    },
  },
  setup() {
    let person = reactive({
      firstName: '张',
      name: '三',
    });
    return {
      person,
    };
  },
};
</script>

img


计算属性只读写法,入参为get方法的回调函数

vue3中的计算属性,引入computed计算属性函数,该函数接收一个回调函数,回调函数中书写数算逻辑return结果

import { reactive, computed } from 'vue';
export default {
  setup() {
    let person = reactive({
      firstName: '张',
      name: '三',
    });
    person.fullName = computed(() => {
      return person.firstName + person.name;
    });
    return {
      person,
    };
  },
};

计算属性读和写,入参为对象,对象中包含get和set属性

let fullName = computed({
  get() {
    return person.firstName + person.name;
  },
  set(value) {
    person.firstName = value.substring(0, 1);
    person.name = value.substring(1);
  },
});

img


watch监视函数

vue2中的watch

vue2中的watch

export default {
  watch:{
    sum(newVal, oldVal) {
      console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
    },
  },
  setup() {
    let sum = ref(1);
    return {
      sum,
    };
  },
};

img


vue3中的watch函数

watch函数接收三个参数,第一个参数为要监视的响应数据,第二个参数为回调函数,第三个为监视特性配置对象

watch的第一个参数类型可以是: ref,reactive对象,getter函数,多个类型数据源数组

数据源为ref变量

import { ref, watch } from 'vue';
export default {
  setup() {
    let sum = ref(1);
    watch(sum, (newVal, oldVal) => {
      console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
    });
    return {
      sum,
    };
  },
};

sum作为第一个参数
img

数据源为getter函数

getter函数结果作为watch回调函数参数,在getter函数中获取ref响应数据源使用.value
如下,newScore和oldScore分别是触发前和触发后getter函数的返回值

setup() {
  let sum = ref(1);
  let subject = ref('英语');
  watch(
    () => subject.value + ': ' + sum.value,
    (newScore, oldScore) => {
      console.log('score的值发生了变化', '新值', newScore, '旧值', oldScore);
    }
  );
  return {
    sum,
    subject,
  };
},

改变非响应数据时,getter函数不会触发,回调函数不会执行

setup() {
  let sum = ref(1);
  let subject = ref('英语');
  let name = '张三';
  watch(
    () => name + '| ' + subject.value + ': ' + sum.value,
    (score, oldScore) => {
      console.log('score的值发生了变化', '新值', score, '旧值', oldScore);
    }
  );
  function changeName() {
    name = '李四';
    console.log('name的值发生了变化');
  }
  return {
    sum,
    subject,
    changeName,
  };
},

只有修改了getter回调函数中用到的任意响应数据时,watch回调才会执行
img


数据源为reactive对象

setup() {
  let sum = 1;
  let subject = '英语';
  let name = '张三';
  let person = reactive({
    name,
    sum,
    subject,
  });
  watch(person, (newPerson, oldPerson) => {
    console.log('person的值发生了变化', '新值', newPerson, '旧值', oldPerson);
  });
  return {
    person,
  };
},

img

监听数据为对象类型时,即使对象中属性值发生了改变,并触发了回调函数,但是新值和旧值相同都是新值对象

vue2监视对象类型时,对象中属性值改变,也会触发回调函数,但是新值和旧值相同都是新值对象

img

原因: 由于监视的是一个对象,这个对象中的属性发生改变通过Proxy回调捕获到,但是新值和旧值仍是该对象的引用地址,因此新值和旧值相同

内存地址 0x1234: { name: "Alice", age: 30 }
          │
          │ 修改 age 属性
          ▼
内存地址 0x1234: { name: "Alice", age: 31 } // 仍是同一地址

watch 回调参数:
  newVal → 指向 0x1234
  oldVal → 指向 0x1234 // 所以值相同

监听reactive对象类型数据时,vue3新版本中开始支持了deep配置,之前的版本中默认为deep:truedeep配置无效

即监听了person对象,修改obj中的属性值改变时,也能触发回调函数

let person = reactive({
  name,
  sum,
  subject,
  obj: {
    a: 1,
    b: 2,
  },
});

在vue3版本中默认为deep: true

watch(
  person,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }
);

img


watch(
  person,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }
  , {deep: false}
);

修改了person.obj.a深度属性值,vue3版本开始可以使用deep属性控制是否深度监视
deep:false: 修改深度属性不监视对象的变化
img


在`vue3之前版本中,默认为deep:true,而deep:fales无效

当监听对象属性时,应该使用getter函数

在vue2中使用obj.属性名方式

watch(
  person.sum,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', newPerson, '旧值', oldPerson);
  }
);

报错,因为person.sum是基本数据类型,它不是refreactive对象、getter函数、多个类型数据源数组,即使使用了深度监视也无效
img


应该使用getter函数

setup() {
  let sum = 1;
  let subject = '英语';
  let name = '张三';
  let person = reactive({
    name,
    sum,
    subject,
  });
  watch(()=>person.sum, (newPerson, oldPerson) => {
    console.log('person的sum值发生了变化', '新值', newPerson, '旧值', oldPerson);
  });
  return {
    person,
  };
},

img

数据源为多个数据源数组

监视多个数据源,只要有一个数据源发生变化,就会执行回调函数

回调函数中两个参数,它们都是数组

第一个参数为新值数组,每个元素就是数据源的新值
第二个参数为旧值数组,每个元素就是数据源的旧值

setup() {
  let sum = ref(1);
  let msg = ref('hello watch');
  watch([sum, msg], (newVal, oldVal) => {
    console.log('sum的值发生了变化', '新值', newVal, '旧值', oldVal);
  });
  return {
    sum,
    msg,
  };
},

img


当回调函数只接收一个参数时,该参数为数组,每个元素为数据源的新值

当回调函数只有一个参数时,该参数也是一个数组,每个元素就是数据源的新值

setup() {
  let sum = ref(1);
  let msg = ref('hello watch');
  watch([sum, msg], ([newVal, oldVal]) => {
    console.log('sum的值发生了变化', '新值', newVal, oldVal);
  });
  return {
    sum,
    msg,
  };
},

img


watch的监听配置项

watch的第三个参数为配置项,配置项中可以配置immediatedeep属性

immediate属性为布尔值,表示是否立即执行回调函数,默认为false,不立即执行回调函数

deep属性为布尔值,表示是否深度监视

深度监视: 监视reactive对象类型时,当该对象中对象属性值发生改变时,也能触发回调函数

当使用getter函数监听对象时,需要使用deep: true,否则只能检测到该对象被整体替换(内存地址改变),无法检测到对象中的属性值改变

由于getter返回的是响应数据对象中对象,该对象并不是reactive函数直接返回的对象,那么该对象就是一个普通对象,因此需要配置deep: true才能深度监视

let person = reactive({
  name,
  sum,
  subject,
  obj: {
    a: 1,
    b: 2,
  },
});
watch(
  () => person.obj,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }
);

img

应使用deep: true配置深度监视对象属性值的改变

watch(
  () => person.obj,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }
  , {
    deep: true,
  }
);

img


总结

1.监视数据源有哪些: ref返回值(RefImpl对象),reactive返回值(Proxy对象),getter函数多数据源的数组
2.监视reactive定义的对象类型数据时,和vue2一样,对象中的属性改变了,触发回调方法,但新值和旧值相同,都为新值
3.监视reactive定义的对象类型数据时,默认为deep:true开启深度监视,在新的vue3版本中也可以设置deep:false取消深度监视
4.监视reactive定义的某一个非对象属性时,应该使用getter方法返回对象中的属性
5.使用getter函数返回reactive定义的某一个属性,该属性是对象时,该对象就是普通对象,deep默认为false,设置deep:true开启深度监视,才能检测到属性值的变化,否则只能检测到该对象整体被替换(内存地址发生改变)
6.不能在回调函数中修改监视属性值,否则产生递归调用,vue3中会报出超出最大递归次数
img


监视ref定义数据时.value问题

let person = ref({
  name,
  sum,
  subject,
  obj: {
    a: 1,
    b: 2,
  },
});

img

想要监视ref中的对象属性值改变有两种方式:
1.监视Ref对象.value 因为value值其实就是reactive定义的Proxy响应式数据
2.直接监视Ref对象,并开启深度监视

watch(
  person.value,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }
);
watch(
  person,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化', '新值', JSON.stringify(newPerson), '旧值', JSON.stringify(oldPerson));
  }, {
    deep: true,
  }
);

watchEffect函数

watchEffect函数和watch函数功能基本一致,都是用于监视数据源,当数据源发生变化时,执行回调函数,但watchEffect函数不需要指定监视的数据源,它会自动监视watchEffect回调函数中使用到的所有响应式数据,一旦使用到的某一个响应数据发生了改变,回调函数将会执行

watch: 既要指明监视数据,又要既定回调函数
watchEffect: 不需要指明监视数据,监视的回调函数中用到哪个响应式数据,就监视哪个响应式数据

watchEffect表现形式有点像computed,一旦所依赖到的响应数据发生改变,就会触发回调函数的执行
不同的是:
1.computed注重return返回值
2.watchEffect注重的是过程(回调函数的函数体)

let person = ref({
  name,
  sum,
  subject,
  obj: {
    a: 1,
    b: 2,
  },
});
watchEffect(() => {
  console.log('执行了监视器');
  person.value.obj.a;
});

挂载后立即执行一次回调函数,只要用到了任何响应数据,即使没有赋值给其他变量,也会触发回调函数的执行
回调函数中用到了person中深度属性a,因此只要a数据发生改变,就会触发回调函数的执行
img


watchEffect中使用异步函数监听响应数据无效

如下代码,watchEffect使用setTimeout异步回调函数监听了响应数据name的变化,但监听无效

let name = ref('');
let otherName = ref('');
watchEffect(() => {
  console.log('name 改变');
  setTimeout(() => {
    otherName.value = name.value;
  });
});
return {
  name,
  otherName,
};

只有初始化时执行一次监视回调,回调函数中使用异步函数,而在异步函数中使用到了响应数据则无法监听,因此监听回调不再执行
img


watchwatchEffect回调函数中使用到的setTimeout回调函数只执行一次

let name = ref('');
let otherName = ref('');
watchEffect(() => {
  console.log('name 改变');
  setTimeout(() => {
    console.log('otherName 改变');
    otherName.value = name.value;
  }, 3000);
});
return {
  name,
  otherName,
};

img

let name = ref('');
let otherName = ref('');
watch(name, (newValue)=>{
  console.log('name 改变');
  setTimeout(() => {
    console.log('执行了setTimeout');
    otherName.value = newValue;        
  },1000)
})

而使用watch进行监听,因为已经指定了监听数据,因此只要一改变就能执行监视回调从而执行了setTimeout的回调
img


总结watchEffect特点

1.页面挂载完成立即执行回调
2.无法获取旧值和新值
3.回调函数中用到了哪个响应数据,就监视哪个响应数据
4.可以深度监视响应属性值改变
5.只要用到了响应属性,即使不赋值给其他变量,也会监视该属性
6.watchEffectwatch都不返回回调函数的返回值
7.watchEffect回调函数中修改监视属性的值,不会触发回调函数执行,也就是不会造成递归调用。只有在回调函数外修改了监视属性才能触发回调函数

watchEffect(() => {
    console.log('执行了监视器');
    person.value.obj.a++;
  });

img
8.watchwatchEffect回调函数中使用setTimeout存在的问题
watchwatchEffect都是只能使setTimetout的回调执行一次
区别在watch可以执行多次,从而执行多次setTimeout回调
而watchEffect则不能执行多次,从而不能执行多次setTimeout回调


生命周期

img

渲染遇到组件: 必须指定挂载到的容器后(.mount(容器)),才渲染组件,走后续流程
vue2生命周期中new Vue()后就可以执行beforeCreatecreated

配置项回调函数

export default {
  setup() {
    let sum = ref(1);
    return {
      sum,
    };
  },
  // 创建app实例前
  beforeCreate() {
    console.log('beforeCreate');
  },
  // 创建app实例后,组合API加载完毕
  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');
  },
};

img

相比vue2的生命周期钩子,有两个钩子名称改变:
beforeDestroy -> beforeUnmount
destroyed -> unmounted

组合API生命周期

beforeCreate -> setup
created -> setup
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeUnmount -> onBeforeUnmount
unmounted -> onUnmounted

vue3设计中并没有提供beforeCreatecreated两个组合式API钩子,而是将setup组合式API作为它们两钩子的组合式API

import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue';
export default {
  setup() {
    let sum = ref(1);
    onBeforeMount(() => {
      console.log("onBeforeMount");
    });
    onMounted(() => {
      console.log("onMounted");
    });
    onBeforeUpdate(() => {
      console.log("onBeforeUpdate");
    });
    onUpdated(() => {
      console.log("onUpdated");
    });
    onBeforeUnmount(() => {
      console.log("onBeforeUnmount");
    });
    onUnmounted(() => {
      console.log("onUnmounted");
    });
    return {
      sum,
    };
  },
};

组合式API生命周期钩子执行优先于配置项生命周期钩子执行


Hook

Hook: 本质是一个函数,把setup中使用到Composition API进行了封装,在setup执行了该hook后,那么该组件也就加载了该hook所封装的Composition API

类似于vue2中的mixin

优势: 复用代码使setup中的逻辑更清晰

hook函数命名规范

hook函数名称必须以use开头,如usePointuseMouseusePoint

hook函数使用

img
1.在src目录下创建hooks文件夹,并在该文件夹下创建usePoint.js文件

2.在usePoint.js文件中编写hook函数

暴露一个匿名函数,该函数就是hook函数,包含了数据和组合api逻辑

import { reactive, onMounted, onBeforeUnmount } from 'vue';
export default function () {
  let point = reactive({
    x: 0,
    y: 0,
  });
  function position(e) {
    point.x = e.pageX;
    point.y = e.pageY;
    console.log(point);
  }
  onMounted(() => {
    window.addEventListener('click', position);
  });

  onBeforeUnmount(() => {
    window.removeEventListener('click', position);
  });
  return point;
}

3.在组件中使用hook函数,使用hook函数返回数据

<template>点击时坐标: Y:{{point.y}} X:{{point.x}}</template>
<script>
import userPoint from '../hooks/userPoint';
export default {
  setup() {
    let point = userPoint();
    return {
      point,
    };
  },
};
</script>

img


toReftoRefs

toRef函数

<template>
  <span>person信息: {{person}}</span>
  <hr />
  姓名: {{newName}}
  <hr />
  <button @click="newName = '王五'">修改姓名</button>
</template>

setup() {
  let person = reactive({
    name: '张三',
    age: 18,
  });
  return {
    person,
    newName: person.name,
  };
},

setup返回了响应式对象中属性name值,并使用newName变量接收,实际上这个newName就是普通的字符串,不是响应式数据,因此修改newName的值,不会触发视图更新
img

要想让newName变成响应式数据,并且和person中的name值同步修改,可以使用toRef函数

toRef函数接收两个参数,第一个参数是响应式对象,第二个参数是响应式对象中的属性名,返回一个响应式数据,这个响应式数据会与响应式对象中的属性值同步修改

setup() {
  let person = reactive({
    name: '张三',
    age: 18,
  });
  return {
    person,
    newName: toRef(person, 'name'),
  };
},

img

toRef函数的作用: 可以把响应式对象中的属性变成响应式数据并返回,并且该响应式属性和响应式对象中的属性值同步修改
img


toRefs函数

toRefs函数的作用: 可以把响应式对象中所有属性变成响应式数据,并且该响应式属性和响应式对象中的属性值同步修改

let person = reactive({
  name: '张三',
  age: 18,
});
console.log(toRefs(person));

将响应式对象中所有属性变成响应式数据
img


<template>
  <span>person信息: {{person}}</span>
  <hr />
  姓名: {{name}}
  <hr />
  <button @click="name = '王五'">修改姓名</button>
</template>
<script>
import { toRef, reactive,toRefs } from 'vue';
export default {
  setup() {
    let person = reactive({
      name: '张三',
      age: 18,
    });
    return {
      person,
      ...toRefs(person),
    };
  },
};
</script>

setup返回了响应式对象person,并返回了所有具有响应式功能的person属性
img


当模板中使用了未定义(setup未返回)的属性,该属性不是响应式数据,则出现警告

<template>
  <span>person信息: {{age}}</span>
  <hr />
  姓名: {{name}}
  <hr />
  <button @click="person.name = '王五'">修改姓名</button>
</template>
 
 <script>
import { reactive } from 'vue';
export default {
  setup() {
    let person = reactive({
      age: 18,
    });
    return {
      person,
      ...person
    };
  },
};
</script>

模板中使用了name属性,但是setup的返回的对象中并没有name属性,因此name不再是响应式数据,即使动态添加person对象中的name属性,也不会再次解构...person对象,因此name属性不是影响式数据
img


总结

1.toRef(): 创建一个ref对象,其value值就是响应式对象中的属性值,并且和响应式对象中的属性值保持同步
2.语法: toRef(对象,属性名)
3.使用场景: 想单独对响应式对象中属性进行响应式使用
4.toRefs(): 创建一个对象,对象中的属性值都是ref对象,并且和响应式对象中的属性值保持同步
5.toRef和toRefs函数操作的数据必须是响应式数据,否则它们加工过属性也不是响应式,但是原始数据是可以修改的

let person = {
  name: '张三',
  age: 18,
};
return {
  person,
  ...toRefs(person),
};

由此可见toRef和toRefs返回的包装数据地址仍然指向了原数数据,但由于原始数据不是响应式,不能触发vue重新解析模板,但是原始数据已经改变
img


shallowReactiveshallowRef

shallowReactive函数

shallowReactive函数作用: 功能和reactive函数相同,把一个对象转为响应式对象,浅层响应式处理
不同点:
1.shallowReactive只对对象类型数据的第一层属性进行响应式处理
2.shallowReactive只对数组类型数据中元素进行响应式处理,不会对元素内部数据进行响应式处理

<template>
  <span>person信息: {{person}}</span>
  <br />
  姓名: {{person.name}}
  <br />
  <button @click="person.name = '王五'">修改姓名</button>
  <br />
  个人爱好: {{person.hobby}}
  <br />
  <button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱好</button>
  <hr />
  书籍: {{books}}
  <br />
  <button @click="books.splice(0,1,'红楼梦')">修改书籍</button>
  <br />
  第三本书详细信息: {{books[2]}}
  <br />
  <button @click="books[2].author = '曹雪芹2'">修改第三本书作者</button>
</template>
<script>
import { shallowReactive } from 'vue';
export default {
  setup() {
    let person = shallowReactive({
      name: '张三',
      hobby: ['看电影', '看小说'],
    });
    let books = shallowReactive(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
    return {
      person,
      books,
    };
  },
};
</script>

对对象类型数据person中的name进行响应式处理:
img
不能对person中的hobby属性中数据进行响应式处理:
img
对数组类型中的元素进行响应式处理,不能对数组中的元素内部的数据进行响应式处理:
img


shallowRef函数

shallowRef函数作用: 将原始数据包装为RefImpl对象,当原始数据是基本数据类型时其功能和ref一样,如果是引用类型(对象和数组),shallowRef则不会将其转为响应式数据,只对基本数据类型进行响应式处理。

<template>
  <span>姓名: {{name}}</span>
  <br />
  <button @click="name = '李四'">修改姓名</button>
  <hr />
  <span>person信息: {{person}}</span>
  <br />
  <button @click="person.name = '王五'">修改姓名</button>
  <br />
  <hr />
  书籍: {{books}}
  <br />
  <button @click="books.splice(0,1,'红楼梦')">修改书籍</button>
</template>
<script>
import { ref,shallowRef } from 'vue';
export default {
  setup() {
    let name = shallowRef('张三');
    console.log('shallow-name', name);
    let person = shallowRef({
      name: '张三',
      hobby: ['看电影', '看小说'],
    });
    console.log('shallowRef-person', person);
    let refPerson = ref({
      name: '张三',
      hobby: ['看电影', '看小说'],
    });
    console.log('ref-person', refPerson);
    let books = shallowRef(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
    console.log('shallowRef-books', books);
    let refBooks = ref(['西游记', '三国演义', { name: '红楼梦', author: '曹雪芹' }]);
    console.log('ref-books', refBooks);
    return {
      name,
      person,
      books,
    };
  },
};
</script>

对象类型数据包装为RefImpl对象,但是value值就是普通对象,不是Proxy对象,因此不会对对象类型的数据进行响应式处理
数组也是引用类型,同样使用shallowRef包装的RefImpl对象的value就是普通数组,和ref函数创建的RefImpl对象的value只不同
img

只对基本数据类型进行响应式处理
img


注意

在使用shallowRefshallowReactive时,只是对某些属性不能响应式,但其值仍然被修改成功了,如果触发了vue重新解析模板,则会重新渲染页面,显示最新的值

如下: shallowRef对对象类型的数据不能响应式,但是修改其属性值是成功的,修改基本数据类型name时,由于name是响应式的数据,触发了vue重新渲染模板,因此刚刚修改的对象中的name值被重新渲染到页面中
img


总结

1.shallowReactive只处理对象最外层的响应式(浅响应),例如对象类型的第一层属性,和数组类型中元素值改变,而不能把数组元素内部的属性转变为响应式
2.shallowRef只处理基本数据类型,例如字符串,数字等,不能处理对象类型和数组类型内部数据响应式,如果对象类型和数组类型,则其value值就是普通对象和数组,而不是响应式对象和响应式数组
3.shallowRef({})时,如果对象的引用地址改变了,也会触发响应式,例如person = {name:'张三'}person的引用地址改变了,也会触发响应式


readonlyshallowReadonly

readonly函数

对象类型数据变成只读数据,无法修改属性值(深度限制)

<template>
  <span>对象: {{person}}</span>
  <br>
  <span>姓名: {{person.name}}</span>
  <br />
  <button @click="person.name += '!'">修改对象中的属性</button>
  <br />
  <span>个人爱好: {{person.hobby}}</span>
  <br />
  <button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱</button>
  <hr />
  <span>书籍: {{books}}</span>
  <br />
  <button @click="books.push('JavaScript权威指南')">修改书籍</button>
</template>
<script>
import { reactive, readonly } from 'vue';
export default {
  setup() {
    let person = reactive({
      name: '张三',
      hobby: ['看电影', '看小说'],
    });
    person = readonly(person);
    console.log('readonly-person', person);
    let books = reactive(['JavaScript高级程序设计', 'Vue.js实战']);
    books = readonly(books);
    console.log('readonly-books', books);
    return {
      person,
      books,
    };
  },
};
</script>

img

修改响应式对象类型属性以及深度属性时修改被限制,修改响应式数组类型数据时也会限制修改
img


let a = 1;
let readOnlyB = readonly(a);
a = ref(a);
return {
  a
};

readonly对对象类型数据才能生效,对基本类型数据无效,使用readonly包装基本数据后,仍然可以使用ref进行响应式
img


readonly处理普通对象后,返回值为Proxy对象实例

let person = {
  name: '张三',
  age: 18,
};
let readOnly = readonly(person);
console.log(readOnly); // Proxy(Object) {name: '张三', age: 18}

shallowReadonly函数

让对象类型数据变成只读数据,无法修改属性值(浅层限制)

<template>
  <span>对象: {{person}}</span>
  <br />
  <span>姓名: {{person.name}}</span>
  <br />
  <button @click="person.name += '!'">修改对象中的属性</button>
  <br />
  <span>个人爱好: {{person.hobby}}</span>
  <br />
  <button @click="person.hobby.splice(0,1,'打游戏')">修改个人爱</button>
  <hr />
  <span>书籍: {{books}}</span>
  <br />
  <button @click="books.push('JavaScript权威指南')">添加书籍</button>
  <br />
  <button @click="books[2].name += '!'">修改第三本书籍</button>
</template>
<script>
import { reactive, shallowReadonly } from 'vue';
export default {
  setup() {
    let person = reactive({
      name: '张三',
      hobby: ['看电影', '看小说'],
    });
    person = shallowReadonly(person);
    console.log('shallowreadonly-person', person);
    let books = reactive(['JavaScript高级程序设计', 'Vue.js实战', { name: 'Vue.js 3企业级应用开发实战', author: '柳伟卫' }]);
    books = shallowReadonly(books);
    console.log('shallowreadonly-books', books);
    return {
      person,
      books,
    };
  },
};
</script>

当修改对象第一层属性,和数组元素值时,无法进行响应式,但是修改对象内部属性和数组元素内部属性时,可以响应式修改
img


总结

1.readonlyshallowReadonly作用是限制源数据修改,和源数据是否是响应数据无关

let person = {
  name: '张三',
  hobby: ['看电影', '看小说'],
};
person = readonly(person);

也能对person普通对象进行限制
img

2.外部组件引入组件数据时,可以在组件中设置readonly或shallowReadonly,防止外部组件修改组件内部数据

3.使用readonly或shallowReadonly后,再对数据进行响应式处理无效,该数据仍然是只读数据

let person = {
  name: '张三',
  hobby: ['看电影', '看小说'],
};
person = shallowReadonly(person);
person = reactive(person);

一旦对数据进行修改的限制,那么该数据响应式无效
img

4.readonly和shallowReadonly都只能对对象类型数据生效
5.readonly和shallowReadonly处理普通对象后,返回值为Proxy对象实例


深度数据和浅度数据的理解

深度数据: 对象类型的深度属性,数组类型的元素内部属性
浅度数据: 对象类型的第一层属性,数组类型的元素


toRawmarkRaw

toRaw函数

toRawreactive包装过的响应式数据转换为普通数据,原始响应式数据仍然能够修改,但是不再响应
toRawref包装过的响应式数据不能转为普通数据,即使ref包装的Proxy数据也不会转为普通数据,仍然可以响应

<template>
  <span>person信息: {{person}}</span>
  <hr />
  姓名: {{name}}
  <hr />
  <button @click="changePerson">修改姓名</button>
  <hr />
  <span>城市: {{city}}</span>
  <button @click="changeCity">修改城市</button>
</template>
<script>
import { toRef, reactive, toRefs, toRaw } from 'vue';
export default {
  setup() {
    let city = toRef('北京');
    let person = reactive({
      name: '张三',
      age: 18,
    });
    function changeCity() {
      let rawCity = toRaw(city);
      console.log('rawCity', rawCity);
      // 仍然可以修改基本类型响应式数据
      rawCity.value = '上海';
    }
    function changePerson() {
      let rawPerson = toRaw(person);
      // 不能修改对象类型响应式数据,但通过toRaw()方法加工后,原始数据不再响应
      rawPerson.name += '!';
      console.log('rawPerson', rawPerson, 'rawPerson === person', rawPerson === person);
      console.log('person', person);
    }
    return {
      person,
      ...toRefs(person),
      city,
      changeCity,
      changePerson,
    };
  },
};
</script>

toRaw对对象类型数据不再响应:
img
toRaw对ref基本类型响应数据无效,仍然响应:
img


toRaw对ref加工过的对象类型数据无效,仍然可以响应

<template>
  <span>城市: {{city}}</span>
  <button @click="changeCity">修改城市</button>
</template>
<script>
import { ref, reactive, toRefs, toRaw } from 'vue';
export default {
  setup() {
    let city = ref({ city: '北京' });
    function changeCity() {
      let rawCity = toRaw(city);
      console.log('rawCity', rawCity);
      // 仍然可以修改基本类型响应式数据
      rawCity.value.city = '上海';
      console.log('city', city);
    }
    return {
      city,
      changeCity,
    };
  },
};
</script>

toRawref加工过的对象类型数据修改时无效,仍然可以使Proxy对象进行响应式
img


toRaw()ref()包装过的响应式数据无效,不能返回原始数据,而是返回了ref包装的响应式对象

let city = ref('北京');
function changeCity() {
  let rawCity = toRaw(city);
  console.log('rawCity', rawCity, 'city === rawCity', city === rawCity);
  // 对ref包装过的响应式数据不能返回原始数据  
  rawCity.value = '上海';
}

img


markRaw函数

<template>
  <span>person信息: {{person}}</span>
  <br />
  <button @click="addCar">为个人添加一辆车</button>
  <button @click="person.car.price = parseInt(person.car.price) + 10 + 'w'">修改车的价格</button>
  <br />
</template>
<script>
import { reactive, markRaw } from 'vue';
export default {
  setup() {
    let person = reactive({
      name: '张三',
      age: 18,
    });
    function addCar() {
      person.car = { name: '奔驰', price: '40w' };
    }
    return {
      person,
      addCar,
    };
  },
};
</script>

为person对象中追加car属性,那么car属性仍然响应式数据
img


当不想让后来添加的对象中的属性响应时,使用marRaw对象该对象进行标记,使该属性不再是响应式数据,但原始数据仍可以修改成功

function addCar() {
  person.car = markRaw({ name: '奔驰', price: '40w' });
}

img

同样markRaw标记的数组,数组中的元素属性值修改时,不会被响应式处理
img


function addTall() {
  person.tall = markRaw(2.0);
}
function changeTall() {
  person.tall = 1.88;
}

markRaw对基本数据类型进行标记,基本数据类型修改时,仍然会响应,因此markRaw只能使引用类型数据不再响应
img


总结

toRaw()proxy对象包装为原始对象,proxy对象由reactive()readonly()shallowReactive() 或者 shallowReadonly() 创建。修改原始对象值时,vue不再响应,除非其他响应式数据修改触发了vue重新渲染,此时才会显示原始对象最新值
使用场景: 限制响应式对象不再响应(整体限制),只能包装proxy对象,不能包装refImpl对象


markRaw(): 标记一个永远不是响应式对象,返回值为永不响应式对象,使该对象中所有属性或元素不再响应式,后续再对该对象进行reactive包装也不会具有响应式

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

markRaw对对象类型中属性和数组类型中元素生效,对基本数据类型无效
使用场景:
1.操作响应式对象时,不想对后续添加的数据进行响应式,可以使用markRaw标记(部分限制)
2.当渲染确定不可变的大数据列表时,可以对该大列表数据标记为markRaw,告诉vue不必再去监听该对象跳过响应式处理,从而提升性能

也适用于嵌套在其他响应性对象

const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

customRef函数: 自定义ref

创建一个自定义的ref,显示声明对其依赖追踪和更新触发的控制方式

如下需求: 修改input框的值,延迟1s同步到其他地方
常规做法: 使用watch监听输入框值的变化,在watch监听属性回调方法中使用setTimeout延迟1s修改其他属性值为输入框的值
这种做法并没有对name进行延迟响应渲染,而是延迟修改其他响应式数据实现

let name = ref('');
let otherName = ref('');
watch(name, newValue => {
  console.log('name 改变');
  setTimeout(() => {
    otherName.value = newValue;
  }, 1000);
});
return {
  name,
  otherName,
};

img


当不使用其他变量,使其1s延迟响应时,就需要使用customRef来实现该功能,可以自定义响应逻辑,实现真正意义的控制响应式延迟

创建一个自定义的响应式函数名字叫myRef
myRef返回customRef API的返回值
customRef API接收两个参数,一个是track,一个是trigger
track: 告诉vue什么时候开始追踪属性,在return响应式数据前调用
trigger: 告诉vue什么时候开始触发更新,当属性值发生改变时调用
customRef返回一个配置对象,配置对象中包含getset方法,当属性被访问时,调用get方法,当属性值发生改变时,调用set方法

<template>
  <input type="text" v-model="name" placeholder="姓名" />
  <br />
  <button @click="name+='$'">修改姓名</button>
  <br />
  <span>{{name}}</span>
</template>
<script>
import { ref, customRef } from 'vue';
export default {
  setup() {
    let timer;
    let myRef = value => {
      return customRef((track, trigger) => {
        return {
          get() {
            console.log(`读取了属性值: ${value}`);
            // 在get方法return的前一步告诉追踪器,return的属性被访问了,需要被追踪,当value只发生改变时,get方法会再次被调用
            track();
            return value;
          },
          set(newValue) {
            console.log(`修改了属性,旧值: ${value} 新值: ${newValue}`);
            value = newValue;
            clearTimeout(timer);
            // 通知vue重新解析模板
            timer = setTimeout(() => {
              trigger();
            }, 1000);
          },
        };
      });
    };
    let name = myRef('');
    return {
      name,
    };
  },
};
</script>

延迟1s通知vue重新解析模板
img

在通知vue重新解析模板前停止上一次的定时器回调立即中断,这样就实现了vue只渲染最近一次更新,可以解决渲染防抖问题
img


总结

customerRef可以让vue延迟解析(读取)响应式数据,本质上数据已经修改,只是延迟了通知vue重新解析模板,从而实现真正的延迟响应式


provideinject函数: 实现祖先组件向后代组件通信

官方推荐祖先向孙子以及后代组件通信使用provideinject,父组件向子组件通信使用props
img

使用方式

父组件使用provide提供数据

import { ref, provide } from 'vue';
export default {
  components: {
    Child,
  },
  setup() {
    let name = ref('张三');
    provide('name', name);
    return {
      name,
    }
  },
};

子组件或孙子组件使用inject接收数据

import {inject, provide} from 'vue';
export default {
  setup() {
    provide('son', 'son');
    return {
      name: inject('name'),
    };
  },
};

img

后代使用provide不能向祖先组件通信:
子组件提供数据:

provide('child', 'child');

父组件则无法接受后代提供的数据

setup() {
  return {
    child: inject('child'),
  };
},

祖先组件不能通过后代组件provide获取数据
img

总结

1.父组件向子组件通信使用props
2.父组件向deepChild(孙子以及孙子后代组件)通信使用provideinjectprovideinject也能实现父向子通信但官方并不推荐,而是推荐props
3.provideinject不直接支持后代向祖先通信,但可以通过父组件向子组件传递函数引用,通过子组件调用触发父组件函数执行实现子组件向父组件通信

父组件

<template>
  <div class="child">
    我是子组件
    <Son />
  </div>
</template>
<script>
import Son from './Son.vue';
import { provide } from 'vue';
export default {
  components: {
    Son,
  },
  setup() {
    function parentMethod(value) {
      console.log('触发父组件方法,接收参数:', value);
    }
    provide('parentMethod', parentMethod);
    return {};
  },
};
</script>
<style scoped>
.child {
  background-color: #f0f0f0;
  padding: 20px;
  border-radius: 10px;
}
</style>

子组件

<template>
  <button @click="parentMethod('子组件参数')">触发父组件方法</button>
</template>
<script>
import { inject } from 'vue';
export default {
  setup() {
    let parentMethod = inject('parentMethod');
    return {
      parentMethod,
    };
  },
};
</script>
<style scoped>
.son {
  background-color: #afd38c;
  padding: 10px;
}
</style>

img


响应式数据的判断

1.isRef 判断是否由ref函数创建的响应式数据
2.isReactive 判断对象是否由reactive函数成功创建的Proxy代理对象实例
3.isReadonly 判断对象是否由readonly函数成功创建的Proxy代理对象只读实例
4.isProxy 判断是否由reactivereadonly函数创建的Proxy代理对象实例

import { ref, reactive, isReactive, isReadonly, isProxy, isRef, readonly, shallowReadonly, } from 'vue';
export default {
  setup() {
    let sum = ref(1);
    let person = {
      name: '张三',
      age: 18,
    };
    let personReactive = reactive({
      name: '张三',
      age: 18,
    });
    let readOnly = readonly(person);
    let readOnlyReactive = readonly(personReactive);
    console.log('sum isRef', isRef(sum));
    console.log('personReactive isReactive', isReactive(personReactive));
    console.log('readOnlyReactive isReadonly', isReadonly(readOnlyReactive));
    console.log('readOnly isReadonly', isReadonly(readOnly));
    console.log('readonly(响应式对象) isProxy', isProxy(readOnlyReactive));
    console.log('readonly(普通对象) isProxy', isProxy(readOnly));
    console.log('shallowReadonly(普通对象) isProxy', isProxy(shallowReadonly(person)));

    return {
    };
  },
};

img

isReadonly(): readonly无法对基本类型数据进行只读,因此isReadonly(readonly(基本类型数据))返回false

let a = 1;
let readOnlyB = readonly(a);
console.log('isReadonly', isReadonly(readOnlyB)); // false

总结

reactive(普通对象)readonly(普通对象)返回值都是proxy对象,因此isProxy返回true


Composition API的优势

使用传统的Options API中,新增或修改一个功能时,需要在data、methods、computed、watch中来回修改,而使用Composition API则只需要在setup函数中修改即可,代码复用性更高
组合式API可以将功能代码进行归纳,并使用hook封装功能到一个函数中
img


vue3模板中可以直接使用$emit$attrs$slots$refs

获取组件实例对象

import {getCurrentInstance} from 'vue'
export default {
  setup(){
    console.log(getCurrentInstance());
    return {}
  },
};

使用getCurrentInstance()获取组件实例对象
img

模板中则隐式的获取到了ctx组件上下文对象,因此在模板中可以直接使用$emit等

<template>
  <div>
    <button @click="$emit('click', 'hello')">点击</button>
  </div>
</template>

vue3为子组件添加系统事件

vue3中不再推荐使用@系统事件.native修饰符,而是直接使用@系统事件

父组件

<template>
  父组件
  <Child @click="customEvent" />
</template>

在子组件标签中直接使用@系统事件="回调函数"

使用场景: 子组件中必须只有一个根元素

事件将添加到子组件的根元素上
img

img


子组件指定节点使用v-bind="$attrs",来触发事件

使用场景: 子组件有多个根元素,添加到指定节点上

<template>
  <div>子组件</div>
  <div v-bind="$attrs">子组件其他根节点</div>
  <div v-bind="$attrs">子组件其他根节点</div>
</template>

$attrs属性中包含了该组件的事件,使用v-bing将事件绑定到具体的元素上
img
img


子组件中使用$emit触发事件

使用场景: 指定子组件某些节点触发事件

<template>
  <div>子组件</div>
  <div @click="$emit('click')">子组件自定义事件</div>
</template>

img

出现警告提示: 在使用emit时应该使用emits属性声明事件名称,声明后不再出现警告

<template>
  <div>子组件</div>
  <div @click="$emit('click')">子组件自定义事件</div>
</template>
<script>
export default {
  emits: ['click'],
};
</script>

系统事件仍有冒泡机制

<template>
  <div @click="parentEvent">
    父组件
    <Child @click="customEvent" />
  </div>
</template>
setup() {
  function customEvent() {
    console.log('customEvent');
  }
  function parentEvent() {
    console.log('parentEvent');
  }
  return {
    customEvent,
    parentEvent,
  };
},

img


新的组件

Fragment组件

vue2模板标签中必须有一个根标签
vue3模板标签中可以没有根标签,vue3会自动创建一个Fragment组件作为虚拟根标签

当组件模板中有多个标签时,vue3会自动将多个标签渲染为Fragment虚拟组件
img


Teleport组件

Son组件中使用Dialog弹窗组件,Dialog弹窗组件会渲染到Son组件中
img
如果想让该弹窗脱离Son组件,传统做法使用定位布局对弹窗位置进行调整,定位元素参考包含块进行定位,而Son组件中不断添加相对定位元素,那么包含块的就会改变,因此定位元素则不再准确

Teleport组件可以将Dialog弹窗组件渲染到body标签中,Teleport组件的to属性指定渲染位置,这样减少了包含块和元素之间的层级嵌套,从而避免了定位元素位置不准确的问题

<template>
  <button @click="show = !show">展示/隐藏弹窗</button>
  <teleport to="body">
    <div v-if="show" class="dialog">
      我是弹窗组件:
      <p>弹窗内容</p>
      <p>弹窗内容</p>
      <p>弹窗内容</p>
    </div>
  </teleport>
</template>

将指定的元素使用teleport组件包裹起来,teleport组件会自动将元素移动到指定的位置
to: 指定要移动的元素位置(元素选择器)
img


使用teleport组件实现弹窗

img

Dialog弹窗组件

<template>
  <button @click="showDialog">展示弹窗</button>
  <teleport to="body">
    <div v-if="show" class="dialog">
      我是弹窗组件:
      <p>弹窗内容</p>
      <p>弹窗内容</p>
      <p>弹窗内容</p>
      <button @click="showDialog">隐藏弹窗</button>
    </div>
  </teleport>
</template>

<script>
import { ref, inject } from 'vue';
export default {
  setup() {
    let changeMask = inject('changeMask');
    let show = ref(false);
    function showDialog() {
      show.value = !show.value;
      changeMask();
    }
    return {
      show,
      showDialog,
    };
  },
};
</script>

<style>
.dialog {
  width: 200px;
  background-color: #72cce0;
  padding: 10px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
</style>

App组件

<template>
  <div class="app" :class="mask">
    我是父组件
    <Child />
  </div>
</template>

<script>
import Child from './components/Child.vue';
import { ref, provide } from 'vue';

export default {
  components: {
    Child,
  },
  setup() {
    let mask = ref('');
    function changeMask() {
      mask.value ? mask.value = '' : mask.value = 'mask';
    }
    provide('changeMask', changeMask);
    return {
      mask,
    };
  },
};
</script>
<style>
.app {
  background-color: #ccc;
  padding: 10px;
}
.mask {
  opacity: 0.5;
  pointer-events: none;
}
</style>

img


最佳实战:
1.在弹窗组件中定义遮罩层为整个body
2.为弹窗内容添加一个父元素
3.父元素添加遮罩层样式,以及控制显示和隐藏

App组件

<template>
  <div class="app">
    我是父组件
    <Child />
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child,
  },
};
</script>
<style>
.app {
  background-color: #ccc;
  padding: 10px;
}
</style>

Dialog弹窗组件:

<template>
  <button @click="showDialog">展示弹窗</button>
  <teleport to="body">
    <div v-if="show" class="mask">
      <div class="dialog">
        我是弹窗组件:
        <p>弹窗内容</p>
        <p>弹窗内容</p>
        <p>弹窗内容</p>
        <button @click="showDialog">隐藏弹窗</button>
      </div>
    </div>
  </teleport>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    let show = ref(false);
    function showDialog() {
      show.value = !show.value;
    }
    return {
      show,
      showDialog,
    };
  },
};
</script>

<style>
.mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
}
.dialog {
  width: 200px;
  background-color: #72cce0;
  padding: 10px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
</style>

img


Suspense组件

Suspense组件用于等待异步组件加载完成,在等待过程中显示fallback属性指定的内容

Suspense组件底层定义了两个插槽:defaultfallback
default: 默认插槽,用于显示正常内容
fallback: 备用插槽,用于显示加载过程的内容

默认情况下,组件需要等待其所有的子组件加载完成才会展示,例如,调整浏览器网速使之等待加载:
img
加载页面速度取决于最慢的组件然选速度,而后一并渲染组件


Suspense组件可以让组件在等待渲染期间展示备用组件

步骤:
1.使用Suspense组件包裹异步组件
2.使用defineAsyncComponent动态定义异步组件代替静态引入组件

<template>
  <div class="app">
    我是父组件
    <Suspense>
      <template #default>
        <Child />
      </template>
      <template #fallback>
        <div>加载中...</div>
      </template>
    </Suspense>
  </div>
</template>
<script>
// import Child from './components/Child.vue'; // 静态加载组件
import { defineAsyncComponent } from 'vue';
const Child = defineAsyncComponent(() => import('./components/Child.vue')); // 动态异步加载组件
export default {
  components: {
    Child,
  },
};
</script>

img


程序员控制组件等待加载时间

1.父组件仍然使用Suspense组件包裹异步组件并动态异步引入子组件
2.子组件中的setup则使用异步async关键字修饰,定义为异步方法,setup方法return时使用await关键字获取异步结果并返回

<template>
  <div class="child">
    我是子组件
    {{sum}}
  </div>
</template>
<script>
export default {
  async setup() {
    let sum = 0;
    let p = new Promise((resolve, reject) => {
      setTimeout(() => {
        sum = 123;
        resolve({ sum });
      }, 1000);
    });
    return await p;
  },
};

不再使用浏览器调整网速的方式延迟加载组件,而是自定义延迟加载组件时间
等待1s后,await p的结果并return出去
img


Vue3中其他改变

vu3中全局API的改变

Vue.xxx调整到app.xxx

Vue2 Vue3
Vue.config.xxx app.config.xxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties

其他改变

1.vue2中非组件化时data可以是一个对象,组件化时是一个函数,而vue3中是否为组件化都必须把data定义为函数
2.过渡类名的更改

vue2中过渡类名:

.fade-enter,
.fade-leave-to {
  opacity: 0;
}

vue3中过渡类名:

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

3.移除keyCode作为v-on的修饰符,同时不再支持config.keyCodes
4.移除v-on.native修饰符
vue2使用.native修饰为原生事件
vue3中默认为原生事件,不再需要.native修饰符,如果是自定义事件,如果父组件中为子组件自定义了事件,子组件应该使用emits配置项声明事件名称

父组件为子组件定义haha事件:

<template>
  父组件
  <Child @haha="customEvent" />
</template>

子组件应使用emits声明事件名称:

<template>
  <div>子组件</div>
  <div @click="$emit('haha')">子组件自定义事件</div>
</template>
<script>
export default {
  emits: ['haha'],
};

如果不使用emits声明事件,控制台则出现警告: 提示使用emits声明
img

5.移除filters
过滤器需要一个自定义语法,打破了大括号内表达式只能是js的表达式的设计,这不仅有学习成本而且有实现成本!官方推荐使用方法调用或计算属性去替换过滤器
例如: 使用过滤器时,{{date | dateFormat('YYYY-MM-DD')}}过滤器语法含义: |前面的结果作为|后面的函数的参数 这种语法含义和js不同

<div id="root">
    今天的日期是: {{date | dateFormat}}
    <br>
    今天的日期是: <input type="text" :value="currentDate | dateFormat('YYYY年MM月DD日 HH:mm:ss')">
</div>
<script type="text/javascript">
    new Vue({
        el: '#root',
        data: {
            currentDate: 1749202679233,
            date: 1749202679233
        },
        filters: {
            dateFormat(value, format='YYYY-MM-DD HH:mm:ss') {
                return dayjs(value).format(format);
            }
        }
    })
</script>
posted @ 2025-07-03 09:18  ethanx3  阅读(32)  评论(0)    收藏  举报