Vue3 Composition API
2020 年 9 月,它终于来了,尽管很早就上了 Beta
版本,但是尤大确一直没有推出正式版,如今正式版上线,让我们来一睹为快,看看 Vue3
带来了哪些改变吧!
Vue3
文档地址:https://v3.cn.vuejs.org/
Composition API
setup
什么是
setup
?vue3
之前我们的代码逻辑会在组件的各个角落,大量碎片化的代码使得理解和维护组件变得异常困难,在处理单个逻辑关注点时,我们必须不断地跳转相关代码的选项块。试想一下如果我们能够将同一个逻辑的相关代码都配置在一起,是不是更容易理解和维护,那么我们将这些逻辑和代码写在哪里了?在vue3
中,我们将此位置称为setup
。
setup
执行时,组件实例尚未被创建 (beforeCreate
- created
),因此在 setup
中没有 this
。setup
选项应该是一个接收 props
和 content
的函数,我们这里先来看看 props
的用法,在 setup
函数中除了 props
之外,我们无法访问组件中声明的任何属性(本地状态、计算属性或方法)。直接上栗子吧:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<test username='张三' />
`
})
app.component('test', {
props: ['username'],
setup(props) {
console.log(props) // Proxy {username: "张三"}
// console.log(this) // window
},
template: `<div>{{username}}</div>`
})
const vm = app.mount('#root')
</script>
</html>
上述代码中,我们创建全局组件 test
, 父组件往 test
上传入 username
,test
中通过 props
接收父组件传递过来的 username
,此时在子组件的 setup
执行时可以接收到 props
中的值,但是打印出 this
是指向 window
的。
但是 setup
函数返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。我们还是在刚刚的 test
组件中来验证下这句话:
app.component('test', {
props: ['username'],
mounted() { // mounted 声明周期中可以使用 setup 中返回出来的 username
console.log(this.username) // 张三
},
setup(props) {
const username = props.username
return { username }
},
template: `<div>{{username}}</div>` // 张三
})
const vm = app.mount('#root')
当然我们也可以在组件的生命周期或者方法中直接访问 setup
函数里面的方法,栗子如下:
const app = Vue.createApp({
mounted() {
// 可以直接在生命周期里面调用 setup() 里面的方法
this.$options.setup().handleClick()
},
setup(props, context) {
return {
handleClick() {
alert('setup')
}
}
}})
总结:执行
setup()
是在created()
之前执行,由于尚未创建实例,所以setup()
中无法直接使用this
,但是methods/computed/watch
等其他地方都可以直接调用setup()
。并且setup()
返回的所有内容都将暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。
ref 语法
在 vue3
之前,我们只需要在 data
中定义变量,那么这个变量在 methods/computed/watch
等中的改变就会直接响应到组件模板中了。但是在 vue3 之中我们只有通过 ref
函数和 reactive
函数来实现响应式变量在组件任何地方起作用。我们先通过小栗子可以了解 ref
函数的基本用法:
const app = Vue.createApp({
setup() {
// 通过 ref 初始化一个响应式变量 count 并将其初始值设为 0
const { ref } = Vue
let count = ref(0)
const increase = () => {
return count.value += 1
}
return { count, increase }
},
template: `
<div>{{count}}</div>
<button @click="increase">增加</button>
`
})
通过上述代码我们可以看到 ref
接受参数,并将其包裹在一个带有 value
property 的对象中返回,然后可以使用该 property
访问或更改响应式变量的值。那么问题来了为什么要将值封装在一个对象中呢?这是因为在 JavaScript
中,Number
或 String
等基本类型是通过值传递的,而不是通过引用传递的。我们知道值传递一般指在调用函数时将实际参数复制一份传递到函数中,这样如果在函数中如果对参数进行修改,不会影响到实际参数,既占用内存空间也无法实现数据的响应式。而通过引用传递其实是直接把内存地址传过去,也就是说在引用传值的过程中操作的都是源数据,也就能更好的实现数据响应式驱动。
其实简单点就是基础类型的值都是存在栈中,引用类型的值都是存在堆中,堆共用一个数据源,而每个栈都是一个独立的空间,所以我们将数据存入堆中才能更好的做到数据响应式改变。
reactive 语法
reactive
语法和 ref
都是支持数据的响应式,但是 reactive
接收的一般是数组和对象,直接来看小栗子:
const app = Vue.createApp({
setup() {
// 通过 reactive 初始化一个响应式对象
const { reactive, computed } = Vue
const data = reactive({
count: 0,
increase: () => data.count++,
docuble: computed(() => data.count * 2)
})
return { data }
},
template: `
<div>{{data.count}}</div>
<div>{{data.docuble}}</div>
<button @click="data.increase">增加</button>
`
})
上述代码中我们会发现模板中每次都要写 data.count
、data.docuble
过于繁琐,如果我们在 return { data }
时使用展开运算符或者对象赋值的方式导出,是否可以直接使用 count
、 docuble
等变量呢。
// 使用展开运算符
return {...data}
// 使用对象赋值
return {
count: data.count,
increase: data.increase,
docuble: data.docuble
}
我们发现取出来之后,count
、increase
、docuble
都丧失了响应式的活性,他们变成了不同的 javascript
类型。那么我们如何解决这种情况呢?vue3
为我们推出了 toRefs
函数。
toRefs 语法
我们先试用 toRefs
改写上面的代码:
const app = Vue.createApp({
setup() {
// 通过 reactive 初始化一个响应式对象
const { reactive, computed, toRefs } = Vue
const data = reactive({
count: 0,
increase: () => data.count++,
docuble: computed(() => data.count * 2)
})
// toRefs 将 data 转变为一个响应式对象
const { count, increase, docuble } = toRefs(data)
return { count, increase, docuble }
},
template: `
<div>{{count}}</div>
<div>{{docuble}}</div>
<button @click="increase">增加</button>
`
})
toRefs
函数接收一个 reactive
对象作为参数,返回一个普通的对象,但是这个普通对象的每一项都变成了 ref
类型的对象,即支持响应式。
1、
ref / reactive
通过proxy
对数据进行封装,当数据变化时,触发模板等内容的更新。
2、ref
处理基础类型的数据。例如ref('cc')
变成proxy({value: 'chen'})
这样一个响应式的引用。
3、reactive
处理非基础类型的数据。例如reactive({name: 'cc'})
变成proxy({name: 'cc'})
这样一个响应式引用。
4、toRefs
接收一个reactive
对象作为参数,返回一个普通对象,并将普通对象的每一项都变成了ref
类型的对象,保持器响应式活力。例如:toRefs proxy({name: 'cc'})
变成{name: proxy({value: 'cc'})}
。
readonly 语法
获取一个对象 (响应式或纯对象) 或 ref 并返回原始 proxy 的只读 proxy。举个栗子:
const app = Vue.createApp({
setup(props, context) {
const { reactive, readonly, toRefs } = Vue
let nameObj = reactive([123])
// 获取 nameObj 这个响应对象为参数
let copyNameObj = readonly(nameObj)
setTimeout(() => {
nameObj[0] = 456
// 因为使用了 readonly 无法改变 copyNameObj 的值
copyNameObj[0] = 456
}, 2000)
return { nameObj, copyNameObj }
}
})
toRef 语法
和 toRefs
只差了一个 s
,那么它有什么用呢?让我们先来假设一个场景,如下栗子:
const app = Vue.createApp({
template: `
<div>{{age}}</div>
`,
setup(props, context) {
const { reactive, toRefs, toRef } = Vue
const data = reactive({ name: 'zhangsan' })
const { age } = toRefs(data)
console.log(age) // undefined
setTimeout(() => {
age.value = 20 // 报错 Cannot set property 'value' of undefined
}, 2000)
return { age }
}
}).mount('#root')
我们定义了一个响应式变量 data
,toRefs
会将相应对象解析成一个普通对象,并保证该普通对象的属性都是 ref
响应式属性,我们又想在新增加一个 age 的响应式变量,结果只得到了一个 undefined
类型而不是预料中的 ref
类型。此时我们可以使用 toRef
改造上面的代码:
const app = Vue.createApp({
template: `
<div>{{age}}</div>
`,
setup(props, context) {
const { reactive, toRefs, toRef } = Vue
const data = reactive({ name: 'zhangsan' })
const age = toRef(data, 'age')
setTimeout(() => {
age.value = 20
}, 2000)
return { age }
}
}).mount('#root')
toRef
可以用来为源响应式对象上的某个 property
新创建一个 ref
,这里我们就新建了一个 ref
类型的 age
变量,然后 age
就可以完美继承响应活性了。
setup 参数 context
前面我们介绍了 setup
函数中的第一个参数 props
,这里我们再来看看它的第二个参数 context
的用法,根据官网文档传递给 setup
函数的第二个参数是 context
。context
是一个普通的 JavaScript
对象,它暴露三个组件的 property
:
export default {
setup(props, context) {
const { attrs, slots, emit } = context
}
}
context
是一个普通的 JavaScript
对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6
解构。
export default {
setup(props, { attrs, slots, emit }) {}
}
那首先我们来看一下 attrs
的使用:
const app = Vue.createApp({
template: `
<div>
<child username="zhangsan"></child>
</div>
`
})
app.component('child', {
template: `
<div></div>
`,
setup(props, context) {
const { attrs, slots, emit } = context
// attrs 接收一个 None-Props 属性
console.log(attrs.username)
}
})
const vm = app.mount('#root')
什么是 None-Props 属性
呢?正常情况下,父组件给子组件传递了 username
,子组件应该通过 props: ['username']
去接收,但是如果子组件如果不使用 props
接收,那么就可以在 attrs
处接收到 username
。而这个 username
就是 None-Props 属性
。
slots
看名字应该就可以大致猜到,这是用来接收插槽的,同样是上面的栗子,我们改写一下:
const app = Vue.createApp({
template: `
<div>
<child>parent</child>
</div>
`
})
app.component('child', {
setup(props, context) {
const { h } = Vue
const { attrs, slots, emit } = context
return () => h('div', {}, slots.default())
}
})
const vm = app.mount('#root')
而如果不在 setup
函数中,而是在组件外部的生命周期函数中,我们只能通过 this
去获取 slots
,现在我们可以完全脱离 vue2
的写法,将所有的东西都聚合到 setup
函数中。
mounted() {
console.log(this.$slots.default())
},
emit
看名字其实应该也可以猜出来,子组件像父组件的事件传递,直接看栗子:
const app = Vue.createApp({
template: `
<div>
<child @change="change"></child>
</div>
`,
setup() {
const change = () => {
alert('change')
}
return { change }
}
})
app.component('child', {
template: `
<div @click="handleClick">child</div>
`,
setup(props, context) {
const { attrs, slots, emit } = context
const handleClick = () => {
// 使用 emit 直接传递给父组件
emit('change')
}
return { handleClick }
}
})
const vm = app.mount('#root')
watch 语法
熟悉 vue2
的小伙伴对 watch
肯定不陌生,在 vue3
中,我们可以把 watch
集成到 setup
函数中进行使用。watch
需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生更改时被调用。
使用 watch
监听单个数据源
const app = Vue.createApp({
setup() {
const { ref, reactive, watch } = Vue
let title = ref("")
const editTitle = () => {
title.value = 'hello world'
}
watch(title, (newValue, oldValue) => {
console.log('old', oldValue) // 'old', ""
console.log('new', newValue) // 'new', "hello world"
})
return { title, editTitle }
},
template: `
<div>ref 值监听:{{title}}</div>
<button @click="editTitle">改变title</button>
`
}).mount('#root')
使用 watch
监听多个数据源
const app = Vue.createApp({
setup() {
const { ref, reactive, watch } = Vue
let title = ref("")
const data = reactive({
count: 0
})
const editTitle = () => {
title.value = 'hello world'
}
const editCount = () => {
data.count++
}
// watch 可以监听一个数组,里面是需要监听的多个值
watch([title, data], (newValue, oldValue) => {
console.log('old', oldValue) // ["", Proxy]
console.log('new', newValue) // ["hello world", Proxy]
})
return { title, editTitle, data, editCount }
},
template: `
<div>reactive 值监听:{{data.count}}</div>
<div>ref 值监听:{{title}}</div>
<button @click="editTitle">改变title</button>
<button @click="editCount">改变count</button>
`
}).mount('#root')
上面的栗子中,我们使用监听了 reactive
新增监听了一个 data
对象,但是如果我们只想监听 data
对象中的 count
属性,而不是监听整个 data
对象,那么我们应该如何实现呢?
// watch 中接收一个函数,其返回值为 reactive 函数对象中想监听的值
watch([title, () => data.count], (newValue, oldValue) => {
console.log('old', oldValue) // ["", 0]
console.log('new', newValue) // ["hello world", 1]
})
watchEffect 语法
watchEffect
和 watch
都是用来对数据的侦听,但是他们有 3 个比较大的差别:
1、
watchEffect
立即执行,没有惰性。
2、不需要传递你要侦听的内容,自动会感知代码依赖,不需要传递很多参数,只需要传递一个回调函数。
3、不能获取之前数据的值 。
举个栗子:
const app = Vue.createApp({
template: `
<input v-model="nameObj.name" />
`,
setup() {
const { reactive, watchEffect } = Vue
const nameObj = reactive({ name: 'cc' })
watchEffect(() => {
// 初次加载就会执行打印出 `abc`
// 但是自动检测到 `abc` 和页面没有任何关系后面就不会在执行
console.log('abc')
// nameObj.name 绑定了页面的 input,所以每次输入改变 name 的值都会执行一次
console.log(nameObj.name)
})
return { nameObj }
}
}).mount('#root')
如果我们要在 5 秒后停止 watch
或者 watchEffect
对属性的监听应该如何做呢,如下栗子:
const app = Vue.createApp({
template: `
<input v-model="nameObj.name" />
`,
setup() {
const { reactive, watchEffect } = Vue
const nameObj = reactive({ name: 'cc' })
// 将函数变成一个命名函数,然后 5s 后在执行一次这个命名函数(watch 同理)
const stop = watchEffect(() => {
console.log(nameObj.name)
setTimeout(() => {
stop()
}, 5000)
})
return { nameObj }
}
}).mount('#root')
老瓶新酒 - 生命周期
vue3
组件中的生命周期和 vue2
组件生命周期基本相同,唯一不同的是将 beforeDestroy
和 destroyed
改成了 beforeUnmount
和 unmounted
,根据尤大的说法是后者语义性更强,更能表达组件卸载的说法。通过代码简单回顾下:
// 生命周期函数:在某一时刻会自动执行的函数
const app = Vue.createApp({
data() {
return {
message: 'see you'
}
},
template: `
<div @click="handleClickItem">{{message}}</div>
`,
methods: {
handleClickItem() {
this.message = 'bye bye'
setTimeout(() => {
app.unmount()
}, 1000)
}
},
// 在实例生成之前会自动执行的函数
beforeCreate() {
console.log('beforeCreated', this.message)
},
// 在实例生成之后会自动执行的函数
created() {
console.log('created', this.message)
},
// 在组件内容被渲染到页面之前自动执行的函数
beforeMount() {
console.log('beforeMounte', document.getElementById('root').innerHTML)
},
// 在组件内容被渲染到页面之后自动执行的函数
mounted() {
console.log('mounted', document.getElementById('root').innerHTML)
},
// 在组件内容被修改之前自动执行的函数
beforeUpdate() {
console.log('beforeUpdate', document.getElementById('root').innerHTML)
},
// 在组件内容被修改之后自动执行的函数
updated() {
console.log('updated', document.getElementById('root').innerHTML)
},
// 在 Vue 应用失效时,自动执行的函数 可以通过 app.unmount() 触发
beforeUnmount() {
console.log('beforeUnmount', document.getElementById('root').innerHTML)
},
// 当 Vue 应用失效且 Dom 完全销毁之后,自动执行的函数
unmounted() {
console.log('unmount', document.getElementById('root'))
}
})
const vm = app.mount('#root')
setup 函数中的调用生命周期钩子
- beforeCreate -> 不需要
- created -> 不需要
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeUnmount -> onBeforeUnmount
- unmounted -> onUnmounted
- errorCaptured -> onErrorCaptured
- renderTracked -> onRenderTracked
- renderTriggered -> onRenderTriggered
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写。
vue3 模块化开发
前面我们所有的逻辑代码都写进了 setup
函数中,虽然所有的代码都写在 setup
函数中是可以将逻辑都聚合到一起,但是如果一个页面所有的逻辑代码都写入一个函数,只会让代码显得更加难以维护。也许你写完当前也页面的所有逻辑代码有 1000 行,但是三天之后你在想维护这段代码,估计你自己也会懵逼,不知从何写起。所以日常开发中我们更应该将 setup
当做一个流程处理函数,将该页面的所有逻辑抽离成一个一个模块,将每个模块的结果导出到 setup
函数中供页面模板使用。我们先来看一个简单的栗子:
我们这里实现一个简单的功能,就是记录每次鼠标点击屏幕时打印出鼠标当前的坐标值!
// 将鼠标坐标值更新逻辑抽离成一个单独的函数
const { reactive, toRefs, onMounted, onUnmounted } = Vue
const updateMouseEffect = () => {
const mouseObj = reactive({
x: 0,
y: 0
})
const updateMouse = (e) => {
mouseObj.x = e.pageX
mouseObj.y = e.pageY
}
onMounted(() => {
document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
document.removeEventListener('click', updateMouse)
})
const { x, y } = toRefs(mouseObj)
return { x, y }
}
const app = Vue.createApp({
// 页面上的每一个逻辑都抽离成对应的函数
// setup 函数只负责将封装的逻辑中导出的值引入,不涉及具体逻辑编写
setup() {
const { x, y } = updateMouseEffect()
return { x, y }
},
template: `
<h1>x: {{x}} y: {{y}}</h1>
`
}).mount('#root')
看了上面的代码,是不是觉得 vue3
可以完美实现各种 hooks
,接下来我们结合 axios 来实现一个涉及外部请求的封装函数。这里都是用的 cdn
,如需复现栗子,记得头部引入 axios
的 cdn
链接。
const { ref } = Vue
const useURLLoader = (url) => {
const result = ref(null) // 响应结果
const loading = ref(true) // 是否显示loading
const loaded = ref(false) // 是否加载完成
const error = ref(null) // 是否响应错误
axios.get(url).then((rawData) => {
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch((e) => {
error.value = e
})
return { result, loading, loaded, error }
}
const url = 'https://dog.ceo/api/breeds/image/random'
const app = Vue.createApp({
setup() {
const { result, loading, loaded, error } = useURLLoader(url)
return { result, loading, loaded, error }
},
template: `
<h1 v-if="loading">Loading!...</h1>
<img v-if="loaded" :src="result.message" >
`
}).mount('#root')
这个栗子其实就是我们在发请求时希望页面显示 loading
,拿到请求结果之后将 loading
状态取消然后展示我们拿到的结果数据,代码很简单,也是将所有的逻辑代码都抽离到一个单独的函数中,setup
函数只负责流程控制和数据导出。
常用的一些都整理出来了,本来想继续整理一些 vue3
中的新特性,但是篇幅太长了,就准备在下一篇中继续整理了,如有错误或不正确的地方欢迎指正,每天进步一点点,加油!!!