在学习前,我们要知道:
# 一、API 风格
Vue 的组件可以按两种不同的风格书写:选项式 API 和 组合式 API 。
比较
官方: https://cn.vuejs.org/guide/extras/composition-api-faq.html
官方也比较人性化,文档是一些示例代码你可以自行选择那种 API 风格:
# 1、选项式 API
特点
选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例
在 非单文件 中:
和 vue2 不同,vue3 中的 data 只能使用函数式。
<div id="app">{{ count }}</div>
<script type="text/JavaScript">
Vue.createApp({
data() {
return {
count: 0
}
}
}).mount('#app')
</script>
在 单文件组件(.vue) 中:
关于 this 指向问题
不能使用箭头函数来定义 method 函数
箭头函数绑定的是父级作用域的上下文(最终指向 windows)
正常情况下,methods 中的函数其 this,指向的是 publicThis,其他可以查看 vue3 源码中的applyOptions (opens new window)
const publicThis = instance.proxy
<template>{{ count }}</template>
<script>
export default {
// reactive data
data() {
return {
count: 0
}
},
// methods
methods: {
increment() {}
},
// lifecycle hooks
mounted() {}
}
</script>
TS ✍ 与 选项式 API
在 vue3 早期的时候,为了更好的在 vue3 组件中使用 TS,往往会使用defineComponent
- 单个 vue 文件
<script lang="ts">
import { defineComponent } from 'vue'
// 提示更好ts类型提示(volar插件会智能提示🤔)
export default defineComponent({
data() {
return {}
},
methods: {}
// ……
})
</script>
- 一次性声明
// .env.d.ts
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
当然,现在 vue3 官方提供的 Vue.vscode-typescript-vue-plugin (opens new window) 插件是可以直接识别并提供 ts 辅助提示的,不用下面的 ts 声明也可以。
# 2、组合式 API ✨
特点
<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用。
为什么要使用 🤔 组合式 API?
参考:
优点:
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,这种形式更加自由,组织和逻辑复用 (opens new window)的模式变得更加强大(对比 vue2 中的 minxins 混入 👀 的 this 指向问题)
基于组合式 API 提供的逻辑复用能力,还孵化一些社区项目,比如:VueUse (opens new window)
在 非单文件 中:
并且,在 HTML 中无法直接使用 ES6 模块的 import 语法
<div id="app">{{ count }}</div>
<script>
// import { createApp } from 'vue'
// 注意:🎈
const { createApp, ref } = Vue
createApp({
setup() {
const count = ref(0)
return { count }
}
}).mount('#app')
</script>
在 单文件组件(.vue) 中:
- 方式 1
setup()
钩子是组合式 API 的入口
<template>{{ count }}</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// reactive data
const count = ref(0)
// methods
function increment() {}
// lifecycle hooks
onMounted(() => {})
return {
count,
increment
}
}
}
</script>
- 方式 2 👍
组合式 API 通常会与
<script setup>
(opens new window) 搭配使用
优点:
-<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖
- 无需 return 声明的变量、函数以及 import 引入的内容
- 引入组件将自动注册
- 在
<script setup>
中必须使用 defineProps 和 defineEmits API 来替代 props 和 emits - 需主动向父组件暴露子组件属性 :defineExpose
<template>{{ count }}</template>
<script setup>
import { ref, onMounted } from 'vue'
// reactive data
const count = ref(0)
// methods
function increment() {}
// lifecycle hooks
onMounted(() => {})
</script>
# 3、混合开发
用于组合式有一个入口 setup,我们不难想到:
在 setup 中不能访问到Vue2.x 配置(data、methos、computed...)
Vue2.x 配置(data、methos、computed...)中可以访问到 setup 中的属性、方法
思考
这里的 setup 和闭包有什么 🤔 相似之处?
出现重名,以 setup 为准
使用示例
<template>{{ count }}</template>
<script>
import { ref } from 'vue'
export default {
// 1、组合式
setup() {
const count = ref(1)
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},
// 2、选项式
data() {
return {
count: 2
}
},
mounted() {
// 出现重名,以setup🤔为准
console.log(this.count) // 1
}
}
</script>
# 二、ref 和 reactive
# 1、ref()
接受一个内部值,返回一个 响应式的、可更改的 ref 对象(引用对象),此对象只有一个指向其内部值的属性 .value
。
提示
在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。
# ① 原始数据类型
代码演示(常规类型)
<div id="app">
内容1(普通):{{ msg1 }} <br />
内容2(响应式):{{ msg2 }} <br />
<button @click="changeValue">修改内容</button>
</div>
<script>
const { createApp, ref, onMounted } = Vue
createApp({
setup() {
let msg = 'lencamo'
let msg1 = msg // 普通
let msg2 = ref(msg) // 响应式
onMounted(() => {
console.log(msg1) // "lencamo"
console.log(msg2) // RefImpl
})
function changeValue() {
msg1 = 'change success'
// 注意🤔
// msg2 = "change success";
msg2.value = 'change success'
}
return {
msg1,
msg2, // 自动浅层解包 msg2.value
changeValue
}
}
}).mount('#app')
</script>
# ② 对象数据类型
代码演示(数组、对象)
<div id="app">
{{ user }} <br />
<button @click="changeValue">修改内容</button>
</div>
<script>
const { createApp, ref, onMounted } = Vue
createApp({
setup() {
// 1、使用ref
let user = ref({
name: 'lencamo',
age: 20
})
onMounted(() => {
console.log(user) // RefImpl
console.log(user.value) // Proxy(Object) -->(这里并不是RefImpl🤔)
})
function changeValue() {
// 注意🤔(why?)
// user.value.name.value = "ren"
user.value.name = 'ren'
}
return {
user,
person,
changeValue
}
}
}).mount('#app')
</script>
way?的原因:具体见下面的 reactive()
# ③ 思考 ✍
我们通过打印一下,vue3 中的响应式和 vue2 中的响应式实现有什么区别呢?
结论:
ref( )对基本类型作响应式处理时,依然使用的是
Object.defineProperty()
的get
与set
完成的。
区别:
vue2 中
vm
实例对象 对vm._data
使用数据代理,可以简化或者说更方便的操作 data 中的数据
vue3 中
RefImpl
引用对象的 value 对其原型对象
上的 value 进行数据代理
vue3 中为什么要弄一个 value 属性,向 vue2 一样直接点不好吗?
答案
Vue 3 的响应式系统是基于 ES6 的 Proxy 对象
实现的,而 Vue 2 的响应式系统是使用 Object.defineProperty
实现的。
所以,Vue 3 引入 value 属性是为了适应新的响应式系统
# 2、reactive()
上接前面 ref()使用对象数据类型存在的疑惑:
答案
<div id="app">
{{ user1 }} <br />
{{ user2 }} <br />
<button @click="changeValue">修改内容</button>
</div>
<script>
const { createApp, ref, reactive, onMounted } = Vue
createApp({
setup() {
// 1、使用ref
let user1 = ref({
name: 'lencamo',
age: 20
})
// 2、使用reactive
let user2 = reactive({
name: 'lencamo',
age: 20
})
onMounted(() => {
// console.log(user1);
console.log(user1.value) // Proxy(Object)
console.log(user2) // Proxy(Object)
})
function changeValue() {
// 区别 🌈
user1.value.name = 'ref'
user2.name = 'reactive'
}
return {
user1,
user2,
changeValue
}
}
}).mount('#app')
</script>
reactive() 返回的是一个原始对象的 Proxy。相较于 ref(),它虽然不用加上.value,但也有一定的局限性:
# Proxy的实例对象:Proxy(Object)
# 原始对象:Object
let Proxy(Object) = reactive(Object)
注意
ref()
是 vue 为我们提供的允许我们创建可以使用任何值类型的响应式 ref(当然,涉及对象类型是其内部会直接调用 reactive())
reactive()
仅对 🚩 对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
代码演示
<div id="app">
{{ name }} <br />
{{ person }} <br />
<button @click="changeValue">修改内容</button>
</div>
<script>
const { createApp, ref, reactive, onMounted } = Vue
createApp({
setup() {
// 1、缺点
// 警告:value cannnot be made reactive: lencamo
let name = reactive('lencamo')
// 2、优点
let person = reactive({
// 数组
hobby: ['喝酒', '吉他', '游泳'],
// 对象(多级)
job: {
type: '前端工程师',
local: {
hot: true
}
}
})
function changeValue() {
// 修改:👀简洁明了
person.hobby[0] = '篮球'
person.job.local.hot = false
}
return {
name,
person,
changeValue
}
}
}).mount('#app')
</script>
# 3、对比总结 ✨
① 从定义数据角度对比:
- ref() 用来定义:基本类型数据。
- reactive() 用来定义:对象类型数据。
备注:ref 也可以用来定义对象(或数组)类型数据, 它内部会自动通过
reactive
转为代理对象。
② 从原理角度对比:
- ref() 通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 - reactive() 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据。
③ 从使用角度对比:
- ref 定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive 定义的数据:操作数据与读取数据:均不需要
.value
。
# 三、常用函数
# 1、ref 属性
ref 用于注册元素或子组件的引用。
在 setup 中是不可以使用 this
<template>
<h2 ref="titleRef">我是标题</h2>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const titleRef = ref()
// 对titleRef的操作
onMounted(() => {
console.log(titleRef.value)
})
return {
titleRef
}
},
mounted() {
console.log(this.$refs.titleRef)
}
}
</script>
# 2、computed()
computed(cb())
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。
说明
computed()也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象
computed:可写的 ref 对象
<template>
姓:<input type="text" v-model="person.fristName" /> <br />
名:<input type="text" v-model="person.lastName" /> <br />
姓名(选项式):<input type="text" v-model="fullName1" /> <br />
姓名(组合式):<input type="text" v-model="fullName2" /> <br />
</template>
<script>
import { reactive, computed } from 'vue'
export default {
name: 'Demo',
setup() {
let person = reactive({
fristName: '张',
lastName: '三丰'
})
// 组合式
// const fullName2 = computed(() => {
// return person.fristName + '-' + person.lastName
// })
const fullName2 = computed({
get() {
return person.fristName + '-' + person.lastName
},
set(newValue) {
let nameArr = newValue.split('-')
person.fristName = nameArr[0]
person.lastName = nameArr[1]
}
})
return { person, fullName2 }
},
// 选项式
computed: {
// fullName1: function () {
// return this.person.fristName + '-' + this.person.lastName
// }
fullName1: {
get: function () {
return this.person.fristName + '-' + this.person.lastName
},
set: function (newValue) {
let nameArr = newValue.split('-')
this.person.fristName = nameArr[0]
this.person.lastName = nameArr[1]
}
}
}
}
</script>
<script>
export default {
name: 'Demo',
setup() {
const msg = ref('')
// 组合式
const reverseMsg2 = computed(() => {
return msg.value.split('').reverse().join('')
})
return { msg, reverseMsg2 }
},
// 选项式
computed: {
reverseMsg1: function () {
// 注意使用this
return this.msg.split('').reverse().join('')
}
}
}
</script>
# 3、watch()
watch(arr,cb(),options)
在 vue3 中,侦听器可以监听一个或多个响应式的数据,并且可以通过一些配置选项来自定义侦听器的行为。
computed() 和 watch() 使用对比
<template>
<input type="text" v-model="msg" />
<p>选项式:{{ reverseMsg1 }}</p>
<p>组合式:{{ reverseMsg2 }}</p>
</template>
<script>
import { ref, computed, watch } from 'vue'
export default {
name: 'Demo',
setup() {
let msg = ref('')
// 二、组合式
// 1、计算属性
// const reverseMsg2 = computed(() => {
// return msg.value.split('').reverse().join('')
// })
// 2、监听器
let reverseMsg2 = ref('')
watch(msg, (newValue) => {
reverseMsg2.value = newValue.split('').reverse().join('')
})
return { msg, reverseMsg2 }
},
// 一、选项式
// 1、计算属性
// computed: {
// reverseMsg1: function () {
// // 注意使用this
// return this.msg.split('').reverse().join('')
// }
// }
// 2、监听器
data() {
return {
// 使用watch监听做数据计算的问题😒
reverseMsg1: ''
}
},
watch: {
msg(newValue, oldValue) {
this.reverseMsg1 = newValue.split('').reverse().join('')
}
}
}
</script>
下面,我们以监听普通数据为例:
<template>
<button @click="sum += 1">改变sum</button>
<button @click="msg += '-'">改变msg</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
setup() {
let sum = ref(0)
let msg = ref('lencamo')
// 监听一个(不用加value)
watch(sum, (newValue, oldValue) => {
console.log(newValue)
})
// 监听多个(不用加value)
watch([sum, msg], (newValue, oldValue) => {
console.log(newValue) // 一个数组
})
return { sum, msg }
}
}
</script>
监听对象数据 ✨
<template>
<button @click="job.type = '外卖小哥'">修改job</button>
<button @click="job.rank = 2">深度监听检测</button>
</template>
<script>
export default {
setup() {
let job = reactive({
type: '前端工程师',
local: {
rank: 2
}
})
// 1、默认隐式地创建一个深层侦听器
watch(job, (newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
// 2、显示的强制创建一个深层侦听器
watch(
() => job.local,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* job.local 被整个替换了
},
{ deep: true }
)
// 3、默认返回Proxy对象,手动获取普通对象🤔
watch(
() => ({ ...job }),
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
},
{ immediate: true }
)
return { job }
}
}
</script>
是否加 value 问题
产生是否加 value 的问题根源是:我们对对象做响应式时采用了 ref,而不是 reactive
<template>
<button @click="job.type += '-'">改变type</button>
<button @click="job.local.rank += 1">改变rank</button>
</template>
<script>
import { watch, ref } from 'vue'
export default {
setup() {
let job = ref({
type: '前端工程师',
local: {
rank: 2
}
})
// 要加上value 🤔
watch(job.value, (newValue, oldValue) => {
console.log('one--', newValue === oldValue) // true
})
// 等效于
// watch(
// job,
// (newValue, oldValue) => {
// console.log('one--', newValue === oldValue) // true
// },
// { deep: true }
// )
return { job }
}
}
</script>
# 4、watchEffect()
watchEffect(cb)
vue3 中的监听器除了,还引入了一种新的侦听器类型,叫做“侦听器副作用函数”(watchEffect),可以用于监听一个或多个响应式数据的变化,并在响应式数据变化时自动执行副作用函数。
watch 与 watchEffect 的区别
- watch 的套路是:既要指明监视的属性,也要指明监视的回调。
- watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
watchEffect 会自动执行(相当于加了一个 { immediate: true })
<script>
export default {
setup() {
// 立即执行当前函数
watchEffect(() => {
console.log('===========')
})
}
}
</script>
思考一下,我们为什么要使用 watchEffect 函数呢?是解决什么问题吗?
答案
当我们的监听源在回调中也使用了,为了简化代码我们可以使用 watchEffect()
<script>
export default {
setup() {
const todoId = ref(1)
const data = ref(null)
// watch 两次使用了 todoId,一次是作为源,另一次是在回调中
watch(
todoId,
async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
},
{ immediate: true }
)
// watchEffect() 会自动跟踪回调的响应式依赖(todoId、data)
watchEffect(async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
})
return { todoId, data }
}
}
</script>
<script>
export default {
setup() {
const name = 'lencamo'
const count = ref(1)
// 立即执行一个函数(相当于加了一个 { immediate: true })
watchEffect(() => {
console.log('===========')
})
// 1、响应式数据发生变化,自动执行
watchEffect(() => {
console.log(name.value)
})
// 2、关闭watchEffect监听器
const unwatch = watchEffect(() => {
console.log(count.value)
if (count.value > 6) {
unwatch() // 手动关闭
}
})
}
}
</script>
# 四、其他
# 1、readonly()
问题描述
在 vue2 我们已经提到了,如果使用 props 向子组件传递对象类型的数据时,子组件是可以修改该对象数据并直接影响到父组件,显然这是不符合 vue 的单向数据流的。
readonly()可以接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理
<template>
<son :info="roInfo"></son>
</template>
<script>
export default {
setup() {
const info = reactive({
name: 'lencamo',
age: 22,
height: 210
})
const roInfo = readonly(info)
return {
roInfo
}
}
}
</script>
# 2、toRefs()
在有些情况下,我们仅仅需要从已定义的 reactive 中取出某个数据,但伴随的可能是响应式的丢失。
单独取出某个数据,往往是处于书写简化的考虑
响应式丢失
reactive 被解构后会变成普通的值,失去响应式。
使用 toRefs()可以将解构后的属性都转换为 ref
<template>
<p>常规方式:{{ job.local.rank }}</p>
<p>简写方式:{{ local.rank }}</p>
</template>
<script>
export default {
setup() {
let job = reactive({
type: '前端工程师',
local: {
rank: 2
}
})
// let job2 = toRefs(job)
let { local } = toRefs(job) // 解构1
// return { job, ...job2 } // 解构2
return {
job,
local
}
}
}
</script>
当然,我们可以不用使用解构把所有的对象属性都变为 ref,如果我们仅仅只需要一个对象属性的话,我们可以使用 toRef()
<script>
export default {
setup() {
let local = toRef(job, 'local') // 只要job对象下的local属性
return {
local
}
}
}
</script>
# 3、浅层对象
- shallowRef()、shallowReactive()、shallowReadonly()
shallowRef() ✨
和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。
简单是说,就是不会求助 reactive,只处理基本数据类型的响应式(不处理对象的响应式)
----> 应用:高德地图 map 对象引入
如果直接使用 ref( ),Vue3 所使用的 Proxy 拦截操作会改变 JS API 原生对象。
所以应该改用 shallowRef( ),即:让 JS API 相关对象采用非响应式的普通对象来存储
import { shallowRef } from '@vue/reactivity'
const map = shallowRef(null)
// 初始化地图
const initMap = () => {
AMapLoader.load({
// ……
}).then((AMap) => {
map.value = new AMap.Map('container', {
// ……
})
})
}
----> 相关:triggerRef 手动触发响应式
强制触发依赖于一个浅层 ref 的副作用
const info = shallowRef({
greet: 'Hello, world'
})
// 1、自动触发
watchEffect(() => {
console.log(shallow.value.greet) // 可以访问,但不是响应式的
})
// 2、手动触发
const changeInfo = () => {
info.value.greet = 'My name is Lencamo!'
triggerRef(info)
}
shallowReactive()
和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
简单是说,只处理对象最外层属性的响应式(浅响应式)
----> 相关:toRaw() 返回 Proxy 的原始对象
const app = Vue.createApp({
methods: {
changeMessage() {
this.info = { name: 'lencamo', age: 22 }
}
},
watch: {
info(newValue, oldValue) {
// console.log({...newValue}) // 错误
console.log(Vue.toRaw(newValue))
}
}
})
# 4、工具函数
- isRef()、isReactive()、isReadonly()、isProxy()
判断响应式数据类型
- unref()
获取普通数据和 ref 数据的值
// const age = 18
// const age = ref(18)
const val = Ref(age) // 相当于:val = isRef(val) ? val.value : val
// const val = age
// const val = age.value
- toRaw()
返回 Proxy(reactive、readonly 相关对象) 的原始对象
const app = Vue.createApp({
methods: {
changeMessage() {
this.info = { name: 'lencamo', age: 22 }
}
},
watch: {
info(newValue, oldValue) {
// console.log({...newValue}) // 错误
console.log({ ...Vue.toRaw(newValue) })
}
}
})