包裹自定义的alert组件,to属性:是有效的查询选择器.._vue3 new vue">
赞
踩
下面是关于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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。