vue3新特性和新用法
下面是关于vue3的一些新特性和新用法的讲解一、新增内置组件:teleport官文解释teleport:Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件场景:在组件A中使用alert弹窗,但不希望alert弹窗渲染的DOM结构嵌套在组件A中;而是渲染在我们指定的位置上实现方式:1)使用<teleport t
下面是关于vue3的一些新特性和新用法的讲解
一、新增内置组件:teleport
官文解释teleport:teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件
场景:在组件A中使用alert弹窗,但不希望alert弹窗渲染的DOM结构嵌套在组件A中;而是渲染在我们指定的位置上
实现方式:
1)使用<teleport to="#alert">包裹自定义的alert组件,to属性:是有效的查询选择器或 HTMLElement
2) 指定alert渲染的DOM结构位置,写上<div id="alert"></div>
注意:to属性和id的值保持一致
下面举个例子:
a)首先:定义Alert.vue
<template>
<teleport to="#alert">
<div class="alert">
<div class="alert-mask" @click="closeAlert"></div>
<div class="alert-content">
<div class="alert_header" v-if="title">
<slot name="header">
<h3>{{title}}</h3>
</slot>
</div>
<div class="alert_content">
<slot></slot>
</div>
<div class="alert_footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</teleport>
</template>
<script>
export default {
name: 'alert',
props: {
title: {
type: String,
default: '测试alert组件'
}
},
emits: ['onClose'],
methods: {
closeAlert() {
this.$emit('onClose')
}
}
}
</script>
b)其次:在组件Page.vue中使用Alert弹窗
<template>
<div class="page">
<div class="page-btn">
<div @click="showAlert" class="btn-item">显示Alert</div>
</div>
<Alert v-if="isShowAlert" @on-close="closeAlert">
<div>
只是测试,只是测试,只是测试,只是测试,只是测试,只是测试,只是测试,只是测试,
</div>
</Alert>
</div>
</template>
c)最后:指定Alert最终渲染的位置。这里指定与App.vue里面的元素同级。App.vue内容如下:
<template>
<div class="app">
<p class="app-title">App.vue页面</p>
<div id="alert"></div>
<Page />
</div>
</template>
下面看下渲染效果:
未显示Alert组件前的DOM效果:
显示Alert组件后的DOM效果:
结论:由图1和图2可以看出,使用teleport组件,通过to属性,可以控制Alert组件渲染的位置,而Alert的显示是由内部组件Page控制的
二、新增选项:emits
场景:在Vue2.x中,子组件向父组件传递数据,一般通过$emit方式传递
举个例子:在上面的Alert.vue中,关闭Alert弹窗的时候,会调用closeAlert方法;同时在emits选项中,会添加‘on-close’
emits: ['onClose'],
methods: {
closeAlert() {
this.$emit('onClose')
}
}
如果将emits选项去掉,看下浏览器显示效果:
控制台会显示警告,大致意思是:组件内自定义事件,需要通过option:emits声明
emits作用:
1)用于声明子组件向父组件抛出的事件,便于其他开发人员知道该组件发出那些事件
2)可以对发出的事件进行验证
emits类型:Array<string> | Object
1)emits的值是简单的数组,如在Alert.vue中的定义
emits: ['onClose']
2)emits的值是Object,每个 property 的值可以为 null
或验证函数
父组件调用:
<Child @on-submit="onSubmit"/>
定义Child子组件,点击“提交”按钮,会触发submit()函数,同时会触发emits里面onSubmit的校验函数
<template>
<div class="child">
<div class="content">
<p>姓名:</p>
<input type="text" v-model="value" class="content-value">
</div>
<div class="btn" @click="submit">提交</div>
</div>
</template>
<script>
export default {
name: 'child',
data() {
return {
value: ''
}
},
emits: {
onSubmit: payload => {
// payload为$emit触发的onSubmit事件的参数
if (payload) {
return true
} else {
return false
}
}
},
methods: {
submit() {
this.$emit('onSubmit', this.value)
}
}
}
</script>
三、新增和删除的全局API
1)vue2.x、vue3.x中的全局API对比,即vue3中全局API只保留了nextTick,其他的都是新增API;被移除的部分全局API,放到了应用实例上了
vue2.x的全局APi: vue3.x的全局API:
Vue.nextTick nextTick
Vue.extend defineComponent
defineAsyncComponent
resolveComponent
resolveDynamicComponent
Vue.directive resolveDirective
withDirective
Vue.set createApp
Vue.delete h
Vue.filter
Vue.component
Vue.use createRender
Vue.mixin
Vue.compile
Vue.observable
Vue.version
2)全局API的引进方式有差异,vue2通过默认导出一个对象,通过引用对象方法使用全局API;vue3通过具名导出方法名,使用全局API直接引进该方法名就可以使用了
vue2中使用全局API vue3中使用全局API
import Vue from 'vue' import { createApp, h } from 'vue'
Vue.component() createApp()
Vue.extend() h()
vue3中这种具名引用的方式,实现了更好的Tree-Shaking;
而vue2.x中,全局API是从Vue对象直接暴露出来的,只要我们引入了Vue对象,那么不管该对象上的全局API是否有用到,最终都会出现在我们线上的代码中
下面讲解下vue3中新增的全局API用法
1、createApp:返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文;可以在 createApp
之后链式调用其它应用API
下面是vue2和vue3创建应用实例的比较:
1)vue3主要是通过createApp()创建应用实例,通过应用实例方法mount将应用实例的根组件挂载在提供的 DOM 元素上
2)vue2主要是通过newVue(options)创建应用实例,并在options里面设置相应的属性
// vue3
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
// vue2
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
2、h(type, props,children):返回一个”虚拟节点“,用于手动编写的渲染函数,生成组件
参数解析:
type:String | Object | Function(html标签名,组件,异步组件) 组件类型 必需
props:Object(和在模板中使用的 attribute、prop 和事件相对应) 组件属性 可选
children:String | Object |Array(子代 VNode) 组件子元素 可选
下面看一个例子:
let p = h('div', {}, [
'Some text comes first.',
h('h1', 'A headline'),
h(Page2, {
someProp: 'foobar'
})
])
console.log(p)
在控制台打印出来的p结果如下:
3、defineComponent:
官文解析:从实现上看,defineComponent
只返回传递给它的对象。但是,就类型而言,返回的值有一个合成类型的构造函数,用于手动渲染函数、TSX 和 IDE 工具支持
注意:在vue2中可以使用Vue.extend()生成组件,但在vue3中将该API移除了,取而代之的是defineComponent
作用:vue3 如果用ts,导出时候要用 defineComponent,这俩是配对的,为了类型的审查正确
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Page3',
props: {
msg: String
},
setup(props, ctx) {
console.log(props, ctx)
}
})
</script>
4、defineAsyncComponent:创建异步组件,只在有需要的时候才会加载
1)在vue2中只在有需要的时候才加载异步组件的实现方式
<template>
<div class="page3">
<h2 v-if="isShowPage">page3</h2>
<AsyncPage2 v-else-if="isShowPage2"/>
</div>
</template>
<script>
export default {
name: 'Page3',
components: {
AsyncPage2: () => import('./Page2') // 定义异步组件
},
data() {
return {
isShowPage2: false,
isShowPage: false
}
},
mounted() {
// 控制是否加载异步组件
this.isShowPage2 = true
}
}
</script>
2)在Vue3中通过defineAsyncComponent加载异步组件实现方式
<template>
<div class="page3">
<h2 v-if="isShowPage">page3</h2>
<AsyncPage2 v-else-if="isShowPage2"/>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
name: 'Page3',
components: {
AsyncPage2: defineAsyncComponent(() => import('./Page2'))
},
data() {
return {
isShowPage2: false,
isShowPage: false
}
},
mounted() {
this.isShowPage = true
}
}
</script>
<style>
</style>
通过1)、2)对比,可以看出仅仅是在定义异步组件的方式有点差异
但是defineAsyncComponent还有第二种用法,defineAsyncComponent({}),参数可以是以一个对象,官文解释如下:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
// 工厂函数
loader: () => import('./Foo.vue')
// 加载异步组件时要使用的组件
loadingComponent: LoadingComponent,
// 加载失败时要使用的组件
errorComponent: ErrorComponent,
// 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
delay: 200,
// 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
// 默认值:Infinity(即永不超时,单位 ms)
timeout: 3000,
// 定义组件是否可挂起 | 默认值:true
suspensible: false,
/**
*
* @param {*} error 错误信息对象
* @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
* @param {*} fail 一个函数,指示加载程序结束退出
* @param {*} attempts 允许的最大重试次数
*/
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
// 请求发生错误时重试,最多可尝试 3 次
retry()
} else {
// 注意,retry/fail 就像 promise 的 resolve/reject 一样:
// 必须调用其中一个才能继续错误处理。
fail()
}
}
})
四、应用API
在vue3中,任何全局改变 Vue 行为的 API 都会移动到应用实例上,所有其他不全局改变行为的全局 API 都改成具名导出了
vue2.x全局API | vue3应用实例API |
Vue.component | app.component |
Vue.config | app.config |
Vue.config.productionTip | remove |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
app.mount | |
app.provide | |
app.unmount |
下面说下新增应用API和使用有改变的API
1、app.directive:自定义指令
1)vue3中directive定义(vue2同vue3)
参数:
{string} name
{Function | Object} [definition]
返回值:
如果传入 definition 参数,返回应用实例
如果不传入 definition 参数,返回指令定义
2)用法 :通过对比可以看出,调用的钩子有区别
2、app.mount:将应用实例的根组件挂载在提供的 DOM 元素上,并返回根组件实例
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
3、app.unmount:在提供的 DOM 元素上卸载应用实例的根组件
// 挂载5秒后,应用将被卸载
setTimeout(() => app.unmount('#my-app'), 5000)
五、应用配置
vue3中的应用配置:可以在应用挂载前修改应用配置 property
import { createApp } from 'vue'
const app = createApp({})
app.config = {...}
1)vue2中的全局配置、vue3中的应用配置对比
vue3应用配置 vue2全局配置
errorHandler errorHandler
warnHandler warnHandler
optionMergeStrategies optionMergeStrategies
performance performance
// 新增配置 // vue3中删除的配置
globalProperties silent
isCustomElement devTools
ignoredElements
keyCodes
productionTip
2)配置使用方式差异:
vue2,直接在Vue.config对象上定义全局配置
vue3,直接在应用实例app.config上定义应用配置
说明:对于保留的配置项,除了挂靠对象不一样,其他都相同
// vue3应用配置
import { createApp } from 'vue'
const app = createApp({})
app.config.errorHandler = (err, vm, info) => {
// 处理错误
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
app.mount('#app')
// vue2全局配置
import Vue from 'vue'
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
下面看下新增的应用配置
1、globalProperties:添加全局 property,任何组件实例都可以访问该属性;属性名冲突时,组件的 property 将具有优先权
作用:可以代替vue2中Vue.prototype扩展
// Vue 2.x
Vue.prototype.$http = () => {}
// Vue 3.x
const app = createApp({})
app.config.globalProperties.$http = () => {}
举个例子:
// main.js中定义全局应用属性
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.pageTitle = 'Page2'
// 在Page2.vue中使用应用属性pageTitle
<template>
<div class="page2">
// 有pageTitle,则渲染
<p v-if="pageTitle">{{pageTitle}}</p>
<Child @on-submit="onSubmit"/>
</div>
</template>
2、isCustomElement:指定一个方法,用来识别在 Vue 之外定义的自定义元素(例如,使用 Web Components API)。如果组件符合此条件,则不需要本地或全局注册,并且 Vue 不会抛出关于 Unknown custom element
的警告
类型: (tag: string) => boolean
// 任何以“ion-”开头的元素都将被识别为自定义元素
app.config.isCustomElement = tag => tag.startsWith('ion-')
六、选项:生命周期
vue3和vue2的选项生命周期对比,从下图可以看出:vue3将vue2中的beforeDestroy、destroyed改成beforeUnmount、unmounted其实只是语义上的改变而已
vue3中新增的选项生命周期钩子还有:renderTracked renderTriggere,具体用法可以参考官文
七、移除的实例property
结论:由对比图可以看出,在vue3删除了vue2.x中的实例property有:$children $scopedSlots $isServer $listeners
下面说下移除这些实例的property原因
1、移除实例propety:$children
在vue2.x中通过$children,可以获取到当前组件的所有子组件的全部实例,所以通过$children可以访问子组件的data、methods等,常用于父子组件通讯
在vue3中可以通过$refs访问子组件实例
下面举个例子看看
1)vue3示例
<template>
<div class="app">
<p class="app-title">App.vue页面</p>
<div id="alert"></div>
<Page2 ref="Page2"/>
<Page ref="Page" class="app-page"/>
</div>
</template>
<script>
import Page2 from './components/Page2.vue'
import Page from './components/Page.vue'
export default {
name: 'App',
components: {
Page2,
Page
},
mounted() {
console.log('this.$children: ', this.$children)
console.log('this.$refs: ', this.$refs)
}
}
</script>
在控制台打印结果:
vue2,在控制台打印效果:
结论:通过上图可以看出,两者获取到的数据是一样的,只不过返回的数据结构不一样罢了,因此在vue3中将实例property:$children去掉,改用$refs获取
2、 移除实例property:$scopedSlots
vue3中将$slots和$scopedSlots统一成$slots,废除$scopedSlots
3、移除实例property:$listeners
先说下vue2.4以上版本中的$attrs、$listeners属性的用法
vm.$attrs
类型:{ [key: string]: string }
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定
(class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件
vm.$listeners
类型:{ [key: string]: Function | Array<Function> }
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
场景:如下图,当组件page1需要向page3传递数据,可以通过$attrs传递数据;page3向page1传递数据可以通过$listeners实现
代码实现:
1)组件page1.vue代码,在组件page2中定义两个prop:title、tips;两个事件:on-update、on-focus
<template>
<div class="hello">
<p class="title">page1</p>
<page2 :title="title" :tips="tips" @on-update="onUpdate" @on-focus="onFocus"/>
</div>
</template>
<script>
import page2 from './page2.vue'
export default {
name: 'HelloWorld',
components: {
page2
},
data () {
return {
title: 'page2',
tips: 'page3'
}
},
methods: {
onUpdate (val) {
this.tips = val
},
onFocus () {
console.log('onfocus')
}
}
}
</script>
2)组件page2代码,在props上定义了title属性,在组件page3中绑定$attrs(会将父作用域即page1中的tips传递给组件page3),$listeners(会将父作用域即page1中的事件监听器on-update,on-focus传给组件page3)
<template>
<div class="page2">
<p class="title">{{title}}</p>
<page3 v-bind="$attrs" v-on="$listeners"/>
</div>
</template>
<script>
import page3 from './page3.vue'
export default {
name: 'page2',
components: {
page3
},
props: {
title: String
}
}
</script>
3)page3代码,定义的props:tips;$emits事件有on-focus,on-update
<template>
<div class="page3">
<p class="tips">{{tips}}</p>
<input type="text" v-model="value" @focus="handleFocus" class="input">
<p @click="changeTitle" class="btn">改变标题</p>
</div>
</template>
<script>
export default {
name: 'page3',
props: {
tips: String
},
data () {
return {
value: ''
}
},
methods: {
handleFocus () {
this.$emit('on-focus')
},
changeTitle () {
this.$emit('on-update', this.value)
}
}
}
</script>
在vue3中,只需要在page2.vue中的page3不绑定$listeners,其他同vue2,得到的效果也是一样的
结论:vue3中将事件监听器认为是$attrs的一部分,因此将$listeners移除
4、移除实例property:$isServer
八、移除的实例方法
下图是vue3和vue2.x的实例方法对比图
从上图可以看出在vue3中移除了实例方法有:
// 数据
$set
$delete
// 事件
$on
$off
$once
// 生命周期
$mount
$destroy
下面说下废除这些实例方法的原因
1、废除$set、$delete
问题:在vue2中实现响应式数据是通过Object.defineProperty实现的,如果在页面挂载完成通过索引修改数组的数据,或者给对象新增属性,视图是不会重新渲染的,因此vue2就提供了$set方法用于解决该问题
vue3删除$set的原因:vue3是通过proxy实现响应式数据的,是对整个对象的监听,因此通过索引修改数组的数据,或者给对象新增属性,视图是会重新渲染的;同理废除了$delete
2、废除$on、$off、$once
在vue2中常用$on $off $once创建 event hub,以创建在整个应用程序中使用的全局事件侦听器,实现数据共享
vue3可以用外部库mitt来代替 $on $emit $off
九、指令
新增指令
1、v-is:类似于动态 2.x :is
绑定——因此要按组件的注册名称渲染组件,其值应为 JavaScript 字符串文本
下面举一个例子:
<template>
<div class="app">
<p class="app-title">App.vue页面</p>
<div id="alert"></div>
<div v-is="page[index]"></div>
<div @click="changeUsePage" class="btn">改变页面使用的组件</div>
</div>
</template>
<script>
import Page2 from './components/Page2.vue'
import Page1 from './components/Page1.vue'
export default {
name: 'App',
components: {
Page2,
Page1
},
data() {
return {
tips: 'page3',
page: ['Page2', 'Page1'],
index: 1
}
},
methods: {
changeUsePage() {
if (this.index === 0) {
this.index = 1
} else {
this.index = 0
}
}
}
}
</script>
页面初识化显示如图9-1,点击按钮
十、组合式API和响应性API
下图是新增的组合式API和响应性API
下面看下具体的API用法
组合式API
1、setup
1)类型:Function,是Vue3.x新增的选项;作为在组件内部使用组合式 API 的入口点(即组合式API和响应式API都在setup函数里面使用)
2)执行时机:在创建组件实例时,在初始 prop 解析之后立即调用 setup
。在生命周期方面,它是在 beforeCreate 钩子之前调用的
下面看个例子
export default {
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
setup() {
console.log('setup')
}
}
控制台输出结果:
3)参数: 接受两个参数---props和context
props:组件传入的属性,是响应式的,不能用ES6解构,否则会消除响应式
context:上下文对象,该对象有三个属性:slots、attrs、emit分别对应vue2中的this.$slots,this.$attrs,this.emit。由于setup中不能访问this对象,所以context提供了vue2中this常用的这三个属性,并且这几个属性都是自动同步最新的值
export default {
props: {
title: String
},
setup(props, context) {
console.log('props.title: ', props.title)
console.log('context.$attrs: ', context.$attrs)
console.log('context.$emit: ', context.$emit)
console.log('context.$slots: ', context.$slots)
let { title } = props.title // title失去响应性
}
}
4)返回值:对象、渲染函数h/JSX的方法
返回对象
<template>
<div class="page2">
<div>标题:{{title}}</div>
<!-- 从 setup 返回的 ref 在模板中访问时会自动展开,因此模板中不需要 .value -->
<div>count:{{count}}</div>
<div>object.foo:{{object.foo}}</div>
<div class="btn" @click="changeTitle">改变title的值</div>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'page2',
props: {
title: String
},
methods: {
changeTitle() {
this.$emit('on-chang-title', '改变后的标题')
}
},
setup(props) {
const count = ref(0)
const object = reactive({ foo: 'page' })
console.log('props.title: ', props.title)
// 暴露到template中
return {
count,
object
}
}
}
</script>
<style scoped>
.page2{
background: #fff;
margin-top: 30px;
padding: 15px;
}
.btn{
width: 100px;
background: #cac6c6;
line-height: 40px;
margin: 20px auto;
}
</style>
渲染函数h/JSX的方法
<script>
import { h, ref, reactive } from 'vue'
export default {
name: 'page2',
setup() {
const count = ref(0)
const object = reactive({ foo: 'page' })
return () => h('div', [count.value, object.foo])
}
}
</script>
2、在setup函数中使用的生命周期钩子
从下图可以看出,在setup中使用的生命周期钩子都增加了on;beforeCreated和created被setup替换,但是在vue3中仍然可以使用beforeCreated、created
下面是在setup中使用生命周期钩子(钩子需要引入)的示例
<template>
<div class="page2">
<div>标题:{{title}}</div>
<div class="btn" @click="changeTitle">改变title的值</div>
</div>
</template>
<script>
import { onBeforeMount, onMounted, onBeforeUpdate,onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from "vue"
export default {
name: 'page2',
props: {
title: String
},
methods: {
changeTitle() {
this.$emit('on-chang-title', '改变后的标题')
}
},
setup() {
console.log('-----setup-----')
// vue3中组合式API的生命周期钩子放在setup中调用
onBeforeMount(() => {
console.log('-----onBeforeMount-----')
})
onMounted(() => {
console.log('-----onMounted-----')
})
onBeforeUpdate(() => {
console.log('-----onBeforeUpdate-----')
})
onUpdated(() => {
console.log('-----onUpdated-----')
})
onBeforeUnmount(() => {
console.log('-----onBeforeUnmount-----')
})
onUnmounted(() => {
console.log('-----onUnmounted-----')
})
onErrorCaptured(() => {
console.log('-----onErrorCaptured-----')
})
onRenderTracked(() => {
console.log('-----onRenderTracked-----')
})
onRenderTriggered(() => {
console.log('-----onRenderTriggered-----')
})
}
}
</script>
3、getCurrentInstance
获取当前组件的实例,只能在setup、setup中使用的生命周期钩子中使用
下面是一个demo
<script>
import { h, onMounted, getCurrentInstance } from "vue"
export default {
name: 'page2',
setup() {
// also works if called on a composable
function useComponentId() {
return getCurrentInstance().uid
}
const internalInstance = getCurrentInstance() // works
console.log('setup getCurrentInstance(): ', getCurrentInstance())
const id = useComponentId() // works
console.log('setup useComponentId(): ', useComponentId())
const handleClick = () => {
getCurrentInstance() // doesn't work
console.log('handleClick getCurrentInstance(): ', getCurrentInstance())
useComponentId() // doesn't work
console.log('handleClick useComponentId(): ', useComponentId())
console.log('handleClick internalInstance: ', internalInstance) // works
}
onMounted(() => {
console.log('onMounted getCurrentInstance(): ', getCurrentInstance()) // works
console.log('onMounted useComponentId(): ', useComponentId()) // works
})
return () => h('button', {onClick: handleClick}, `uid: ${id}`)
}
}
</script>
显示效果如下:在setup中可以直接使用getCurrentInstance
getCurrentInstance()返回的对象如下所示:
当点击uid:1的时候,控制台显示如下:
在handleClick中直接使用getCurrentInstance(),返回为null;由于handleClick中调用getCurrentInstance返回null,因此在去读返回结果的属性,浏览器就会报错
响应式API
4、reactive、ref、toRef、toRefs
在vue2.x中, 数据一般都定义在data
中, 但Vue3.x 可以使用reactive
和ref
来进行数据定义
既然reactive
和ref都可以进行数据定义,那他们的区别是什么?使用时机是什么时候?
下面先看下ref、reactive的基本定义
1)ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
注意:
a)在JS中访问ref定义的数据,需要通过:属性名.value方式访问
b)在DOM中访问ref定义的数据,不需要通过.value方式访问
下面是一个demo:
<template>
<div class="page2">
<div>data中的msg:{{msg}}</div>
<div>setup中的</div>
<div>count:{{count}}</div>
<div>obj.num:{{obj.num}}</div>
<div>obj.name:{{obj.name}}</div>
</div>
</template>
<script>
import { ref } from "vue"
export default {
name: 'page2',
data() {
return {
msg: '测试'
}
},
setup() {
const count = ref(0)
let timer = null
let obj = ref({
num: 1,
name: '张三'
})
timer = setTimeout(() => {
clearTimeout(timer)
count.value += 1
obj.value.num += 1
obj.value.name = '李四'
}, 8000)
return {
count,
obj
}
}
}
</script>
页面初始化显示效果:
8s后的显示效果
注意:从上面的demo代码可以看出,
a)ref可以代理对象和基本类型,例如字符串、数字、boolean等;
b)选型data定义的数据和组合式API中的ref定义的数据是共存的;vue3之所以推出ref来定义数据,是为了将数据、和方法放在一块,便于代码的维护等
问题:如果在data和ref中定义相同的变量会发生什么呢?
将上面的代码修改如下,只是在setup中定义一个同data中的msg变量,并返回
<template>
<div class="page2">
<div>data中的msg:{{msg}}</div>
<div>setup中的</div>
<div>count:{{count}}</div>
<div>obj.num:{{obj.num}}</div>
<div>obj.name:{{obj.name}}</div>
</div>
</template>
<script>
import { ref } from "vue"
export default {
name: 'page2',
data() {
return {
msg: '测试'
}
},
setup() {
const count = ref(0) // count虽然定义成const,但是count为ref对象,因此可以修改属性value的值
let timer = null
let obj = ref({
num: 1,
name: '张三'
})
let msg = '测试2' // 新增
timer = setTimeout(() => {
clearTimeout(timer)
count.value += 1
obj.value.num += 1
obj.value.name = '李四'
}, 8000)
return {
count,
obj,
msg // 新增
}
}
}
</script>
控制台抛错如下:
40:7 error Duplicated key 'msg' vue/no-dupe-keys
即data和ref中定义的返回数据不能同名
2)reactive:返回对象的响应式副本,但不能代理基本类型,例如字符串、数字、boolean等
下面看个demo
<template>
<div class="page2">
<div>obj.num:{{obj.num}}</div>
<div>obj.name:{{obj.name}}</div>
<div>count:{{count}}</div>
</div>
</template>
<script>
import { reactive } from "vue"
export default {
name: 'page2',
setup() {
let timer = null
let obj = reactive({
num: 1,
name: '张三'
})
let count = reactive(0)
timer = setTimeout(() => {
clearTimeout(timer)
obj.num += 1
obj.name = '李四'
count += 1
}, 8000)
return {
obj,
count
}
}
}
</script>
页面初始化效果:
8s后显示效果
通过上面两张图可以看出:reactive可以代理一个对象,并且是响应式的;但不能代理基本类型,否则控制台会提示定义的变量不是响应式的
因此我们可以得出ref和reactive定义的数据的区别和使用时机:
a)ref返回对象或者基础类型的响应式副本;在JS中访问ref定义的数据,需要通过:属性名.value方式访问
b)reactive只返回对象的响应式副本
c)当需要定义响应式对象的数据的时候,可以用ref和reactive定义变量;当需要定义响应式的基础类型数据的时候,用ref定义变量
3)toRefs
定义:将响应式对象转为普通对象,结果对象的每个 property 都是指向原始对象相应 property 的ref
在上面的demo中,在DOM中需要通过obj.num,obj.name访问对象的属性,这样写有点麻烦,我们可以将obj的属性结构出来吗?答案是:否,因为会消除他的响应式
问题:如果我们就是想在DOM中用结构后的数据,怎么办呢?答案是使用toRefs
下面看个demo
<template>
<div class="page2">
<div>obj.num:{{obj.num}}</div>
<div>obj.name:{{obj.name}}</div>
<div>num:{{num}}</div>
<div>name:{{name}}</div>
</div>
</template>
<script>
import { reactive, toRefs } from "vue"
export default {
name: 'page2',
setup() {
let timer = null
let timer2 = null
let obj = reactive({
num: 1,
name: '张三'
})
const objAsRefs = toRefs(obj)
timer = setTimeout(() => {
clearTimeout(timer)
obj.num += 1
obj.name = '李四'
}, 8000)
timer2 = setTimeout(() => {
clearTimeout(timer2)
objAsRefs.num.value += 1
objAsRefs.name.value = '王老五'
}, 14000)
return {
obj,
...objAsRefs
}
}
}
</script>
页面初始化显示效果如下:
8s后显示效果如下:
14s后显示效果如下:
结论:使用toRefs可以在DOM中使用对象结构出来的数据,并且还保持着响应式性质
4)toRef
定义:可以用来为源响应式对象上的 property 性创建一个 ref
。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接
下面看个demo
<template>
<div class="page2">
<div>obj.num:{{obj.num}}</div>
<div>obj.name:{{obj.name}}</div>
<div>nameRef:{{nameRef}}</div>
</div>
</template>
<script>
import { reactive, toRef } from "vue"
export default {
name: 'page2',
setup() {
let timer = null
let timer2 = null
let obj = reactive({
num: 1,
name: '张三'
})
const nameRef = toRef(obj, 'name')
timer = setTimeout(() => {
clearTimeout(timer)
obj.num += 1
obj.name = '李四'
}, 8000)
timer2 = setTimeout(() => {
clearTimeout(timer2)
nameRef.value = '王老五'
}, 14000)
return {
obj,
nameRef
}
}
}
</script>
页面初始化渲染效果如下:
8s后的效果如下:
14s后的效果如下:
结论:
a)toRef是为源响应式对象上的具体的 property 创建一个 ref
,并且保持对其源 property 的响应式连接;
b)toRefs将响应式对象转为普通对象,结果对象的每个 property 都是指向原始对象相应 property 的ref
5)shallowReactive
定义:
创建一个响应式代理,该代理跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 (暴露原始值)
demo1:
<script>
import { watchEffect, shallowReactive } from "vue"
export default {
name: 'page2',
setup() {
const original = shallowReactive({
count: 0,
info: {
name: '张三'
}
})
watchEffect(() => {
// 适用于响应性追踪
console.log('original.count', original.count)
})
// 变更original 会触发侦听器依赖副本
original.count = 1
}
}
</script>
控制台显示效果:
当修改count的值时,会触发watchEffect
demo2:
<template>
<div>name:{{original.info.name}}</div>
</template>
<script>
import { watchEffect, shallowReactive } from "vue"
export default {
name: 'page2',
setup() {
const original = shallowReactive({
count: 0,
info: {
name: '张三'
}
})
watchEffect(() => {
// 适用于响应性追踪
console.log('original.info.name', original.info.name)
})
// 变更original 会触发侦听器依赖副本
original.info.name = '李四'
return {
original
}
}
}
</script>
当修改original.info.name的时候,不会触发watchEffect
6)isReactive
定义:检查对象是否是 reactive
创建的响应式 proxy;如果 proxy 是 readonly
创建的,但还包装了由 reactive
创建的另一个 proxy,它也会返回 true
7)isProxy
定义:检查对象是 reactive
还是 readonly
创建的代理
返回值:Boolean,true--是reactive创建的代理,false-
是 readonly
创建的代理
<script>
import { reactive, ref, isProxy } from "vue"
export default {
name: 'page2',
setup() {
const reactiveObj = reactive({
count: 0,
info: {
name: '张三'
}
})
const refNum = ref(0)
console.log('reactiveObj: ', isProxy(reactiveObj))
console.log('refNum: ', isProxy(refNum))
}
}
</script>
8)unref-----拆出原始值的语法糖
定义:如果参数为 ref
,则返回内部值value,否则返回参数本身;它是 val = isRef(val) ? val.value : val的语法糖
9)isRef
定义:检查一个值是否为ref对象
10)shallowRef
定义:创建一个 ref,它跟踪自己的 .value
更改,但不会使其值成为响应式的
下面看个用shallowRef定义一个对象的demo
<template>
<div class="page2">
<div>obj.name:{{obj.name}}</div>
<div>obj.jobInfo.companyName:{{obj.jobInfo.companyName}}</div>
</div>
</template>
<script>
import { shallowRef } from "vue"
export default {
name: 'page2',
setup() {
const obj = shallowRef({
name: '张三',
jobInfo: {
companyName: '张三的公司'
}
})
let timer = null
let timer2 = null
timer = setTimeout(() => {
clearTimeout(timer)
obj.value.name = '李四'
obj.value.jobInfo.companyName = '李四的公司'
console.log('8s后的obj:', obj.value)
}, 8000)
timer2 = setTimeout(() => {
clearTimeout(timer2)
obj.value = {
name: '王老五',
jobInfo: {
companyName: '王老五的公司'
}
}
console.log('14s后的obj:', obj.value)
}, 14000)
return {
obj
}
}
}
</script>
页面初始化效果如下:
8s后的效果:
14s后的效果:
结论:从上面的效果图可以看出,shallowRef定义的对象型数据,没有响应性;但是如果给该对象的value重新赋值,可以在DOM中更新
注意:shallowRef定义的一般类型数据仍然具有响应性
问题:如果在代码中同时用shallowRef定义一般类型和object类型数据,修改一般类型属性的值,能在DOM中变更吗?
如下demo中用shallowRef同时定义了一般类型和object类型数据
<template>
<div class="page2">
<div>obj.name:{{obj.name}}</div>
<div>obj.jobInfo.companyName:{{obj.jobInfo.companyName}}</div>
<div>count:{{count}}</div>
</div>
</template>
<script>
import { shallowRef } from "vue"
export default {
name: 'page2',
setup() {
let count = shallowRef(0)
const obj = shallowRef({
name: '张三',
jobInfo: {
companyName: '张三的公司'
}
})
let timer = null
let timer2 = null
timer = setTimeout(() => {
clearTimeout(timer)
obj.value.name = '李四'
obj.value.jobInfo.companyName = '李四的公司'
console.log('8s后的obj:', obj.value)
count.value = 1
console.log('8s后的count:', count.value)
}, 8000)
timer2 = setTimeout(() => {
clearTimeout(timer2)
obj.value = {
name: '王老五',
jobInfo: {
companyName: '王老五的公司'
}
}
console.log('14s后的obj:', obj.value)
count.value = 2
console.log('14s后的count:', count.value)
}, 19000)
return {
obj,
count
}
}
}
</script>
页面初始化渲染效果:
8s后的效果:即数据更新了,DOM中的数据也跟着更新了-------即用shallowRef定义的一般类型数据,具有响应式
14s后的效果:
11)triggerRef
定义:手动执行与 shallowRef
关联的任何效果
问题:上述demo中,用shallowRef定义一个object,然后修改其property,在DOM中没有更新;如果我们想用shallowRef定义object数据,同时修改property值后在DOM中更新,怎么办呢?
答案:使用triggerRef
<template>
<div class="page2">
<div>obj.name:{{obj.name}}</div>
<div>obj.jobInfo.companyName:{{obj.jobInfo.companyName}}</div>
</div>
</template>
<script>
import { shallowRef, triggerRef } from "vue"
export default {
name: 'page2',
setup() {
const obj = shallowRef({
name: '张三',
jobInfo: {
companyName: '张三的公司'
}
})
let timer = null
timer = setTimeout(() => {
clearTimeout(timer)
obj.value.name = '李四'
obj.value.jobInfo.companyName = '李四的公司'
console.log('8s后的obj:', obj.value)
triggerRef(obj) // 触发DOM更新
}, 8000)
return {
obj
}
}
}
</script>
页面初始化效果:
8s后的效果:
12)customRef
定义:创建一个自定义的 ref,并对其依赖项跟踪track和更新触发trigger进行显式控制
参数:它需要一个工厂函数,该函数接收 track
和 trigger
函数作为参数
返回值:返回一个带有 get
和 set
的对象
下面看一个demo:使用 v-model
、自定义 ref 实现值绑定的示例
<template>
<div class="page2">
<input class="input" type="text" v-model="text" @change="changeVal">
</div>
</template>
<script>
import { customRef } from "vue"
export default {
name: 'page2',
setup() {
function useDebouncedRef(value, delay= 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
// do something
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// do something
trigger()
}, delay)
}
}
})
}
function changeVal(val) {
console.log('val.target.value', val.target.value)
console.log('val: ', val)
}
let text = useDebouncedRef('hello')
return {
text,
changeVal
}
}
}
</script>
页面初始化效果:
改变输入框的值,显示:
13)computed
a)参数是 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象,即不可直接修改其value值
b)参数具有 get
和 set
函数的对象,来创建可写的 ref 对象
下面看下参数是getter函数demo
<template>
<div class="page2">
<div>plusOne:{{plusOne}}</div>
</div>
</template>
<script>
import { ref, computed } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(1)
const plusOne = computed(() => count.value + 1) // count.value改变时,plusOne.value也跟着改变
console.log(plusOne.value) // 2
plusOne.value++ // error,不能直接修改plusOne.value的值,浏览器会有警告
return {
plusOne
}
}
}
</script>
页面显示效果:
下面看下参数是具有 get
和 set
函数的对象的demo
<template>
<div class="page2">
<div>count:{{count}}</div>
<div>plusOne:{{plusOne}}</div>
</div>
</template>
<script>
import { ref, computed } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
setTimeout(() => {
plusOne.value = 3
console.log(count.value)
}, 8000)
return {
plusOne,
count
}
}
}
</script>
页面初始化效果:
8s后显示效果:
14)watch、watchEffect
watch定义:watch
API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch
需要侦听特定的 data 源,并在单独的回调函数中副作用。默认情况下,它也是惰性的——即,回调是仅在侦听源发生更改时调用
watch(source, callback, [options])
参数说明:
source:ref,reactive Object,getter/effect function,Array(item是前面几个类型);用于指定要侦听的响应式变量
callback:执行的回调函数
option:支持deep、immediate 和 flush 选项
返回值:返回停止监听的函数
侦听单一数据源: data 源可以是返回值的 getter 函数,也可以是 ref
a)侦听一个getter:侦听active对象的某个属性
<template>
<div class="page2">
<div>name:{{name}}</div>
<div>age:{{age}}</div>
</div>
</template>
<script>
import { reactive, toRefs, watch } from "vue"
export default {
name: 'page2',
setup() {
const userInfo = reactive({ name: "张三", age: 10 })
setTimeout(() =>{
userInfo.name = '李四'
userInfo.age = 12
},7000)
// 修改age值时会触发 watch的回调
watch(
() => userInfo.age,
(curAge, preAge) => {
console.log("age新值:", curAge, "age老值:", preAge)
}
)
return {
...toRefs(userInfo)
}
}
}
</script>
页面初识化效果:
7s后的效果:
如果将watch里面的source改成直接监听reactive的prop,其余代码不变,会发生什么呢?
watch(userInfo.name, (cur, pre) => {
console.log('curName: ', cur)
console.log('preName: ', pre)
})
控制台会警告:watch的source只能是下面的形式:
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
getter函数侦听的是ref对象的属性
const person = ref({
name: '1',
age: 1
})
setTimeout(() =>{
person.value.name = 2
}, 100)
watch(() => person.value.name, (n, o) => {
console.log(n)
console.log(o)
})
输出1 2
b)直接侦听一个ref
普通数据类型
<template>
<div class="page2">
<div>count:{{count}}</div>
</div>
</template>
<script>
import { ref, watch } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(0)
setTimeout(() =>{
count.value++
}, 7000)
watch(
count,
(curCount, preCount) => {
console.log("count新值:", curCount, "count老值:", preCount)
}
)
return {
count
}
}
}
</script>
页面初识化效果:
7s后的效果:
直接侦听一个ref引用类型对象:
setup() {
const person = ref({
name: '1',
age: 1
})
setTimeout(() =>{
person.value.name = 2
}, 100)
watch(person, (n, o) => {
console.log(n, o)
})
}
像这样是监听不到的,需要在watch里面添加第三个参数:deep:true
但是只能拿到新的值,旧的值拿不到,即n. o的值一样都是变化后的值
watch(person, (n, o) => {
console.log(n)
console.log(o)
}, {deep: true})
直接侦听ref对象的某个属性,会抛类型错误
c)直接侦听reactive
<template>
<div class="page2">
<div>count:{{count}}</div>
<div>name:{{userInfo.name}}</div>
<div>age:{{userInfo.age}}</div>
</div>
</template>
<script>
import { ref, watch, reactive } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(0)
const userInfo = reactive({ name: "张三", age: 10 })
setTimeout(() =>{
count.value++
userInfo.name = '李四'
userInfo.age = 12
}, 2000)
watch(userInfo, (cur, pre) => {
console.log('curUserInfo: ', cur)
console.log('preUserInfo: ', pre)
})
return {
count,
userInfo
}
}
}
</script>
页面初始化效果:
2s后的显示效果如下:
可以看到:如果直接监听一个reactive,那么只会返回变化的值,之前的状态拿不到
如果reactive对象内属性是嵌套多层的对象,那么在watch中不添加第三个参数deep:true,也是可以监听到变化的,因为reactive类型是响应式的,内部是直接代理整个对象,当里面属性发生变化的时候,会反应出来的
侦听多个数据源(source使用数组)
上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据
<template>
<div class="page2">
<div>count:{{count}}</div>
<div>name:{{userInfo.name}}</div>
<div>age{{userInfo.age}}</div>
</div>
</template>
<script>
import { ref, watch, reactive } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(0)
const userInfo = reactive({ name: "张三", age: 10 })
setTimeout(() =>{
count.value++
userInfo.name = '李四'
userInfo.age = 12
}, 2000)
watch([() => userInfo.age, count], ([curAge, curCount], [preAge, preCount]) => {
console.log("curAge:", curAge, "preAge:", preAge)
console.log("curCount:", curCount, "preCount:", preCount)
})
return {
count,
userInfo
}
}
}
</script>
页面初始化的时候效果:
2s后的显示效果:
侦听复杂的嵌套对象(使用第三个参数:options)
const info = reactive({
room: {
id: 200,
attrs: {
size: "400平方米",
type:"三室两厅"
}
}
});
watch(() => info.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType)
}, { deep: true, immediate: true })
监听复杂的嵌套对象,如果不使用第三个参数deep:true
, 是无法监听到数据变化的
默认情况下,watch是惰性的
那怎么样可以立即执行回调函数呢?答案是: 给第三个参数设置immediate: true
即可
停止侦听
在组件中创建的watch
监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()
函数的返回值
const stopWatchCount = watch(
count,
(curCount, preCount) => {
console.log("count新值:", curCount, "count老值:", preCount)
}
)
setTimeout(() => {
stopWatchCount()
}, 100000)
watchEffect
定义:在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它
<template>
<div class="page2">
<div>count:{{count}}</div>
<div>name:{{name}}</div>
<div>age:{{age}}</div>
<div>num:{{num}}</div>
</div>
</template>
<script>
import { ref, watchEffect, reactive, toRefs } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(0)
const userInfo = reactive({
name: '张三',
age: '11'
})
let num = 2
setTimeout(() =>{
count.value++
userInfo.age++
num++
}, 7000)
watchEffect(() => {
console.log('count: ', count.value)
console.log('userInfo:', userInfo)
console.log('num: ', num)
})
return {
...toRefs(userInfo),
count,
num
}
}
}
</script>
从控制台打印信息可以看出,watchEffect里面的方法执行了两次,一次是页面渲染完成的时候,另一次是过了7s后修改值的时候
watchEffect可以监听到复杂数据吗?
<template>
<div class="page2">
<div>count:{{count}}</div>
<div>name:{{name}}</div>
<div>age:{{age}}</div>
<div>num:{{num}}</div>
</div>
</template>
<script>
import { ref, watchEffect, reactive, toRefs } from "vue"
export default {
name: 'page2',
setup() {
const count = ref(0)
const userInfo = reactive({
name: '张三',
age: '11',
job: {
title: 'test'
}
})
let num = 2
setTimeout(() =>{
count.value++
userInfo.job.title = 'change'
userInfo.name = 'change'
num++
}, 8000)
watchEffect(() => {
console.log('userInfo:', userInfo)
console.log('userInfo.job:', userInfo.job)
})
return {
...toRefs(userInfo),
count,
num
}
}
}
</script>
页面初识化效果如下:
8s后的效果:
可以看到DOM更新了,但是watchEffect里面没有监听到数据的变化,即watchEffect里面监听不到复杂数据的变化
watch与watchEffect的区别
a)watchEffect 不需要手动传入依赖
b)watchEffect 会先执行一次用来自动收集依赖
c)watchEffect 无法获取到变化前的值, 只能获取变化后的值
12)readonly
定义:获取一个对象 (响应式或纯对象) 或 ref ,返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的
<script>
import { reactive, watchEffect, readonly } from "vue"
export default {
name: 'page2',
setup() {
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 适用于响应性追踪
console.log('copy.count:', copy.count)
})
// 变更original 会触发侦听器依赖副本
original.count++
// 变更副本将失败并导致警告
setTimeout(() => {
copy.count++ // 不能对只读变量做修改,否则控制台会警告
}, 8000)
}
}
</script>
控制台显示:
说明:执行setup函数的化,马上执行一次watchEffect函数里面的回调,打印: copy.count: 0;接着执行:original.count++,马上触发watchEffect函数里面的回调,打印: copy.count: 1;8s后对只读对象copy.count++进行加1,控制台会提示,目标表象是只读的,不能对其修改
结论:
a)readonly返回一个只读代理,不能直接对其做修改;
b)如果其原始代理做了修改,只读代理也会跟着改变;
c)如果原始代理是响应式的,或者ref,可以在watchEffect中监听到只读代理的变化
15)shallowReadonly
定义:创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)
demo1:
<script>
import { reactive, watchEffect, shallowReadonly } from "vue"
export default {
name: 'page2',
setup() {
const original = reactive({
count: 0,
info: {
name: '张三'
}
})
const copy = shallowReadonly(original)
watchEffect(() => {
// 适用于响应性追踪
console.log('copy.count:', copy.count)
})
// 变更original 会触发侦听器依赖副本
original.count++ // 触发watchEffect
// 变更副本将失败并导致警告
setTimeout(() => {
copy.count++
}, 3000)
}
}
</script>
demo2:
<script>
import { reactive, watchEffect, shallowReadonly } from "vue"
export default {
name: 'page2',
setup() {
const original = reactive({
count: 0,
info: {
name: '张三'
}
})
const copy = shallowReadonly(original)
watchEffect(() => {
// 适用于响应性追踪
console.log('copy.info.name', copy.info.name)
})
// 变更original 会触发侦听器依赖副本
original.info.name = '李四'
setTimeout(() => {
copy.info.name = '王老五' // 正常
}, 3000)
}
}
</script>
通过demo1和demo2的对比,可以看出:对于响应式的原对象,shallowReadonly返回的对象,可以修改其嵌套对象里面的属性值,即只读只对其首层属性有作用
demo3:
<script>
import { ref, watchEffect, shallowReadonly } from "vue"
export default {
name: 'page2',
setup() {
const original = ref({
count: 0,
info: {
name: '张三'
}
})
const copy = shallowReadonly(original)
watchEffect(() => {
// 适用于响应性追踪
console.log('copy.count', copy.value.count)
})
// 变更original 会触发侦听器依赖副本
original.value.count = 1
setTimeout(() => {
copy.value.count = 2 // 正常
}, 3000)
}
}
</script>
demo4:
<script>
import { ref, watchEffect, shallowReadonly } from "vue"
export default {
name: 'page2',
setup() {
const original = ref({
count: 0,
info: {
name: '张三'
}
})
const copy = shallowReadonly(original)
watchEffect(() => {
// 适用于响应性追踪
console.log('copy.info.name', copy.value.info.name)
})
// 变更original 会触发侦听器依赖副本
original.value.info.name = '李四'
setTimeout(() => {
copy.value.info.name = '王老五'
}, 3000)
}
}
</script>
通过demo3和demo4可知,对于原对象是ref,shallowReadonly返回的只读对象代理,可以直接修改只读对象的属性
16)isReadonly:
检查对象是否是由readonly
创建的只读代理
17)markRaw
定义:标记一个对象,使其永远不会转换为代理。返回对象本身
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
18)toRaw
返回 reactive
或 readonly
代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
十一、简单说下vue2.x和vue3.x的响应式
我们都知道vue2.x中实现数据的响应式重要的一环是Object.defineProperty,但在vue3中确使用Proxy代替了它
下面做个简单的对比:
1)Object.defineProperty只对对象的属性进行劫持,因此需要遍历对象的属性,如果属性是对象就还得递归,进行深度遍历;Proxy是直接代理对象,因此不需要递归
2)Object.defineProperty对新增属性,需要手动Observe;因此在vue2.x中,给数组或对象新增属性时,需要$set,才能保证新增属性是响应式,而$set内部也是通过Object.defineProperty进行处理
十二、Fragment片段
在vue2.x中,template下只允许有一个根节点;而vue3.x可以有多个根节点
<!-- vue2.x写法 -->
<template>
<div>
<div>test</div>
<div>test </div>
</div>
</template>
<!-- vue3.x写法 -->
<template>
<div>test</div>
<div>test </div>
</template>
十三、v-model用法升级
v-model的用法变化如下:
1)变更:在自定义组件上使用v-model
时, 属性以及事件的默认名称变了
2)变更:v-bind
的.sync
修饰符在 Vue 3 中被去掉了, 合并到了v-model
里
3)新增:同一组件可以同时设置多个 v-model
4)新增:开发者可以自定义 v-model
修饰符
1)在自定义组件上使用v-model
时, 属性以及事件的默认名称变了
vue2.x中,使用v-model绑定的默认属性:value;默认事件:input
vue3.x中,使用v-model绑定的默认属性:modelValue;默认事件:update:modelValue
// Vue2.x中, 在组件上使用 v-model相当于传递了value属性, 并触发了input事件
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :value="searchValue" @input="searchValue=$event"><search-input>
// vue3.x中,在组件上使用v-model相当于传递modelValue属性,并触发update:modelValue事件
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :modelValue="searchValue" @update:modelValue="searchValue=$event"><search-input>
2)变更:v-bind
的.sync
修饰符在 Vue 3 中被去掉了, 合并到了v-model
里
举一个双向绑定的例子:
在vue2.x中,一个modal弹窗,外部可以控制组件的属性visible,达到弹窗的显示、隐藏效果;组件内部可以控制visible属性隐藏,并将visible属性同步传输到外部
组件内部, 当我们关闭modal
时, 在子组件中以update:visible模式触发事件:
this.$emit('update:visible', false)
然后父组件可以监听这个事件进行数据更新
<modal :visible.async="isVisible"></modal>
// 相当于
<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
在vue3中,可以给v-model传递属性visible,达到绑定属性的目的
<modal v-model:visible="isVisible" v-model:content="content"></modal>
<!-- 相当于 -->
<modal
:visible="isVisible"
:content="content"
@update:visible="isVisible"
@update:content="content"
/>
从而可以看出,Vue 3 中抛弃了.async
写法, 统一使用v-model
3)新增:同一组件可以同时设置多个 v-model
从上面的例子可以看出同一个组件可以设置多个v-model
十四、将具名插槽slot、作用域插槽slot-scope改成统一使用v-slot(vue2.6.0就有了)
在vue2中使用具名插槽
<!-- 子组件中:-->
<slot name="title"></slot>
<!-- 父组件中:-->
<div slot="title">
<h1>歌曲:成都</h1>
<div>
在vue2中使用作用域插槽(在slot上绑定数据)
// 子组件
<slot name="content" :lesson="lesson"></slot>
export default {
data(){
return{
lession:['English', 'Chinese']
}
}
}
<!-- 父组件中使用 -->
<template slot="content" slot-scope="scoped">
<div v-for="item in scoped.lession">{{item}}</div>
<template>
vue3中使用v-slot实现具名插槽和作用域插槽
// 子组件
<slot name="content" :lesson="lesson"></slot>
export default {
data(){
return{
lession:['English', 'Chinese']
}
}
}
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<div v-for="item in scoped.lession">{{item}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{ lession }">
<div v-for="item in lession">{{item}}</div>
</template>
下面看下v-slot的具体用法
1)默认插槽
a)如果不设置<slot>元素的name属性,那么出口会带有隐含的名字:default;注意:默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确
b)v-slot:可以简写为#;注意:简写方式只适用于有参数适合有效???
c)v-slot只能用于<template>元素上;除了当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用
<-- 子组件Child中 -->
<main>
<slot></slot>
</main>
<-- 父组件Parent中 -->
<Child>
<template v-slot:defalut>
<div>default</div>
</tempalte>
</Child>
<-- 父组件Parent中写法2 -->
<Child>
<template v-slot>
<div>default</div>
</tempalte>
</Child>
<-- 父组件Parent中写法3,有效 -->
<-- 但这与文档中说法有矛盾:文档说这种写法无效 -->
<Child #>
<div>default</div>
</Child>
2)具名插槽
给<slot>元素设置name属性
<-- 子组件Child中 -->
<header>
<slot name=""header></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
<-- 父组件Parent中 -->
<Child>
<template #header>
<div>header</div>
</tempalte>
<template #default>
<div>main</div>
</tempalte>
<template #footer>
<div>footer</div>
</tempalte>
</Child>
3)作用域插槽
场景:在父组件作用域中访问子组件数据
<-- 子组件Child中 -->
<main>
<slot :lession="lession"></slot>
</main>
<-- 父组件Parent中 -->
<Child>
<template #default="slotProps">
<div>{{slotProps.lession}}</div>
</tempalte>
</Child>
4)解构插槽Props
<-- 子组件Child中 -->
<main>
<slot :lession="lession"></slot>
</main>
<-- 父组件Parent中 -->
<Child>
<template #default="{ lession }">
<div>{{ lession }}</div>
</tempalte>
</Child>
<-- 父组件Parent中写法2,prop 重命名 -->
<Child>
<template #default="{ lession: class }">
<-- 父组件Parent中写法2,lession重命名为class -->
<div>{{ class }}</div>
</tempalte>
</Child>
<-- 父组件Parent中写法3,定义后备内容 -->
<Child>
<template #default="{ lession = ['French] }">
<-- 父组件Parent中写法2,lession重命名为class -->
<div>{{ lession }}</div>
</tempalte>
</Child>
5)动态插槽名
<-- 父组件Parent中 -->
<Child>
<template #default="dynamicName">
<div>{{ lession }}</div>
</tempalte>
</Child>
// 在父组件中,根据不同条件设置dynamicName的值,来展示相应的插槽内容
参考文章:
1、vue3中文文档:https://vue3js.cn/docs/zh/api/global-api.html#%E5%8F%82%E6%95%B0
2、vue3英文文档:https://v3.vuejs.org/guide/component-custom-events.html#validate-emitted-events
1、关于vue3中的render方法:https://juejin.cn/post/6844904205426098183
3、Vue3.0 新特性以及使用变更总结(实际工作用到的):https://mp.weixin.qq.com/s/OKCxvrUPoPM0hR-z9reESA
4、vue3全局API变更:https://blog.csdn.net/qq_38290251/article/details/112412522
更多推荐
所有评论(0)