前言
上一篇文字里面主要写了Vue2和Vue3的响应式原理及区别
这一篇主要对vue3新特性进行进一步了解与记录,同时也希望给其他小伙伴提供学习借鉴。
☆废话不多说,直接上正题
一、vue3安装与项目创建
参考地址:www.vue3js.cn/docs/zh/gui…
准备工作:脚手架全局安装
对于 Vue 3,你应该使用 npm
上可用的 Vue CLI v4.5 作为 @vue/cli@next
。要升级,你应该需要全局重新安装最新版本的 @vue/cli
:
yarn global add @vue/cli@next
# or
npm install -g @vue/cli@next
然后在 Vue 项目运行:
vue upgrade --next
方式1:以原有vueCli脚手架创建
创建项目:
vue create demo
直接选择 Vue 3 Preview
,并回车,不考虑eslint问题。
然后执行:
# 进入目录
cd demo
# 用vscode打开当前目录
code .
# 运行项目
npm run serve
or
yarn serve
方式2:vite创建
npm init vite-app demo
# 相当于
npx create-vite-app demo
# 安装后执行
npm install
# or
yarn
# 运行项目
npm run dev
# or
yarn dev
创建项目并运行,会发现我们 npm run dev or yarn dev
后是秒开项目的,运行速度极快。
二、Composition API
相当于 React Hooks
我们先使用以前vue2的方式实现一个累加:
<template>
<h2>{{count}}</h2>
<button @click="btnClick">累加</button>
</template>
<script>
export default {
data(){
return {
count: 0
}
},
methods: {
btnClick(){
this.count++;
}
}
}
</script>
这套代码可以实现一个累加的效果,但如果以后我们想把这个组件中的 count
字段与 btnClick
单独拎出来管理,那就比较麻烦了,因为 count
和 btnClick
不在同一个方法内,很难抽离。
1、setup
setup有以下特性:
1、
setup函数
是处于 生命周期函数beforeCreate
和Created
两个钩子函数之间的函数,在BeforeMount
和Mounted
之前
我们先下面验证一把
<template>
<div></div>
</template>
<script>
import { reactive, toRefs, onBeforeMount, onMounted } from "vue";
export default {
name: "",
beforeCreate() {
console.log("beforeCreate执行了");
},
created() {
console.log("created执行了");
},
setup() {
console.log("1-开始创建组件-setup");
const data = reactive({});
onBeforeMount(() => {
console.log("2.组件挂载页面之前执行----onBeforeMount");
});
onMounted(() => {
console.log("3.-组件挂载到页面之后执行-------onMounted");
});
return {
...toRefs(data),
};
},
};
</script>
看下面控制图输出的顺序就明白了
也就说在 setup函数中是无法 使用 data 和 methods 中的数据和方法的
2、setup函数是 Composition API(组合API)的入口,相比vue2 Options API,两者区别,如图:
数据和业务逻辑分散在同一个文件的 N 个地方,随着业务复杂度的上升,可能会出现动图左侧的代码组织方式,不利于管理和维护。
为了解决这个问题,Composition API由此产生。可以把同一功能的数据和业务逻辑组织到一起,方便复用和维护。
3、在setup函数中定义的变量和方法最后都是需要 return 出去的 不然无法再模板中使用
4、由于我们不能在 setup函数中使用 data 和 methods,所以Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined
<template>
<div></div>
</template>
<script>
import {
reactive,
toRefs,
onMounted,
} from "vue";
export default {
data() {
return {
b: "data数据",
};
},
setup() {
console.log("this是否有值呢?", this);
return {
};
},
};
</script>
5、setup函数只能是同步的不能是异步的,但是一定要想变成异步,同步要转异步要特殊处理
在Vue3中,如果当前组件的setup使用了async/await
,那么其调用组件的父组件的外层需要嵌套一个suspense标签
,例如:
// 子组件
<template>
<div>···</div>
</template>
export default {
async setup () {
let ···
// 接口A
await getA().then(() => {
···
}).catch(() => {
···
})
// 接口B
await getB().then(() => {
···
}).catch(() => {
···
})
return {
···
}
}
}
// 父组件
<suspense>
<async-component/>
</suspense>
2、ref
ref 函数,可以把简单数据类型包裹为响应式数据(复杂类型也可以),注意 JS 中操作值的时候,需要加 .value
属性,模板中正常使用即可。
-
作用: 定义一个响应式的数据
-
语法:
const xxx = ref(initValue)
- 创建一个包含响应式数据的引用对象(reference对象,简称ref对象) 。
- JS中操作数据:
xxx.value
- 模板中读取数据: 不需要.value,直接:
<div>{{xxx}}</div>
-
备注:
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠
Object.defineProperty()
的get
与set
完成的。 - 对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数——
reactive
函数。
我们先来体验一下vue3怎么处理这个累加效果:
<template>
<h2>{{count}}</h2>
<button @click="btnClick">累加</button>
</template>
<script>
import {ref} from 'vue'
export default {
data(){
return {
count: 0
}
},
setup(){
const count = ref(1); // 此时我们使用ref指定count的默认值为1,因此上面data中的count会失效
let btnClick = () => {
count.value++; // 修改ref中的值要用xxx.value
}
return {count, btnClick}
}
}
此时如果我想单独管理这个累加效果,我就可以这么操作:
<template>
<h2>{{count}}</h2>
<button @click="btnClick">累加</button>
</template>
<script>
import {ref} from 'vue'
export default {
data(){
return {
// count: 0 // 一旦把setup中的代码抽离,return中对应的值要去掉,否则ref无效
}
},
setup(){
// 函数调用后就会返回一个对象,因此我们直接return
return clickCountFn()
// 如果后期还想同时返回其他数据,可以将clickCountFn()的返回结果展开
// return {...clickCountFn(), 其他数据}
}
}
// 封装一个函数,这样这块功能我们就能单独管理了
function clickCountFn(){
const count = ref(1);
let btnClick = () => {
count.value++;
}
return {count, btnClick}
}
</script>
3、reactive
再来了解另一个API :
reactive函数和ref作用非常接近,但是它的参数是一个对象,我们可以在对象中定义其方法,而通过这个形式,就不需要再对其进行进行
.value
调用了。
- 作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用
ref
函数) - 语法:
const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象) - reactive定义的响应式数据是“深层次的”。
- 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
<template>
<h2>{{count}}</h2>
<button @click="btnClick">累加</button>
<p>姓名:{{obj.username}}</p>
<button @click="btnClick1">修改姓名</button>
</template>
<script>
import {ref, reactive} from 'vue'
export default {
setup(){
// 使用reactive
let obj = reactive({
username: "Jack"
})
let btnClick1 = () => {
obj.username = "Mary"
}
return {...clickCountFn(), obj, btnClick1}
}
}
function clickCountFn(){
const count = ref(1);
let btnClick = () => {
count.value++;
}
return {count, btnClick}
}
</script>
使用 reactive
生成的对象与 ref
生成的值都是响应式的。
这里可以看到我们在 html 中调用数据时,使用的是 obj.username
,那我们是否可以直接写 username
呢?答案是可以的,但这里需要注意:
由于reactive返回的对象本质上已经是一个Proxy对象,所以通过…扩展符号展开的属性,是无法进行响应式的
也就是说,如果这么写:
return {...clickCountFn(), ...obj, btnClick1}
那么是无法实现的。
4、toRef和toRefs
- 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
- 语法:
const name = toRef(person,'name')
- 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
- 扩展:
toRefs
与toRef
功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)
eg:
<template>
<h2>{{count}}</h2>
<button @click="btnClick">累加</button>
<!-- 无需obj.username,直接username即可 -->
<p>姓名:{{username}}</p>
<button @click="btnClick1">修改姓名</button>
</template>
<script>
// 新增toRefs方法
import {ref, reactive, toRefs} from 'vue'
export default {
setup(){
let person = reactive({
username: "Jack"
})
let btnClick1 = () => {
person.username = "Mary"
}
const name2 = toRef(person, "username");
console.log("####", name2);
// 通过toRefs方法
let refObj = toRefs(person);
// 通过...refObj将数据扩展
return {...clickCountFn(), ...refObj, btnClick1,name: toRef(person, "username"),}
}
}
//#region
// 其他代码...
//#endregion
</script>
5、Vue3.0中的响应式原理
1、vue2.x的响应式
-
实现原理:
-
对象类型:通过
Object.defineProperty()
对属性的读取、修改进行拦截(数据劫持)。 -
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
Object.defineProperty(data, 'count', { get () {}, set () {} })
-
-
存在问题:
- 新增属性、删除属性, 界面不会更新。
- 直接通过下标修改数组, 界面不会自动更新。
2、Vue3.0的响应式
-
实现原理:
-
通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
-
通过Reflect(反射): 对源对象的属性进行操作。
-
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
6、computed计算属性与watch监视
1、computed函数
-
用法
import {computed} from 'vue' setup(){ ... //计算属性——简写 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] } }) }
2、watch函数
-
六大使用场景
- 情况一:监视ref定义的响应式数据
watch(sum,(newValue,oldValue)=>{ console.log('sum变化了',newValue,oldValue) },{immediate:true})
- 情况二:监视ref所定义的多个响应式数据
watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变化了',newValue,oldValue) })
- 情况三:监视reactive所定义的一个响应式数据的全部属性
watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) },{immediate:true,deep:false})
- 情况四:监视reactive所定义的一个响应式数据中的某个属性
watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true})
- 情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true})
- 情况六:监视reactive定义的对象中某个属性,deep配置有效
watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true})
- 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效。
所有代码
<template>
<h2>当前求和为:{{ sum }}</h2>
<button @click="sum++">点我+1</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 += '~'">修改姓名</button>
<button @click="person.age++">增长年龄</button>
<button @click="person.job.j1.salary++">涨薪</button>
</template>
<script>
import { reactive, toRefs, watchEffect, onMounted, ref, watch } from "vue";
export default {
name: "demo",
setup() {
let sum = ref(0);
let msg = ref("你好啊");
let person = reactive({
name: "张三",
age: 18,
job: {
j1: {
salary: 20,
},
},
});
//情况一:监视ref定义的响应式数据
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
},{immediate:true})
//情况二:监视多个ref定义的响应式数据
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变化了',newValue,oldValue)
})
/* 情况三:监视reactive定义的响应式数据
1.注意:此处无法正确的获取oldValue
2.注意:强制开启了深度监视(deep配置无效)
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不再奏效
//情况四:监视reactive定义的响应式数据中的某个属性
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
//情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
//情况六:监视reactive定义的对象中某个属性,deep配置有效
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true})
3、watchEffect函数
-
watch的套路是:既要指明监视的属性,也要指明监视的回调。
-
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
-
watchEffect有点像computed:
- 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 watchEffect(()=>{ const x1 = sum.value const x2 = person.age console.log('watchEffect配置的回调执行了') })
7、生命周期
对比生命周期升级到vue3,跟vue2区别,确实本质上没什么区别的
vue2.x的生命周期
vue3.0的生命周期
-
Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
beforeDestroy
改名为beforeUnmount
destroyed
改名为unmounted
-
Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
beforeCreate
————>setup()
created
————>setup()
beforeMount
————>onBeforeMount
mounted
————>onMounted
beforeUpdate
————>onBeforeUpdate
updated
————>onUpdated
beforeUnmount
————>onBeforeUnmount
unmounted
————>onUnmounted
8、自定义hook函数
- 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
- 类似于vue2.x中的mixin。
- 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
三、Provide与Inject跨组件通信
1、Vue2写法
以往我们的父传子是通过props传的:
<!-- Father.vue父组件 -->
<template>
<Child :num="num" />
</template>
<script>
import Child from './Child.vue'
export default {
data(){
return {
num: 123
}
},
components: {
Child
}
}
</script>
<!-- Child.vue子组件 -->
<template>
<h2>父组件传过来的值:{{num}}</h2>
</template>
<script>
export default {
props: ['num']
}
</script>
这个时候限制死了数据必须来自父组件,我们其实还有 Provide
和 Inject
:
<!-- Father.vue父组件 -->
<template>
<Child />
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
},
provide: {
num: 456
}
}
</script>
<!-- Child.vue子组件 -->
<template>
<h2>父组件传过来的值:{{num}}</h2>
</template>
<script>
export default {
inject: ['num']
}
</script>
Provide/Inject
相比于 props
的好处在于:
如果组件嵌套较多,那么 props
需要一级一级往下传递,后期很难维护。Provide+Inject
相当于是跨级组件传值,比如孙子组件也想用上面这个 num
的值,就不用一级一级往下传,直接在孙子组件使用即可:
<!-- Sun.vue孙子组件 -->
<template>
<h4>孙子组件:{{num}}</h4>
</template>
<script>
export default {
// 将Sun组件在Child组件中引入,即可实现跨级组件传值
inject: ['num']
}
</script>
2、Vue3写法
vue3中的 provide/inject
。两者都只能在当前活动实例的 setup()
期间调用。
格式为:
// provide
import {provide} from 'vue' // 显式导入
export default {
setup() {
// 此处name必须是String类型,value则不限制
provide(name, value)
}
}
// inject
import {inject} from 'vue' // 显式导入
export default {
setup(){
// name即为传过来的字段,第二个参数可选,可填写默认值
const val = inject(name, defaultValue);
return {val}
}
}
我们修改以上案例的代码:
<!-- Father.vue父组件 -->
<template>
<Child />
</template>
<script>
import {provide} from 'vue' // 显式导入
import Child from './Child.vue'
export default {
components: {
Child
},
setup(){
provide('num', 789)
}
}
</script>
<!-- Sun.vue孙子组件 -->
<template>
<h4>孙子组件:{{mynum}}</h4>
</template>
<script>
import {inject} from 'vue' // 显式导入
export default {
setup(){
const mynum = inject('num');
return {mynum}
}
}
</script>
3、响应性
所谓的 Provide/Inject
响应性,其实就是把传递的值结合上文提及的 ref
或 reactive
一起使用:
<!-- Father.vue父组件 -->
<template>
<Child />
<button @click="changeNumFn">修改num</button>
</template>
<script>
import {provide, ref} from 'vue' // 显式导入
import Child from './Child.vue'
export default {
components: {
Child
},
setup(){
// 使用ref来定义num的值
const num = ref(123);
// 声明一个函数,专门用于修改num
let changeNumFn = () => {
num.value = 456;
}
provide('num', num)
// 返回这个函数
return {changeNumFn}
}
}
</script>
此时,当你点击按钮时,孙子组件接收到的 num
就会被修改了。在父子通信一般都用props,虽然Provide/Inject
子组件也可以取到值,但不推荐。
四、Teleport(传送门)
在vue2中,想要将子节点渲染到存在于父组件以外的 DOM
节点时,需要通过第三方库 portal-vue 去实现。而vue3中,Teleport
是一种能够将我们的模板移动到 DOM
中 Vue app
之外的其他位置的技术。
举个最简单的例子:
我们在 index.html
中 #app
同级的地方新增一个 #test
元素:
<div id="app"></div>
<div id="test"></div>
由于vue的 main.js
中规定了打包出来的代码都放入 #app
中:
createApp(App).mount('#app')
因此,你现在没有办法将代码放入 #test
中。此时,我们可以使用传送门:
App.vue
中:
<template>
<Home />
</template>
<script>
import Home from './components/Home.vue'
export default {
name: 'App',
components: {
Home
}
}
</script>
Home.vue
中:
<template>
<p>这段话是渲染在#app中的</p>
<teleport to="#test">
<p>这段话是渲染在#test中的--1</p>
</teleport>
<teleport to="#test">
<p>这段话是渲染在#test中的--2</p>
</teleport>
</template>
此时,你打开浏览器控制台,就可以看到第2、3个p标签已经被渲染到 #test
中。
备注:
- 1、标签身上都to属性,填写的是css选择器。
- 2、多个传送门书写时,会按照自上而下的顺序传送至另一个DOM元素。
五、Suspense(等待)
Suspense组件用于等待异步组件时渲染一些额外内容,让应用有更好的用户体验。
那我们什么时候需要使用异步组件呢?多了去了,比如:
- 在页面加载之前显示加载动画
- 显示占位符内容
- 处理延迟加载的图像
那么,让我们看看 Suspense
怎么使用,我们先来提一个需求:
在等待组件获取数据并解析时显示“玩命加载中…”之类的内容
OK,我们来写一个 Child.vue
组件:
使用步骤:
// 异步引入组件
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/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>
六、Fragment(碎片)
- 在Vue2中: 组件必须有一个根标签:
<template>
<div>你好欢迎我的世界</div>
</template>
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
<template>
<div>你好</div>
<div>欢迎</div>
<div>我的世界</div>
</template>
原因是代表任何Vue组件的Vue实例需要绑定到一个单一的DOM元素中。唯一可以创建一个具有多个DOM节点的组件的方法就是创建一个没有底层Vue实例的功能组件。
这情况同样存在于react,但react可以使用空标签 <></>
来包裹,或者是使用一个名为Fragment的虚拟元素:
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>你好</td>
<td>世界</td>
</React.Fragment>
<>
<td>你好</td>
<td>世界</td>
</>
)
}
}
尽管Fragment
看起来像一个普通的DOM元素,但它是虚拟的,根本不会在DOM树中呈现。目前你可以在Vue 2中使用vue-fragments库
来使用Fragments,而在Vue 3中,你直接使用就行了,无需引入任何库。
七、TreeShaking(摇树)
TreeShaking
是一个术语,指的是在打包构建过程中移除没有被引用到的代码,这些代码可以成为 dead code。这个概念最早在基于 ES6 的打包工具 Rollup
中提出,后来被引入到 webpack 中。TreeShaking 比较依赖于 ES6 模块系统的静态结构特性,比如 import
和 export
。
举个例子:
vue2中我们常使用 Vue.nextTick(()=>{})
来预操作DOM,但有时候我们不用这个 nextTick
,比如改用别的方式来代替(如setTimeout),那么项目打包时,vue 全局的 nextTick
就成为一个多余的代码,从而使你的项目打包体积变大。
在vue3中,官方团队重构了所有全局 API 的组织方式,让所有的 API 都支持了 TreeShaking。所以vue3中如果还想使用全局的 nextTick
,就需要引入:
import { nextTick } from 'vue';
nextTick(() => {
// 和 DOM 有关的一些操作
});
如果你在 Vue 3 中不引入而直接调用 Vue.nextTick()
,就会得到一个报错:undefined is not a function
。
官方也给出了Vue 2.x 中的受此更改影响的全局 API:
Vue.nextTick
Vue.observable
(用Vue.reactive
替换)Vue.version
Vue.compile
(仅全构建)Vue.set
(仅兼容构建)Vue.delete
(仅兼容构建)
八、Performance(性能)
vue3.0相对于vue2.0来说性能快1.2到1.5倍,主要原因如下:
1、diff方法优化
- Vue2 中的虚拟dom是进行全量的对比
- Vue3 新增了静态标记(PatchFlag),只比对带有 PF 的节点,并且通过 Flag 的信息得知 当前节点要比对的具体内容。
2、静态提升
- Vue2中无论元素是否参与更新, 每次都会重新创建, 然后再渲染
- Vue3中对于不参与更新的元素, 会做静态提升, 只会被创建一次, 在渲染时直接复用即可
3、cacheHandlers 事件侦听器缓存
- 默认情况下onClick会被视为动态绑定, 所以每次都会去追踪它的变化
- 但是因为是同一个函数,所以没有追踪变化, 直接缓存起来复用即可
4、SSR渲染
- 当有大量静态的内容时候,这些内容会被当做
纯字符串
推进一个buffer
里面, 即使存在动态的绑定,会通过模板插值嵌入进去。这样会比通过虚拟DOM
来渲染的快上很多很多。 - 当静态内容大到一定量级时候,会用
_createStaticVNode
方法在客户端去生成一个static node
, 这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。
九、Setup的生命周期
组合式API需要在setup中使用,setup中含有的生命钩子与vue的大体一致:
具体参考:《setup生命周期钩子》
十、TypeScript支持
vue3新增了对TS语法的支持。
//script标签上 **lang="ts"**
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
//定义一个类型type或者接口interface来约束data
type Todo = {
id: number,
name: string,
completed: boolean
}
export default defineComponent({
//使用reactive时,可以用toRefs解构导出,在template就可以直接使用了
const data = reactive({
todoList: [] as Todo[]
})
//可以使用ref或者toRefs来定义响应式数据
const count = ref(0);
//使用ref在setup读取的时候需要获取xxx.value,但在template中不需要
console.log(count.value)
return {
...toRefs(data)
}
})
</script>
十一、其它 Composition API
跟响应式数据有关的方法、跨组件通信、自定义ref
1、shallowReactive 与 shallowRef
-
shallowReactive
:只处理对象最外层属性的响应式(浅响应式)。 -
shallowRef
:只处理基本数据类型的响应式, 不进行对象的响应式处理。 -
啥时候使用合适?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。
// let person = shallowReactive({
let person = reactive({
name: "张三",
age: 18,
job: {
j1: {
salary: 20,
},
},
});
// 只考虑基本类型的响应式,不考虑对象类型的响应式
let x = shallowRef({
y: 0,
});
console.log("******", x);
//返回一个对象(常用)
return {
x,
person,
...toRefs(person),
};
2、readonly 与 shallowReadonly
readonly
: 让一个响应式数据变为只读的(深只读)。shallowReadonly
:让一个响应式数据变为只读的(浅只读)。- 应用场景: 不希望数据被修改时。
<template>
<h4>当前求和为:{{ sum }}</h4>
<button @click="sum++">点我++</button>
<hr />
<h4>{{ person }}</h4>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>薪资:{{ job.j1.salary }}K</h2>
</template>
<script>
import { reactive, toRefs, onMounted, ref } from "vue";
export default {
setup() {
//数据
let sum = ref(0);
let person = reactive({
name: "张三",
age: 18,
job: {
j1: {
salary: 20,
},
},
});
// person = readonly(person);
person = shallowReadonly(person);
// sum = readonly(sum);
// sum = shallowReadonly(sum);
//返回一个对象(常用)
return {
sum,
...toRefs(person),
};
},
};
</script>
3、toRaw 与 markRaw
-
toRaw
:- 作用:将一个由
reactive
生成的响应式对象转为普通对象。 - 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- 作用:将一个由
-
markRaw
:-
作用:标记一个对象,使其永远不会再成为响应式对象。
-
应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类库等。
- 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
-
<template>
<h4>当前求和为:{{ sum }}</h4>
<button @click="sum++">点我++</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 += '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">涨薪</button>
<button @click="showRawPerson">输出最原始的person</button>
<button @click="addCar">给人添加一台车</button>
<button @click="person.car.name += '!'">换车名</button>
<button @click="changePrice">换价格</button>
</template>
<script >
import { ref, reactive, toRefs, toRaw, markRaw } from "vue";
export default {
name: "Demo",
setup() {
//数据
let sum = ref(0);
let person = reactive({
name: "张三",
age: 18,
job: {
j1: {
salary: 20,
},
},
});
/* -
作用:将一个由```reactive```生成的----响应式对象--->转为---普通对象---。
- 使用场景:用于读取响应式对象对应的普通对象,
对这个普通对象的所有操作,不会引起页面更新。 */
const showRawPerson = () => {
const p = toRaw(person);
p.age++;
console.log(p);
};
const addCar = () => {
let car = { name: "奔驰", price: 40 };
person.car = markRaw(car);
};
/* markRaw:
- 作用:标记一个对象,使其永远不会再成为响应式对象。
- 应用场景:
1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。 */
const changePrice = () => {
person.car.price++;
console.log(person.car.price);
};
//返回一个对象(常用)
return {
sum,
person,
...toRefs(person),
showRawPerson,
addCar,
changePrice,
};
},
};
</script>
4、customRef
-
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
-
实现防抖效果:
<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
-
作用:实现祖与后代组件间通信
-
套路:父组件有一个
provide
选项来提供数据,后代组件有一个inject
选项来开始使用这些数据 -
具体写法:
-
祖组件中:
setup(){ ...... let car = reactive({name:'奔驰',price:'40万'}) provide('car',car) ...... }
-
后代组件中:
setup(props,context){ ...... const car = inject('car') return {car} ...... }
-
6、响应式数据的判断
isRef
: 检查一个值是否为一个ref 对象
isReactive
: 检查一个对象是否是由reactive
创建的响应式代理isReadonly
: 检查一个对象是否是由readonly
创建的只读代理isProxy
: 检查一个对象是否是由reactive
或者readonly
方法创建的代理
十二、其他变动
1、全局API的转移
-
Vue 2.x 有许多全局 API 和配置。
-
例如:注册全局组件、注册全局指令等。
//注册全局组件 Vue.component('MyButton', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>' }) //注册全局指令 Vue.directive('focus', { inserted: el => el.focus() }
-
-
Vue3.0中对这些API做出了调整:
-
将全局的API,即:
Vue.xxx
调整到应用实例(app
)上2.x 全局 API( Vue
)3.x 实例 API ( app
)Vue.config.xxxx app.config.xxxx Vue.config.productionTip 移除 Vue.component app.component Vue.directive app.directive Vue.mixin app.mixin Vue.use app.use Vue.prototype app.config.globalProperties
-
2、其他改变
-
data选项应始终被声明为一个函数。
-
过度类名的更改:
-
Vue2.x写法
.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }
-
Vue3.x写法
.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
-
-
移除keyCode作为 v-on 的修饰符,同时也不再支持
config.keyCodes
-
移除
v-on.native
修饰符-
父组件中绑定事件
<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" />
-
子组件中声明自定义事件
<script> export default { emits: ['close'] } </script>
-
-
移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
十三、Composition API 的优势
1、Options API 存在的问题
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
2、Composition API 的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
结语
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下阿绵哈哈。
宝贝们
,都看到这里了,要不点个赞呗 👍
写作不易,希望可以获得你的一个「赞」。如果文章对你有用,可以选择「关注 + 收藏」。 如有文章有错误或建议,欢迎评论指正,谢谢你。❤️
参考网址
Vue官网文档:v3.cn.vuejs.org/
Vue中文文档:www.vue3js.cn/docs/zh/gui…
Vite中文网:vitejs.cn/
在线编辑器:codesandbox.io/