vue3新特性和新用法

下面是关于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生命周期图谱
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

猜你喜欢

转载自blog.csdn.net/tangxiujiang/article/details/115364243