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 中的新特性,但是篇幅太长了,就准备在下一篇中继续整理了,如有错误或不正确的地方欢迎指正,每天进步一点点,加油!!!
