记一次vue3 vue2 差异分享
1、响应式
响应式:这是一个比较模糊的概念。通常可以理解成对某些操作有所反应。
vue2和vue3响应式原理的区别
- 无法检测到对象属性的新增或删除(vue2提供了vue.set方法来解决)
- 直接通过下标修改数组,无法监听数组的变化,
- 深度监听,层层处理,影响性能,性能不好,需要对每一个key循环递归处理,特别是处理大数据尤为明显
- Object.defineproperty()每调用一次都只能对对象的某一个属性进行数据劫持,所以要采用循环遍历,代码写起来比较麻烦。
Vue2时代Option Api ,data、methos、watch.....分开写,这种是碎片化的分散的,代码一多就容易高耦合,维护时来回切换代码是繁琐的!
Vue3时代Composition Api,通过利用各种Hooks和自定义Hooks将碎片化的响应式变量和方法按功能分块写,实现高内聚低耦合
形象的讲法:Vue3自定义Hooks是组件下的函数作用域的,而Vue2时代的Mixin是组件下的全局作用域。全局作用域有时候是不可控的,就像var和let这些变量声明关键字一样,const和let是var的修正。Composition Api正是对Vue2时代Option Api 高耦合和随处可见this的黑盒的修正,Vue3自定义Hooks是一种进步。
2. 响应式api
Vue3 提供了两种方式构建响应式数据:ref 和 reactive
2.1 ref & reactive
ref 用于构建简单值的响应式数据,比如String,Number,基于 Object.defineProperty 监听 value 值,原理是将普通的值转化为对象,并且在获取和设置值时可以增加依赖收集和触发更新功能
// ref 源码部分
function ref(value){
return createRef(value)
}
function convert(rawValue){
return isObject(rawValue) ? reactive(rawValue) : rawValue
}
// shallw
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
export class RefImpl {
private _rawValue: any;
private _value: any;
public dep;
public __v_isRef = true;
constructor(value) {
this._rawValue = value;
// 看看value 是不是一个对象,如果是一个对象的话
// 那么需要用 reactive 包裹一下
this._value = convert(value);
this.dep = createDep();
}
get value() {
// 收集依赖
trackRefValue(this);
return this._value;
}
set value(newValue) {
// 当新的值不等于老的值的话,
// 那么才需要触发依赖
if (hasChanged(newValue, this._rawValue)) {
// 更新值
this._value = convert(newValue);
this._rawValue = newValue;
// 触发依赖
triggerRefValue(this);
}
}
}
let num1 = ref(111)
// Vue 3.0 内部将 ref 悄悄的转化为 reactive
let num1 = reactive({
value: 111
})
可以看到,ref方法将这个字符串进行了一层包裹,返回的是一个RefImpl类型的对象,译为引用的实现(reference implement),在该对象上设置了一个不可枚举的属性value,所以使用name.value来读取值。
ref通常用于定义一个简单类型,那么是否可以定义一个对象或者数组?
const param = ref({
name: 'lili',
age: 25
})
console.log(param, param.value.name)
控制台可以看到,对于复杂的对象,值是一个被proxy拦截处理过的对象,但是里面的属性name和age不是RefImpl类型的对象,proxy代理的对象同样被挂载到value上,所以可以通过obj.value.name来读取属性,这些属性同样也是响应式的,更改时可以触发视图的更新
通过上面ref的使用案例,起始不管是复杂引用类型,如array,object等,亦或者是简单的值类型string,number都可以使用ref来进行定义,但是,定义对象的话,通常还是用reactive来实现
reactive 用于构建复杂的响应式数据,不能定义普通类型,基于 Proxy 对数据进行深度监听
reactive 参数必须是对象(json 或 Array),不能定义普通类型
【ref 与reactive的区别与联系】
一般来说,ref被用来定义基本数据类型,reactive定义引用数据类型
ref定义对象时,value返回的是proxy,reactive定义对象时返回的也是proxy,而这确实存在一些联系,ref来定义数据时,会对里面的数据类型进行一层判断,当遇到复杂的引用类型时,还是会使用reactive来进行处理
2.2 toRef & toRefs
toRef
接收两个参数target和attr,target是一般是reactive的响应式对象,attr是对象的属性,返回响应式变量(采用引用的方式,修改响应式数据,会影响原始数据,并且数据发生改变)
<template>
<div>{{ name }}:{{ data.age }}:{{ pq }}</div>
<br />
<button @click="change2">职业</button>
</template>
<script>
import { reactive, toRef } from '@vue/reactivity';
export default {
name: 'App',
setup() {
let data = reactive({
name: 'xiaozhi',
age: 23,
job: {
p: {
zhiye: '打工人'
}
}
});
function change2() {
data.name = 'xiaobai';
data.age = 34;
data.job.p.zhiye = 'hh';
}
return {
data,
name: toRef(data, 'name'),
pq: toRef(data.job.p, 'zhiye'),
change2
};
}
};
</script>
<style scoped></style>
toRefs:批量处理
作用将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象,提供给外部使用
批量处理只能处理一层数据,深层的单独取出,在setup()中可以用return { ...toRefs(object)}的方式,将整个响应式对象object的所有属性提供给外部使用。
<script>
import { reactive, toRefs } from 'vue'
export default {
setup () {
const data = reactive({
list: [],
count: 0,
title: ''
})
return {
...toRefs(data)
}
}
}
</script>
2.3 watch & watchEffect
watch
watch 的功能和之前的 Vue 2.0 的 watch 是一样的。和 watchEffect 相比较,区别在 watch 必须指定一个特定的变量,并且不会默认执行回调函数,而是等到监听的变量改变了,才会执行。并且你可以拿到改变前和改变后的值
watch有三个参数:
参数1:监听的参数
参数2:监听的回调函数
参数3:监听的配置(immediate)
<template>
<h2>我是TestA组件</h2>
<h2>当前求和为:{{sum}}</h2>
<button @click="sum++">点击按钮sum +1</button>
<h2>现在页面展示信息:{{info}}</h2>
<button @click="info = info + 'test组件'">点击按钮</button>
<div>
<div>姓名:{{obj.name}}</div>
<div>年龄:{{obj.age}}</div>
<button @click="obj.age++">点击修改年龄</button>
<button @click="obj.name = '李四'">点击修改姓名</button>
</div>
</template>
<script>
import { reactive, ref, watch } from 'vue'
export default {
name: 'TestA',
setup () {
// 数据
const sum = ref(0)
const info = ref('我是')
const obj = reactive({
name: 'ls',
age: 20
})
// 监视属性
watch(sum, (newValue, oldValue) => {
// 回调函数形式
console.log('求和的值变了', '变化后的值是' + newValue, '变化前的值是' + oldValue)
})
watch([sum, info], (newValue, oldValue) => {
// 回调函数形式
console.log(newValue, 'new=value', oldValue)
console.log('求和的值变了', '变化后的值是' + newValue, '变化前的值是' + oldValue)
})
watch(obj, (newValue, oldValue) => {
// 回调函数形式
// watch属性是强制开启深度监视的,无论数据有多少层,只要数据一改变,在Vue3中都是能被监视到,
// 但是在Vue2中,如果不开启深度监视的话,watch属性是无法监视到深层次数据的改变的
console.log(newValue, 'obj=new=value', oldValue)
})
watch(() => obj.age, (newValue, oldValue) => {
// 监听响应书数据中一个数值的改变
console.log(newValue, 'new=value', oldValue)
})
// 返回对象
return {
sum,
info,
obj
}
}
}
</script>
监视reactive所定义的响应式数据中的某一个值和监视reactive所定义的响应式数据中的一些数据的改变区别于直接对reactive所定义的响应数据中所有数据进行监视,在默认情况下,监视reactive所定义的响应数据中所有数据是开启深度监视的,也就是说,无论数据在第几层,都能监视到,但是最后情况,是针对其中某一个值或某一些值进行监视的,如果还要监视其属性下的更深层的值,是要开启深度监视的,否则无法监视得到
watchEffect
- 它是立即执行的,在页面加载时会主动执行一次,来收集依赖
- 不需要传递需要侦听的内容,它可以自动感知代码依赖,只需要传递一个回调函数
- 它不能获取之前数据的值
- 它的返回值用来停止此监听
<template>
<div>
<h1>{{state.search}}</h1>
<button @click="handleSearch">改变查询字段</button>
</div>
</template>
<script>
import {reactive ,watchEffect } from 'vue'
export default {
setup(){
let state=reactive({
search:Date.now()
})
watchEffect(
()=>{
console.log(`监听${state.search}`)
}
)
const handleSearch=()=>{
state.search=Date.now()
}
return{
state,
handleSearch
}
}
}
</script>
watchEffect 函数返回一个新的函数,我们可以通过执行这个函数或者当组件被卸载的时候,来停止监听行为
setup() {
let timer = null
let state = reactive({
search: Date.now()
})
// 返回停止函数
const stop = watchEffect((onInvalidate) => {
console.log(`监听查询字段${state.search}`)
})
const handleSearch = () => {
state.search = Date.now()
}
setTimeout(() => {
console.log('执行 stop 停止监听')
stop() // 2 秒后停止监听行为
}, 2000)
return {
state,
handleSearch
}
}
watchEffect 的回调方法内有一个很重要的方法,用于清除副作用。它接受的回调函数也接受一个函数 onInvalidate。重要的是它将会在 watchEffect 监听的变量改变之前被调用一次
export default {
setup () {
const state = reactive({
search: Date.now()
})
// 返回停止函数
const stop = watchEffect(
(onInvalidate) => {
console.log(`监听查询字段${state.search}`)
onInvalidate(
() => {
console.log('执行 onInvalidate')
})
})
const handleSearch = () => {
state.search = Date.now()
}
return {
state,
handleSearch
}
}
}
</script>
2.3 shallowRef & shallowReactive(浅响应)
shallowRef
只监听.value属性的值的变化,对象内部的某一个属性改变时并不会触发更新,只有当更改value为对象重新赋值时才会触发更新
const foo = shallowRef({
c: 1,
})
const change = () => {
foo.value.c = 2 // 视图不更新
foo.value={a:1} // 视图更新
}
shallowReactive(浅响应)
只监听对象的第一层属性,对嵌套的对象不做响应式处理
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
const change = () => {
state.foo = 2 // 视图更新
state.nested={count:2}// 视图更新
state.nested.bar =3 // 视图不更新
}
3. hooks(组合式函数)
介绍
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间而抽取一个可复用的函数。这个格式化函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。
本质是一个函数,将setup函数中的composition API进行了封装
类似vue2的mixin
复用代码,是setup中的逻辑更清楚易懂
3.1 正常写一个功能
需求:鼠标经过打印出当前经过点的坐标
<template>
Mouse position is at: {{ x }}, {{ y }}
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
3.2 使用hooks
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
<template>Mouse position is at: {{ x }}, {{ y }}</template>
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
3.3 嵌套组合式函数 (升级版)
还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是我们决定将实现了这一设计模式的 API 集合命名为组合式 API 的原因。
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
useEventListener(window, 'mousemove', update)
return { x, y }
}
3.4 可以引入多个hook文件并且还可以传参 (终极版)
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
综上所诉,核心逻辑一点都没有被改变,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式API函数。现在,在任何组件中都可以使用 useMouse() 功能了。
相比于 Mixin 区别
- mixin向外暴露出的是一个对象,hooks则是一个可传参数的方法
- Mixin 命名容易发生冲突:因为每个 mixin 的变量和方法都被合并到同一个组件中,所以为了避免变量名和方法名冲突,仍然需要了解其他每个特性;Mixin同名变量会被覆盖,Vue3自定义Hook可以在引入的时候对同名变量重命名
- 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
- Mixin不明的混淆,我们根本无法获知属性来自于哪个Mixin文件,给后期维护带来困难
export default {
mixins: [ a, b, c, d, e, f, g ], //一个组件内可以混入各种功能的Mixin
mounted() {
console.log(this.name) //问题来了,这个name是来自于哪个mixin?
}
}
4. setup
4.1 理解
一个组件选项,在组件被创建之前,props 被解析之后执行。它是composition API(组合式 API) 的入口
setup的执行周期在beforeCreate之前,因此this为undefined
-
setup 是一个新的配置项,值是一个函数
-
所有的composition Api都放在setup里面
-
必须要有返回值,值是一个对象,在模板中直接使用
4.1 setup的用法
export default {
props: {
title: String
},
setup(props,context) {
console.log(props.title)
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数) ****** 查询下二是否有
console.log(context.expose)
}
}
4.2 单文件组件
//子组件 代码
{{info}}
- **添加响应性**
为了给 provide/inject 添加响应性,使用 ref 或 reactive 。
完整实例2:provide/inject 响应式
```vue
//父组件代码
<template>
<div>
info:{{info}}
<InjectCom ></InjectCom>
</div>
</template>
<script>
import InjectCom from "./InjectCom"
import { provide,readonly,ref } from "vue"
export default {
setup(){
let info = ref("今天你学习了吗?")
setTimeout(()=>{
info.value = "不找借口,立马学习"
},2000)
provide('info',info)
return{
info
}
},
components:{
InjectCom
}
}
</script>
// InjectCom 子组件代码
<template>
{{info}}
</template>
<script>
import { inject } from "vue"
export default {
setup(){
const info = inject('info')
setTimeout(()=>{
info.value = "更新"
},2000)
return{
info
}
}
}
</script>
上述示例,在父组件或子组件都会修改 info 的值。
provide / inject 类似于消息的订阅和发布,遵循 vue 当中的单项数据流,什么意思呢?
就是数据在哪,修改只能在哪,不能在数据传递处修改数据,容易造成状态不可预测。
在订阅组件内修改值的时候,可以被正常修改,如果其他组件也使用该值的时候,状态容易造成混乱,所以需要在源头上规避问题。
readonly 只读函数,使用之前需要引入,如果给变量加上 readonly 属性,则该数据只能读取,无法改变,被修改时会发出警告,但不会改变值。
使用方法:
import { readonly } from "vue"
let info = readonly('只读info值')
setTimout(()=>{
info="更新info" //两秒后更新info的值
},2000)
运行两秒后,浏览器发出警告,提示 info 值不可修改。
所以我们就给provide发射出去的数据,添加一个只读属性,避免发射出去的数据被修改。
完整实例2的 provide 处添加 readonly 。
provide('info', readonly(info))
在子组件修改值的时候,会有一个只读提醒。
修改值的时候,还是需要在 provide 发布数据的组件内修改数据,所以会在组件内添加修改方法,同时也发布出去,在子组件处调用就可以了。如:
完整示例3:修改数据
6. 新增的一些组件
1. Fragment
- 在Vue2 中:组件必须有一个根标签
- 在Vue3 中:组件可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中
- 好处:减少标签层架,减少内存占用,并且该标签不会出现在dom树中。
2.Teleport
Teleport是一种能将我们的组件html结构移动到指定位置的技术。
好处:当我们使用组件时,不用担心展开模态框会破坏页面结构
父组件App.vue:
<template>
<div class="app">
<h1>这是祖先组件</h1>
<ChildComponent></ChildComponent>
</div>
</template>
<script>
import ChildComponent from "@/components/ChildComponent";
export default {
name: 'App',
components: {ChildComponent}
}
</script>
<style>
.app {
background-color: gray;
padding: 10px;
}
</style>
子组件ChildComponent.vue:
<template>
<div class="child">
<h1>这是孩子组件</h1>
<GrandsonAssembly></GrandsonAssembly>
</div>
</template>
<script>
import GrandsonAssembly from "@/components/GrandsonAssembly";
export default {
name: "ChildComponent",
components: {GrandsonAssembly}
}
</script>
<style scoped>
.child {
background-color: skyblue;
padding: 10px;
}
</style>
孙组件GrandsonAssembly.vue:(用到了A模块框组件)
<template>
<div class="son">
<h1>这是孙子组件</h1>
<ModalBox></ModalBox>
</div>
</template>
<script>
import ModalBox from "@/components/ModalBox";
export default {
name: "GrandsonAssembly",
components: {ModalBox}
}
</script>
<style scoped>
.son {
background-color: orange;
padding: 10px;
}
</style>
A模块框组件ModalBox.vue:
<template>
<div>
<button type="button" @click="isShow = true">弹出模态框</button>
<teleport to="body"><!-- 将元素传送到body标签的最后面 -->
<section v-if="isShow" class="mask"><!-- 语义化标签,表示一个独立的区块,这里用来做遮罩效果 -->
<div class="modalBox"><!-- 模态框 -->
<h3>这是一些内容</h3>
<h3>这是一些内容</h3>
<h3>这是一些内容</h3>
<h3>这是一些内容</h3>
<h3>这是一些内容</h3>
<button type="button" @click="isShow = false">关闭模态框</button>
</div>
</section>
</teleport>
</div>
</template>
<script>
import {ref} from "vue";
export default {
name: "ModalBox",
setup() {
let isShow = ref(false)
return {isShow}
}
}
</script>
<style scoped>
.mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.modalBox {
width: 300px;
height: 300px;
background-color: lightgreen;
text-align: center;
/* 相对于父元素来达到水平和垂直的居中 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
3.Suspense(实验阶段)
-
类似于 keep-alive 的形式不需要任何的引入,可以直接进行使用。
-
自带两个 slot 分别为 default、fallback。顾名思义,当要加载的组件不满足状态时,Suspense 将回退到 fallback状态一直到加载的组件满足条件,才会进行渲染。
-
等待异步组件时渲染一些额外内容,有更好的用户体验
-
使用步骤
-
- 异步引入组件:
import { defineAsyncComponent } from "vue";
const Child = defineAsyncComponent(()=>import('./compoments/Child.vue'))
-
- 使用Suspense包裹组件,并配置好 default 与 fallback
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中。。。</h3>
</template>
</Suspense>
</div>
</template>
7、tips
1、关于过滤器
在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。
2、状态驱动的动态 CSS
<template>
<div>
<h1 class="bg-red">11111</h1>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup () {
const color = ref('red')
return {
color
}
}
}
</script>
<style>
.bg-red {
color: v-bind(color);
}
</style>
3. VueUse
官网文档:https://vueuse.org/
是为Vue 2和3服务的一套Vue Composition API的常用工具集
通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装
//isFullscreen 当前是否是全屏
//toggle 是函数直接调用即可
const { isFullscreen, toggle } = useFullscreen();
//text 粘贴的内容
//copy 是粘贴函数
const { text, copy, isSupported }
= useClipboard({ copiedDuring: 1500 });
const title = useTitle()
console.log(title.value) // print current title
title.value = 'Hello' // change current title
VueUse常用方法总结
VueUse将所有方法按照功能性进行了分类,包含:Animation、Browser、Component、Formatters、Misc、Sensors、State、Utilities、Watch,详见vueuse.functions。其中较为常用的有:
useClipboard 复制到剪贴板
useFetch fetch 请求
useFullscreen 全屏
useLocalStorage localStorage 存储
useDebounceFn 防抖/节流
useThrottleFn