下面是关于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全局APIvue3应用实例API
Vue.componentapp.component
Vue.configapp.config
Vue.config.productionTipremove
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.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生命周期图谱
vue3生命周期钩子函数图谱
 

           

vue2.x生命周期钩子函数图谱
 

                                                                                                                                                                        

vue3中新增的选项生命周期钩子还有:renderTracked        renderTriggere,具体用法可以参考官文

七、移除的实例property

vue3和vue2.x的实例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 可以使用reactiveref来进行数据定义

既然reactiveref都可以进行数据定义,那他们的区别是什么?使用时机是什么时候?

下面先看下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>

控制台显示:

页面初始化控制台打印信息

8s后控制台打印的信息

说明:执行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>
页面初始化,控制台打印信息

3s后控制台打印信息

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>
页面初识化,控制台打印信息

3s后控制台信息

通过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>
页面初始化控制台信息

3s后控制台信息
​​​​​​

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>
页面初始化控制台信息
3s后控制台信息

通过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

3、vue3中文文档:https://vue3js.cn/docs/zh/guide/migration/render-function-api.html#%E6%B8%B2%E6%9F%93%E5%87%BD%E6%95%B0%E7%AD%BE%E5%90%8D%E6%9B%B4%E6%94%B9

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

Logo

前往低代码交流专区

更多推荐