Composition API 示例
# 安装 vue3
npm i vue@3.0.0
示例内容:实时展示鼠标坐标
createApp
createApp 用于创建 Vue 对象,它可以接收一个(组件)选项对象作为参数,和 Vue 2 中给 Vue 构造函数传入的选项一样。
选项中的 data 应该是一个函数,不同于 Vue 2。这样统一了 data 的写法
<div id="app">
x: {{ position.x}}<br />
y: {{ position.y}}
</div>
<script type="module">
import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
// Vue 根实例的 data 也必须是一个函数
data() {
return {
position: {
x: 0,
y: 0
}
}
}
})
console.log(app)
app.mount('#app')
</script>
Vue 实例成员
Vue 3 创建的实例对象的成员比 Vue 2 中的少很多,并且没有用 $
开头,说明未来基本不用给这个对象上新增成员。
component
、directive
、mixin
、use
和 Vue 2 用法一致mount
和 Vue 2 的$mount
一样unmount
类似 Vue 2 的$destroy
方法
setup
Composition API 还是在选项中书写,不过要用到一个新的选项 setup
。
setup
函数是 Composition API 的入口。
接收两个参数:
props
接收外部传入的参数,且 props 是响应式对象,不能被解构context
一个对象:{ attrs, emit, slots }
setup
返回一个对象,这个对象的属性可以在模板、methods、computed 以及 生命周期的钩子函数中直接使用。
setup
执行的时机是在 props
被解析完毕,组件实例被创建之前执行的。
所以在 setup
内部无法通过 this
获取组件的实例。
因为组件实例还未被创建,所以在 setup
中也无法访问到组件中的 data
、computed
、methods
。
setup
内部的 this 此时指向 undefined
<div id="app">
x: {{ position.x}}
<br />
y: {{ position.y}}
</div>
<script type="module">
import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
const position = {
x: 0,
y: 0
}
return {
position
}
},
mounted() {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
响应式转化
上面示例在 mounted
中设置了 position.x
的值,但并没有渲染到页面,说明 position
不是响应式的。
过去可以在 data 选项中设置响应式对象。
为了让某一个逻辑的所有代码都能够被封装到一个函数中,Vue 3 中提供了一个新的 API(reactive
),用于创建响应式对象。
它就像过去使用的 Vue.observeable
, Vue 3 没有使用 observeable
作为函数的名称,是因为避免和另一个函数库 RxJS observables 重名,出现混淆。(官方文档有讲)
reactive
函数的作用是把一个对象转换成响应式对象,并且该对象的嵌套属性,也都会转换成响应式对象。
它返回的是一个 Proxy 对象。
<div id="app">
x: {{ position.x}}
<br />
y: {{ position.y}}
</div>
<script type="module">
import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
const position = reactive({
x: 0,
y: 0
})
return {
position
}
},
mounted() {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
小结
目前了解了 Vue 3 中的 3个新增 API。
createApp
用于创建 Vue 实例对象setup
Composition API 的入口reactive
用于创建响应式对象
生命周期钩子函数
在 setup
中使用生命周期钩子函数,注册鼠标移动事件 mousemove
。
当组件被卸载的时候,移除这个鼠标移动事件。
在 setup
函数中使用生命周期钩子函数,需要在函数名前添加 on
前缀,并且首字母大写。
Options API 和 Composition API 之间的生命周期映射
setup
是在组件初始化之前(beforeCreate
和created
之间)执行的,所以beforeCreate
和created
中的代码,都应该放在setup
函数中。unmounted
钩子函数类似以前的destroy
renderTracked
和renderTriggered
都是在render
函数被重新调用的时候触发的- 不同的是
renderTracked
在首次调用render
的时候也会触发,``renderTriggered不会。
- 不同的是
Options Api | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
在 setup
中使用的生命周期钩子函数,接收一个要注册的处理函数,当生命周期被触发的时候,调用这个函数。
<div id="app">
x: {{ position.x}}
<br />
y: {{ position.y}}
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
const position = reactive({
x: 0,
y: 0
})
// 定义事件处理函数
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return {
position
}
}
})
console.log(app)
app.mount('#app')
</script>
重构代码
Composition API 的优点就是把同一个功能的逻辑提取到一个地方,使其容易阅读并可以重用。
当前示例的最终目标,是要把 “获取鼠标位置” 这个功能封装到一个函数中。
<div id="app">
x: {{ position.x}}
<br />
y: {{ position.y}}
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
function useMousePosition () {
const position = reactive({
x: 0,
y: 0
})
// 定义事件处理函数
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return position
}
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
const position = useMousePosition()
return {
position
}
}
})
console.log(app)
app.mount('#app')
</script>
现在如果这个函数出现问题,可以很好的定位,并且可以放在一个模块中,将来在任何组件中都可以使用。
reactive/toRefs/ref
reactive / toRefs / ref 3个函数都是用于创建响应式数据的。
- reactive 用于把对象转化成响应式对象,是一个代理对象
- ref 用于把基本类型的数据转化成响应式对象
- toRefs 用于把代理对象中的所有属性都转化成响应式对象
- toRefs 处理对象中属性的时候,类似 ref
- 通过 toRefs 处理 reactive 返回的代理对象,可以进行解构
reactive 的问题
上例中如果想简化 x
y
的使用,一般来说就会想到去解构 useMousePosition
返回的对象。
<div id="app">
x: {{ x}}
<br />
y: {{ y}}
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
function useMousePosition () {
const position = reactive({
x: 0,
y: 0
})
// 定义事件处理函数
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return position
}
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
// const position = useMousePosition()
const { x, y } = useMousePosition()
return {
x,
y
}
}
})
console.log(app)
app.mount('#app')
</script>
但是查看效果,x
y
并没有实时更新,说明解构出来的 x
和 y
不是响应式对象。
这是因为 useMousePositon
中将 {x:0, y:0}
转化为响应式对象的方式,是调用 reactive
将其转化成 Proxy 代理对象。
此时的 position
是一个 Proxy 对象,将来访问 position
的 x
和 y
的时候,会调用代理对象的 getter
拦截收集依赖。
当 x
y
变化后,会调用代理对象的 setter
进行拦截触发更新。
当 const { x, y } = useMousePosition()
把代理对象解构的时候,就相当于定义了 x
和 y
两个变量来接收 position.x
和 position.y
。
这里的 x
和 y
就是两个基本类型的变量,解构的过程就是基本类型的赋值,只是把值在内存中复制一份,和代理对象无关。
当重新给它们赋值的时候,并不会调用代理对象的 setter
,无法触发更新的操作。
所以,不能对响应式对象解构。
// ES6 解构语法
const {x,y} = position
// babel 降级处理后的结果
"use strict";
var _position = position,
x = _position.x,
y = _position.y;
toRefs
如果想要实现上述简化的效果,可以使用 toRefs
。
toRefs
用于把一个响应式对象中的所有属性也转换成响应式的。
<div id="app">
x: {{ x}}
<br />
y: {{ y}}
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
function useMousePosition () {
const position = reactive({
x: 0,
y: 0
})
// 定义事件处理函数
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return toRefs(position)
}
const app = createApp({
setup() {
// 第一个参数 props:接收外部传入的参数
// 第二个参数 context:对象 {attrs, emit, slots}
// const position = useMousePosition()
const { x, y } = useMousePosition()
return {
x,
y
}
}
})
console.log(app)
app.mount('#app')
</script>
内部实现原理
toRefs()
要求传入的必须是一个代理对象,否则会报警告。
接着它内部会创建一个新的对象,然后遍历传入的这个代理对象的所有属性,把所有属性的值都转化成响应式对象,然后都挂载到新创建的对象上。
最后返回这个新创建的对象。
它内部会为代理对象的每一个属性,创建一个具有 value
属性的对象,该对象是响应式的。
value
属性具有 getter/setter
。
getter
中返回代理对象中对应属性的值。
setter
中给代理对象的属性赋值。
所以返回的每一个属性都是响应式的。
因为toRefs
会将响应式对象中的每个属性都转化成响应式对象,所以可以解构它返回的对象,并且解构的每个属性也都是响应式的。
-
在模板中使用解构后的属性的时候,可以把
value
省略。 -
但是在代码中使用的时候,不能省略
value
。
ref
ref
函数用于把一个普通数据转化成一个响应式数据。
不同于reactive
是把一个对象转化成响应式数据,ref
可以把一个基本类型的数据包装成响应式对象。
它内部的原理同 toRefs
一样,也是将数据转化成一个具有 value
属性的对象,value
具有 getter/setter
。
<div id="app">
<button @click="increase">按钮</button>
<span>{{ count }}</span>
</div>
<script type="module">
import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'
function useCount () {
const count = ref(0)
return {
count,
increase: () => {
// count 在模板中使用可以省略value
// 在代码中使用不可省略
count.value++
}
}
}
const app = createApp({
setup(){
return {
...useCount()
}
}
}).mount('#app')
</script>
内部实现原理
基本数据类型存储的是值,所以不可能是响应式数据。
ref()
传递的参数
- 如果是对象,内部会调用
reactive()
返回一个代理对象。 - 如果是基本类型的值,内部会创建一个只有
value
属性的对象。- 该对象的
value
属性具有getter/setter
。 - 在
getter
中收集依赖,在setter
中触发更新
- 该对象的
ref 和 reactive 如何选择
ref
和 reactive
的区别:
ref
是把原始值添加一层包装,使其变成响应式的引用类型的值reactive
是引用类型的值变成响应式的值
所以两者的区别在于是否需要添加一层引用包装,其目的都是对数据添加响应式效果。
需要注意,使用 ref
包装后,需要使用 .value
才能进行取值和赋值操作。所以在创建对象时,如果有一个 value
的属性,容易引起混淆,难以区分是不是 ref
方法加上去的属性。
其他区别参考:https://juejin.cn/post/6860349065742745613
computed
计算属性的作用是简化模板中的代码,可以缓存计算的结果,当数据变化后才会重新计算。
在 setup
中可以通过 computed
函数创建计算属性。
computed
函数有两种用法:
- 传入一个获取值的函数
- 函数内部依赖响应式的数据,当依赖的数据发生变化后,会重新执行该函数获取数据。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
- 传入一个具有
getter
和setter
的对象
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
computed
函数返回一个不可变的响应式对象,类似于使用 ref
创建的对象,只有一个 value
属性,获取计算属性的值要通过 value
属性获取,模板中使用可以忽略 .value
。
第一种方式返回对象的value
不能被修改。
第二种方式返回的对象的value
可以修改,会调用计算属性的 setter
方法。
以第一个方式为例:
<div id="app">
<button @click="push">按钮</button>
未完成:{{ activeCount }}
</div>
<script type="module">
import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
const data = [
{text: '看书', completed: false},
{text: '敲代码', completed: false},
{text: '约会', completed: true}
]
createApp({
setup() {
const todos = reactive(data)
const activeCount = computed(() => {
return todos.filter(item => !item.completed).length
})
return {
activeCount,
push: () => {
todos.push({
text: '开会',
completed: false
})
}
}
}
}).mount('#app')
</script>
watch
在 setup
函数中可以使用 watch()
创建一个侦听器。
它的使用方式和之前使用的 this.$watch
和 选项中的 watch
一样,监听响应式数据的变化,然后执行一个相应的回调函数。
可以获取到监听数据的新值和旧值。
watch()
函数的3个参数:- 第一个参数:要监听的数据
- 可以是获取值的函数、ref 或 reactive 返回的对象、数组
- 注意:Vue 2 中是一个字符串表达式
- 第二个参数:监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值
- 第三个参数:选项对象,deep 和 immediate
- 第一个参数:要监听的数据
watch()
的返回值- 取消监听的函数
<div id="app">
<p>
请问一个 yes/no 的问题:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script type="module">
import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'
createApp({
setup(){
const question = ref('')
const answer = ref('')
watch(question, async (newValue, oldValue) => {
const response = await fetch('https://www.yesno.wtf/api')
// ref 返回的对象不可变,应该修改它的 value 属性
const data = await response.json()
answer.value = data.answer
})
return {
question,
answer
}
}
}).mount('#app')
</script>
watchEffect
watchEffect
是watch
函数的简化版本,也用来监听数据的变化- 内部实现是和
watch
调用的同一个函数doWatch
- 不同的是
watchEffect
没有第二个回调函数的参数 watchEffect
接收一个函数作为参数,监听函数内响应式数据的变化- 这个函数会立即执行,当响应式数据发生变化,会重新执行
- 它也返回一个取消监听的函数
<div id="app">
<button @click="increase">increase</button>
<button @click="stop">stop</button>
<br>
{{ count }}
</div>
<script type="module">
import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'
createApp({
setup(){
const count = ref(0)
// watchEffect 接收的函数,初始时会立即执行
const stop = watchEffect(() => {
console.log(count.value)
})
return {
count,
stop,
increase: () => {
count.value++
}
}
}
}).mount('#app')
</script>
在一些监听数据的场景中,当数据变化后,记录数据到 localStorage
中,使用 watchEffect
会非常的方便。
directive 自定义指令
Vue 3 自定义指令不同于 Vue 2.x 地方主要是钩子函数的重命名。
Vue 3 将指令中钩子函数的名称和组件钩子函数的名称保持一致,容易理解。
但是自定义指令的钩子函数和组件的钩子函数执行方式是不一样的。
第二个参数是对象时:
- Vue 2.x
Vue.directive('editingFocus', {
bind(el, binding, vnode, prevVnode) {},
inserted() {},
update() {}, // remove
componentUpdated() {},
unbind() {}
})
- Vue 3.0
app.directive('editingFocus', {
// DOM挂载
beforeMount(el, binding, vnode, prevVnode) {}, // 类似 bind
mounted() {}, // 类似 inserted
// DOM更新
beforeUpdate() {}, // new
updated() {}, // 类似 componentUpdated
// DOM卸载
beforeUnmount() {}, // new
unmounted() {} // 类似 unbind
})
第二个参数是函数时,Vue 3 和 Vue 2 的用法一样:
// Vue 2 函数会在 bind 和 update 时执行
Vue.directive('editingFocus', (el, binding) => {
binding.value && el.focus()
})
// Vue 3 函数会在 mounted 和 updated 时执行
app.directive('editingFocus', (el, binding) => {
binding.value && el.focus()
})