系统性学习vue-vue3
- Vue3简介
- 创建Vue3.0工程
- 分析工程结构(cli创建的)
- 安装开发者工具
- 初识setup
- ref函数
- reactive函数
- 回顾Vue2的响应式原理
- vue3响应式原理---Proxy
- reactive对比ref
- setup的两个注意点
- 计算属性与监视
- Vue3生命周期
- 自定义hook
- toRef与toRefs
- shallowReactive与shallowRef
- readonly与shallowReadonly
- toRaw与markRaw
- customRef
- provide与inject
- 响应式数据的判断
- [Composition API的优势](#Composition API的优势)
- 新组件
- 其他
Vue3简介
代号:One Piece
支持Typescript
创建Vue3.0工程
使用vue-cli创建
- 保证目前的vue-cli的版本在4.5.0以上
- 在对应目录下创建项目
创建期间报错了,看输出可能是node版本太低了
Node下载官网
还是报错,去掉淘宝镜像npm config set registry https://registry.npmjs.org/
可以了= =
- 运行 √
使用vite创建工程
(上一个项目就是这种)
优势:
- 开发环境中,无需打包操作。可快速的冷启动
- 轻量快速的热重载(HMR)
- 真正的按需编译,不再等待整个应用编译完成
- 创建工程
npm init vite-app <project_name>
- 安装依赖
npm i
- 运行
npm run dev
分析工程结构(cli创建的)
main.js
对比以前写法,可以看到相似性
但是如果使用原来写法,报错,根本引入不来Vue
js
// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from "vue";
import App from "./App.vue";
// createApp(App)-创建应用实例对象(类似于vue2中的vm,但比vm更"轻")
// .mount("#app")-挂载
createApp(App).mount("#app");
// vue2的写法
// new Vue({
// render: (h) => h(App),
// }).$mount("#app");
App.vue
组件中的模板结构可以没有根标签
其他就没有什么区别了
html
<template>
<!-- 组件中的模板结构可以没有根标签 -->
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
</template>
安装开发者工具
运行项目到Chrome,看到之前安装的开发者工具没有亮起
因为版本不对,需要重新下载安装支持Vue3的
直接去Chrome商店下载
哎~没梯子
没事还有第二种方法-离线下载
发现一个宝藏网站
安装完,亮起!
初识setup
- 是Vue3中一个新的配置项,值为一个函数
- setup是所有Composition API(组合API)"表演的舞台"
- 组件中所有用到的:数据、方法等等,均要配置在setup中
- setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用(常用)
html
<template>
<h1>信息:{{ name }}-{{ age }}</h1>
<button @click="sayHello">打招呼</button>
</template>
<script>
export default {
name: "App",
setup() {
// 数据
let name = "Qiu";
let age = 18;
// 方法
function sayHello() {
alert("Hello! " + name);
}
// 返回对象
return {
name,
age,
sayHello,
};
},
};
</script>
2)若返回一个渲染函数,则可以自定义渲染内容
html
<script>
import { h } from "vue";
export default {
name: "App",
setup() {
// 返回渲染函数
return () => h("h1", "Hello World!");
},
};
</script>
注意点:
- 是向下兼容的,也可以用data,methods等Vue2配置,但不要混用vue2和vue3配置,因为在setup中不能读取到data或methods中数据,且setup优先级要高
- setup不能是一个async函数,因为返回值不再是return的对象,而是promise,模板看不到return对象中的数据
ref函数
此ref非之前学的ref属性
之前写的代码展示目前没有问题,但是数据并没有响应式
要想实现响应式,需要将数据用ref包装
js
import { ref } from "vue";
处理基本类型
js
// 数据
let name = ref("Qiu");
let age = ref(18);
在函数中控制台输出name
RefImpl(reference implement)引用实现对象
其中setter和getter放在了原型对象中,可以类似理解为vm中的_data
修改数据就要通过value改
js
function changeInfo() {
name.value = "CAI";
age.value = "19";
}
而模板中不需要.value
因为解析时会自动提取RefImpl对象的value
处理对象类型
js
let school = ref({
name: "TUST",
add: "TJ",
});
输出school.value
,看到并不是RefImpl对象,而是Proxy对象
因此修改对象数据应该school.value.name = "TJKJDX";
reactive函数
作用:定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)
const 代理对象 = reactive(源对象)
reactive定义的响应式数据时"深层次的"(无论有几层都能定义到)
内部基于ES6的Proxy实现,通过代理对象操作源对象内部数据进行操作
js
import { ref, reactive } from "vue";
//......
let school = reactive({
name: "TUST",
add: "TJ",
});
输出school
,直接就是Proxy对象
修改数据就相比要简单school.name = "TJKJDX";
由此可见,ref函数碰到对象时,也会用reactive将对象包装为Proxy
reactive也可以处理数组对象
回顾Vue2的响应式原理
- 实现原理:
对象类型:通过Object.definedProperty()
对属性的读取、修改进行拦截(数据劫持)
数组类型:通过重写更新数组的一系列方法来实现拦截- 存在问题:
1)新增属性、删除属性,界面不会更新
2)直接通过下标修改数组,界面不会自动更新
vue3响应式原理---Proxy
试验一下看看vue3会有上面的问题么
html
<h1>信息:{{ school.name }}-{{ school.add }}-{{ school.age }}</h1>
<button @click="changeInfo">修改信息</button>
js
setup() {
//数据
let school = reactive({
name: "TUST",
add: "TJ",
});
function changeInfo() {
school.age = 60;
}
// 返回对象
return {
school,
changeInfo,
};
},
直接看结果
所以vue3新增属性是响应式的
模拟Vue3的响应式
js
let person = {
name:"Qiu",
age:18
}
// 模拟vue3中实现响应式
const p = new Proxy(person,{
get(target,propName){
console.log("捕获到获取了",propName);
return target[propName];
},
set(target,propName,value){
console.log(`捕获到修改了${propName},修改为${value}`);
target[propName] = value;
},
// 添加了属性删除回调
deleteProperty(target,propName){
console.log("捕获到删除了",propName);
return delete target[propName];
}
});
对数据进行增删改查,可以看到都是可以进行响应式的
但其实真正vue3中并不是target[propName] = value;
直接对源数据进行操作
而是使用window上的一个属性Reflect
js
// 模拟vue3中实现响应式
constp = new Proxy(person,{
// 读取属性回调
get(target,propName){
console.log("捕获到获取了",propName);
// return person[propName];
return Reflect.get(target,propName);
},
// 修改、添加属性回调
set(target,propName,value){
console.log(`捕获到修改了${propName},修改为${value}`);
// person[propName] = value;
return Reflect.set(target,propName,value);
},
// !添加了属性删除回调
deleteProperty(target,propName){
console.log("捕获到删除了",propName);
// return delete target[propName];
return Reflect.deleteProperty(target,propName);
}
});
reactive对比ref
从定义数据角度对比:
- ref用来定义:基本类型数据
- reactive用来定义:对象或数组数据类型
- 备注:ref也可以用来定义对象或数组数据类型,它内部会自定通过reactive转为代理对象
从原理角度对比:
- ref通过
Object.definedProperty()
的get与set来实现响应式(数据劫持)- reactive通过使用
Proxy
来实现响应式(数据劫持),并通过Reflect
操作源对象内部的数据从使用角度对比:
- ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
- reactive定义的数据:操作数据与读取数据-均不需要.value
setup的两个注意点
setup的执行时机:在beforeCreate之前 执行一次,this是undefined
setup的参数:
- props:值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于vue2中的
this.$atters
- slots:收到的插槽内容,相当于vue2中的
this.$slot
- emit:分发自定义事件的函数,相当于vue2中的
this.$emit
注意:vue3中建议插槽内容template使用
v-slot:xxx
,因为可能会有兼容性问题
计算属性与监视
computed函数
与vue2中的computed功能配置一致
js
import { ref, reactive, computed } from "vue";
//....
// 计算属性
let fullName = computed(() => {
return person.firstName + person.lastName;
});
// 完整写法(考虑读和写)
let fullName = computed({
get() {
return person.firstName + "-" + person.lastName;
},
set(value) {
const nameArr = value.split("-");
person.firstName = nameArr[0];
person.lastName = nameArr[1];
},
});
//....
watch监视
总结一下下面这些情况:
主要是看watch函数的第一个参数类型
- 如果是RefImpl类型的,即用ref定义的数据,默认不开启深度监视(也没必要开启),监视value值内容
- 如果是Proxy类型对象,即用reactive定义的数据,就会强制开启深度监视,监视所有属性值(但oldValue没有用)
- 如果是函数或函数数组,返回某一个数据中的属性,默认不开启深度监视,监视当前返回属性,如为object需开启深度监视
所以当我们监视一个用ref定义的object时,需要.value或者改为深度监视(细品)
有六种情况
js
// 情况一:监视ref所定义的一个响应式数据
watch(sum, (newValue, oldValue) => {
console.log("sum变化了", newValue, oldValue);
});
// 完整写法
// deep无效(看情况三)
watch(
sum,
(newValue, oldValue) => {
console.log("sum变化了", newValue, oldValue);
},
{ immediate: true, deep: true }
);
// 情况二:监视ref所定义的多个响应式数据
// newValue:[Object]
watch([sum, msg], (newValue, oldValue) => {
console.log("监视到变化了", newValue, oldValue);
});
// 情况三:监视reactive所定义的一个响应式数据的全部属性
// 强制开启了深度监视,deep配置无效
// 有个坑:此处无法正确获取oldValue
watch(person, (newValue, oldValue) => {
console.log("person变化了", newValue, oldValue);
});
// 情况四:监视reactive所定义的一个响应式数据的某一个属性
watch(
() => person.age,
(newValue, oldValue) => {
console.log("person.age变化了", newValue, oldValue);
}
);
// 情况五:监视reactive所定义的一个响应式数据的某些属性
watch([() => person.age, () => person.name], (newValue, oldValue) => {
console.log("person某些属性变化了", newValue, oldValue);
});
// 特殊情况(person.job:Object)
// 又一坑:监视数据中的Object属性,要加上deep,什么鬼
watch(
() => person.job,
(newValue, oldValue) => {
console.log("person.age变化了", newValue, oldValue);
},
{ deep: true }
);
watchEffect函数
不用指定监视哪个属性,只要回调函数中使用到的数据发生变化,就直接重新执行回调函数,默认开启immediate(好智能~)
js
// school.name发生变化时,回调执行
watchEffect(() => {
console.log("===watchEffect===", school.name);
});
Vue3生命周期
和Vue2区别的地方:
- Vue2中是在created之后检测是否有el配置,而Vue3是在一开始就检测配置是否完整
- 将beforeDestory和destoryed更名为beforeUnmount和ummounted
使用组合式API使用生命周期钩子,即写在setup中(需要引入)(麻烦, 一般不用)
优先级要比配置项钩子高
官网文档
beforeCreate
======>setup()
created
==========>setup()
beforeMount
=======>onBeforeMount
mounted
==========>onMounted
beforeUpdate
======>onBeforeUpdate
updated
==========>onUpdated
beforeUnmount
=====>onBeforeUnmount
unmounted
========>onUnmounted
自定义hook
本质是一个函数,把setup函数中使用的Composition API进行了封装
类似于混合 mixin
js
/**
* useMousePos.js
* 鼠标点击位置功能相关
*/
import { reactive, onMounted, onBeforeUnmount } from "vue";
export default function () {
let pos = reactive({ x: 0, y: 0 });
function getMousePos(event) {
pos.x = event.pageX;
pos.y = event.pageY;
}
onMounted(() => {
window.addEventListener("click", getMousePos);
});
onBeforeUnmount(() => {
window.removeEventListener("click", getMousePos);
});
return pos;
}
这样就可以直接引用使用了
js
import useMousePos from "./hooks/useMousePos";
setup() {
// ...
let pos = useMousePos();
//...
}
toRef与toRefs
作用:创建一个ref对象,其value值指向另一个对象中的某个属性
语法:
const name = toRef(person, name)
应用:要将响应式对象中的某个属性单独提供给外部使用
扩展:
toRefs
和toRef
功能一直,但可以批量创建多个ref对象
当我们希望在模板中直接使用某个变量时
html
<h1>学校名称:{{ mySchoolName }}</h1>
js
setup() {
let school = reactive({
name: "TUST",
add: "TJ",
teacher: {
name: "LIU",
age: "40",
},
});
return {
mySchoolName: school.name,
};
}
可以看到mySchoolName
仅仅是一个字符串数据,并没有响应式,修改school中的数据mySchoolName并不会改变
那用ref包装呢
html
mySchoolName: ref(school.name),
确实可以变成ref类型,相当于重新定义了一个ref类型对象,与school.name并不是同一个引用,数据还是不互通的
这就要用到toRef 了,这样数据就与school.name为同一引用了
mySchoolName: toRef(school, "name"),
如果有多个这样的需求,就用toRefs
html
return {
mySchoolName: toRef(school, "name"),
...toRefs(school),
};
会返回所有属性改为ref后的对象集合
以上记录的组合式都是比较常用的
shallowReactive与shallowRef
shallowReactive:浅层次的响应式
这样teacher内数据不会是响应式的
js
let school = shallowReactive({
name: "TUST",
add: "TJ",
teacher: {
name: "LIU",
age: "40",
},
});
shallowRef:只处理基本数据类型的响应式,不进行对象内部的响应式处理
shallowRef传入基本类型与ref没有区别
传入对象类型,就不会在内部使用reactive
用于性能优化(自己悟吧~)
readonly与shallowReadonly
用readonly包装的响应式所有数据都不允许修改(深只读)
用shallowReadonly包装的响应式只会控制浅层的数据不进行修改(浅只读)
toRaw与markRaw
toRaw与reactive是反向操作,会将Proxy响应式对象还原为普通对象(RefImpl不行)
当向Proxy响应式对象追加一个深层次的数据,且并不需要后续修改,如果正常追加,就也会成为响应式,没必要
如下,使用markRow包裹的对象就不会成为响应式,添加的car就不会成为响应式数据
js
let car = {name:"大众",price:20}
person.car = markRow(car)
customRef
贴个完整代码,比较难理解,
应该是为了暴露出getter和setter,方便进行自定义操作
修改数据时工作流程:
① value = newValue
修改value值为更改后数据
② 走get()
获取最新数据(需要添加track()
)
③ trigger()
重新解析模板
html
<template>
<input type="text" v-model="keyWord" />
<h4>{{ keyWord }}</h4>
</template>
<script>
import { ref, customRef } from "vue";
export default {
name: "CustomRefDemo",
setup() {
// let keyWord = ref(""); // 精装房
let keyWord = myRef(""); // 毛坯房 需要自己根据自己需求装修
// 自定义ref
function myRef(value) {
return customRef((track, trigger) => {
return {
get() {
console.log("从myRef中读取了数据");
track(); // 通知vue追踪value数据改变
return value;
},
set(newValue) {
console.log("myRef中数据修改了");
value = newValue; // 修改数据
trigger(); //通知vue重新解析模板
},
};
});
}
return {
keyWord,
};
},
};
</script>
<style></style>
provide与inject
作用:实现祖组件及其后代组件间通信(就不用逐层传了)
祖组件有一个provide选项来提供数据,孙组件有一个inject选项来开始使用这些数据
其实不止孙组件,所有后代组件都可以用inject获取数据
js
// 祖组件
provide("data",data);
js
// 后代组件
let data = inject("data");
响应式数据的判断
isRef()
isReactive()
isReadonly()
isProxy()
-- 检查是否为reactive或readyonly包装的对象
Composition API的优势
- Vue2中使用的是OpitionsAPI(配置API),即将需求代码放在data,methods等中,那实现一个功能的数据、方法等都要拆分放到这些配置项中,会比较混乱
- Vue3的组合式API就可以将每个功能的相关放到一起
新组件
Fragment
在Vue2中:组件必须有一个根标签
而在Vue3中组件可以没有根标签,其实内部是将多个标签包含在一个Fragment虚拟元素中
优点:减少标签层级,减小内存占用
Teleport
如图,在Son组件中使用了Dialog组件来创建弹窗,但是希望是按钮显示在Son组件中 而弹窗显示在外部
就需要在Dialog组件中使用teleport传送组件,将需要传送出去的结构包裹在teleport标签中,并指定要传送的位置
html
<template>
<div>
<button @click="isShow = true">点我弹个窗</button>
<teleport to="body">
<div v-show="isShow" class="dialog">
<h3>我是一个弹窗</h3>
<h4>balabalabalabalabalabalbala</h4>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
</div>
</template>
Suspense
一般我们引入组件import Child from "./components/Child.vue";
为静态引入组件 ,这样引入父组件与子组件是互相等待同时展示出来的
另一种引入是动态/异步引入组件
js
import {defineAsyncComponent} from "vue";
const Child = defineAsyncComponent(() => import("./components/Child.vue"));
这样就是谁先准备好了就先展示谁
但是这样也会有一个问题
当页面上只展示出来父组件,而子组件还在加载中时,则完全不知道会有子组件存在,而导致忽略一些东西
这就需要使用Supense组件,将动态引入的组件包裹
html
<div class="app">
<h3>我是APP组件</h3>
<Suspense>
<template v-slot:default>
<Child></Child>
</template>
<template v-slot:fallback>
<h3>稍等。。。加载中。。。</h3>
</template>
</Suspense>
</div>
当使用异步引用和suspend组合技时,setup中return的就可以是Promise实例对象
其他
全局API的转移
vue2中我们需要注册全局组件和注册全局指令是使用全局API去配置
js
// 全局注册组件
Vue.component("comp",comp);
// 全局注册指令
Vue.directive('focus',{
inserted: el => el.focus()
})
而Vue3中对这类API做了调整,将Vue.xxx
调整为app.xxx
有一个需要注意的是原来的Vue.config.productionTip
在Vue3中已经移除
还有一个特殊的Vue.prototype
调整为app.config.globalProperties
其他改变
- data选项必须被声明为一个函数
- 过渡类型的更改(就是加了个-from)
css
// vue2
.trans-enter,
.trans-leave-to {
transform: translateX(-100%);
}
.trans-enter-to,
.trans-leave {
transform: translateX(0);
}
css
//vue3
.trans-enter-from,
.trans-leave-to {
transform: translateX(-100%);
}
.trans-enter-to,
.trans-leave-from {
transform: translateX(0);
}
- 移除了keyCode作为v-on的修饰符,同时也不再支持config.keyCodes(自定义别名按键)
- 移除v-on.native修饰符(Vue3中通过不声明事件指定为原生事件)
- 移除过滤器 filter (建议使用计算属性或方法调用)
- ...
完结撒花~