前言
最近公司正式将Vue3.0作为前端框架,自己也参与了2个Vue3.0项目的开发,所以准备出一个Vue3.0的学习系列,提到Vue3.0,就不得不说composition API
,所以本文就作为Vue3.0系列的开篇之作。
解决什么痛点?
使用过Vue2.0通常知道,用(data、computed、methods、watch
)组件选项来组织逻辑很有效,然而,随着组件变大,逻辑复制时,导致组件难以阅读合理解和后续维护性非常复杂,特别是接手别人代码时,往往需要来回跳转阅读。基于此composition api
应用而生。对比图如下:
setup
新的setup
选项在组件创建之前执行,一旦props
被解析,就将作为Composition API
的入口。
setup中避免使用this,因为无法获取组件实例;同理,setup的调用发生在data、property、computed property、methods被解析之前,同样在setup中无法获取。
setup接收两个参数:
props
(props是响应式的,不能使用ES6解构,如需解构使用toRefs
或toRef
)context
(普通对象,可解构,包含3个属性。context.attrs、context.slots、context.emit
)
import { toRefs,roRef } from 'vue'
props: {
name: String,
age: Number
},
setup(props,context){
console.log(props.name)
//toRefs 不会创建一个 ref ,而toRef 可以:
const {name}=toRefs(props)
const {age}=toRef(props,'age')
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
结论
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
ref 与 reactive
在vue3中,官方提供了ref()
与reactive()
两种方式声明响应式数据,接下来我们将详细介绍各自的用法以及两者的区别。
ref函数
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
import { ref } from 'vue'
const age = ref(0)
console.log(age.value) // 0
age.value++
console.log(age.value) // 1
而为何将值封装到一个对象里面?
官方给出了分析和解释:
在 JavaScript 中,
Number
或String
等基本类型是通过值而非引用传递的。封装成对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。
结论
ref
为我们的值创建了一个响应式引用
reactive函数
返回对象的响应式副本,响应式转换是“深层”的——它影响所有嵌套 property。
import { reactive } from 'vue'
const obj=reactive({
name:'Vue3',
age:1
})
reactive
将解包所有深层的 refs,同时维持 ref 的响应性。
const age = ref(1)
const obj = reactive({ age })
// ref 会被解包
console.log(obj.age === age.value) // true
// 它会更新obj.age
age.value++
console.log(age.value) // 2
console.log(obj.age) // 2
// 它也会更新 age ref
obj.age++
console.log(obj.age) // 3
console.log(age.value) // 3
当将 ref 分配给 reactive
property 时,ref 将被自动解包。
const age = ref(1)
const obj = reactive({})
obj.age = age
console.log(obj.age) // 1
console.log(obj.age === age.value) // true
区别
reactive
和 ref
都是用来定义响应式数据的 reactive
更推荐去定义复杂的数据类型, ref
更推荐定义基本类型
ref
和 reactive
本质我们可以简单的理解为ref
是对reactive
的二次包装, ref
定义的数据访问的时候要多一个.value
使用ref
定义基本数据类型,ref
也可以定义数组和对象。
reactive 不能代理基本类型,例如string、number、boolean
等
toRef与toRefs
前面我们提到Props不能使用使用ES6解构,同样,响应式对象也不能使用ES6解构,但是我们想要解构但有不要失去响应怎么办?这个时候toRef
或toRefs
就来了。
toRef
可以用来为源响应式对象上的某个 property 新创建一个 ref
。
toRefs
用于将一个 reactive
对象转化为属性全部为 ref
对象的普通对象。
import { defineComponent, reactive, toRefs, toRef } from "vue";
export default defineComponent({
setup () {
const person = reactive({
name: 'icey',
age: 18,
height: 160
})
const ageRef = toRef(person, 'age')
const { age } = toRefs(person)
ageRef.value++
console.log(person.age) // 19
console.log(age.value) //19
person.age++
console.log(ageRef.value) // 20
console.log(age.value) //20
}
})
区别
toRef
会为源对象不存在的 property生成ref
。toRefs
只会为源对象中包含的 property 生成ref
。 当你要将 prop 的 ref 传递给复合函数时,toRef
很有用: 即使源 property 不存在,toRef
也会返回一个可用的ref
。这使得它在使用可选 prop 时特别有用。
computed 计算属性
- 接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式
ref
对象。
import { defineComponent, computed, ref } from "vue";
export default defineComponent({
setup () {
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++
console.log(plusOne.value) // 2
}
})
- 使用具有
get
和set
函数的对象来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watch 与watchEffect
在官方文档中关于watch与watchEffect讲解中,扑面而来的是一个感觉相当重要,可是有不太理解的词---副作用(side effect)。为了更好的吸收这块知识,首先我们解释下什么是函数副作用?
副作用(side effect)
对于所有初学者,Vue2到Vue3最直观的变化就是Composition API-几乎所有的Vue2 options方法都被放到了setup函数里。这个较大的风格转变通俗的讲,就是就是从基于对象的编程(OOP)转向了函数式编程(FP)。
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。借用网上一张图,图片来源
vue3中响应式数据的变更造成的其他连锁反应,以及后续逻辑,这些连锁反应都叫副作用。副作用不一定是不被需要的。它可以是获取数据、事件监听或订阅、改变应用状态、修改 DOM、输出日志等等。
watchEffect
watchEffect
根据响应式状态自动应用和重新应用副作用。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watchEffect(effect, [options])
参数说明:
-
effect:侦听副作用传入的函数,该函数可以接收一个
onInvalidate
函数作入 参,用以清除副作用。 -
options:包含3个属性的对象,3个属性均为可选项
-
flush
:pre
|post
|sync
。用来改变副作用的刷新时机。 -
onTrack
: 将在响应式 property 或ref
作为依赖项被追踪时被调用。 -
onTrigger
: 将在依赖项变更导致副作用被触发时被调用。onTrack
和onTrigger
只能在开发模式下工作。
侦听与停止侦听
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
setup () {
const count = ref(0)
const stop = watchEffect(() => { //页面进入时执行一次watchEffect
console.log(count.value)
})
// -> logs 0
setTimeout(() => {
count.value++ //依赖变化时,执行watchEffect
// -> logs 1
}, 1000)
setTimeout(() => {
stop() //停止侦听
count.value++ //不在执行watchEffect
// 控制台无logs输出
}, 1000)
}
})
清除副作用(onInvalidate )
它是effect
函数中传入的参数,用于清除effect
产生的副作用,onInvalidate
只作用于异步函数,并且只有在如下两种情况下才会被调用:
- 副作用即将重新执行时(页面进入不执行)
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时) 换言之,onInvalidate(fn)
传入的回调会在watchEffect
重新运行或者watchEffect
停止的时候执行。
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
setup () {
const count = ref(0)
const stop = watchEffect((onInvalidate) => { // 首次进入执行副作用,不执行onInvalidate回调
console.log('watchEffect', count.value)
onInvalidate(() => {
console.log('onInvalidate', count.value)
})
})
//-->logs: watchEffect 0
setTimeout(() => {
count.value++ // 执行onInvalidate回调,执行副作用
}, 1000)
//-->logs: onInvalidate 1
// watchEffect 1
setTimeout(() => {
stop() // 执行onInvalidate回调,不执行副作用
//-->logs: onInvalidate 1
}, 1000)
}
})
控制台输出
上述例子只是为了验证onInvalidate执行时机,真实的使用需求是: 有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。
副作用刷新时机
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。
如果有个watchEffect监听了2个变量count和count1,你觉得监听器会调用2次?当然不会,Vue会合并成1次去执行,代码如下,console.log只会执行一次:
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
setup () {
const count = ref(0)
const count1 = ref(1)
watchEffect(() => {
console.log('watchEffect', count.value, count1.value)
})
}
})
count
会在初始运行时同步打印出来- 更改
count
时,将在组件更新前执行副作用。
如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有 flush
选项的附加 options
对象 (默认为 'pre'
):
-
flush
:pre
在组件更新前执行副作用。 -
flush
:post
,在组件更新后触发,这样你就可以访问更新的 DOM。 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。 -
flush
:sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
Watch
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
侦听单个数据源
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
侦听多个数据源
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次。注意多个同步更改只会触发一次侦听器。
setup() {
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// 打印 ["John", "Smith"] ["", ""]
}
return { changeValues }
}
通过更改设置 flush: 'sync'
,我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行
const changeValues = async () => {
firstName.value = 'John' // 打印 ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}