Appearance
Composition API
这个API仅仅是Vue3.0中新增的API,我们依然可以使用Options API,
新增API
下面要介绍三个新增API
- createApp:创建vue对象
- setUp:Composition API的入口
- reactive:创建响应式对象
createApp
createApp
createApp的作用是可以创建一个vue对象,可以接受一个选项作为参数,参数中可以设置属性data,computed,mounted、created、methods等
createApp返回一个app,即Vue对象,和2.x的不同
不同的点:
- 成员比Vue2少很多
- 而且都没有使用$开头,说明未来基本不用给这个对象新增成员
- mount相当于$mount,unmount类似$destroy方法
首先在安装了Vue3.0的项目中,创建html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>createApp</title>
</head>
<body>
<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({
// data这里统一了函数的写法,不支持对象的写法
data () {
return {
position: {
x: 0,
y: 0
}
}
}
})
console.log(app)
// 调用vue对象的mount方法,这个mount方法类似于$mount,把实例挂载到指定的位置
app.mount('#app')
</script>
</body>
</html>
setup
setup
两个参数: 第一个参数是 props,是响应式对象,其不能被解构 第二个参数是 context 是个对象,有三个成员 attrs、emit、slots
返回值: setup 返回一个对象,这个对象可以使用到模板、computed、methods及生命周期钩子函数里
执行时机: 在props被解析完毕,在组件实例被创建之前执行的。
注意事项:
- 无法通过this获取组件实例,因为组件实例还未被创建,这个时候this是undefined,
- 无法访问组件中的data、methods、computed。
setup属性是composition API的入口。
html
<body>
<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 () {
// 普通对象,并不是响应式对象
const position = {
x: 0,
y: 0
}
// 将position放入setup返回的对象里,这个对象可以使用到模板、computed、methods及生命周期钩子函数里
return {
position
}
},
mounted () {
// 因为这里组件实例已经创建完毕,所以可以通过this访问到position
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
</body>
上面的例子,运行之后发现position.x并没有修改,因为position并不是响应式数据,如果想要将position弄成响应式数据,那么需要使用reactive函数。
reactive
reactive
参数是对象,返回一个响应式对象
功能:将对象转化成响应式对象,并且该对象的嵌套属性也应该成为响应式对象
设置成响应式数据,setup中还可以使用data,但是在Vue3.0中还有一个API可以让我们用reactive创建响应式对象。
html
<body>
<div id="app">
x: {{ position.x }} <br/>
y: {{ position.y }}
</div>
<script type="module">
// 在使用reactive之前,需要先导入API
import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
setup () {
// 这里的使用和observable作为函数名是避免和另一个函数库rxjs重名出现混淆。
const position = reactive({
x: 0,
y: 0
})
return {
position
}
},
mounted () {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
</body>
这样就可以看到position.x的值进行了改变。
生命周期钩子函数
案例中注册mousemove事件可以在mounted中实现,但是最终的目标是获取鼠标的整个逻辑封装到一个函数中,让任何组件都可以重用,这个使用就不适合用mounted选项,其实我们在setup中也可以使用生命周期的钩子函数。
可以看看setup里面对应的钩子函数名称,就是在前面加了on,首字母大写。
Options API | setup Hook inside |
---|---|
beforeCreate | not needed * |
created | not needed * |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmouned | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
其中setup是在beforeCreate和created两个函数中间执行的,所以并不需要单独领出来。
renderTracked和renderTriggered都是调用render的时候触发的,不同的是,renderTracked是首次调用render的时候也会触发。renderTriggered在首次调用的时候不会触发。
案例
跟着上面的案例,我们的目标:
- 注册鼠标移动的事件,当鼠标移动的时候显示当前鼠标的位置
- 当组件被卸载的时候,鼠标被移动的事件也要被移除
html
<body>
<div id="app">
x: {{ position.x }} <br/>
y: {{ position.y }}
</div>
<script type="module">
// 导入onMounted, onUnmounted函数
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
const app = createApp({
setup () {
const position = reactive({
x: 0,
y: 0
})
// 接收一个参数,事件对象e,把鼠标的位置记录到position的x和y中
const update = e ={
position.x = e.pageX
position.y = e.pageY
}
// onMounted生命周期触发的时候创建侦听器,监听mousemove事件
onMounted(() ={
window.addEventListener('mousemove', update)
})
// onUnmounted生命周期触发的时候移除侦听器,移除mousemove事件处理函数
onUnmounted(() ={
window.removeEventListener('mousemove', update)
})
return {
position
}
},
mounted () {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
</body>
这个时候把页面打开,可以看到鼠标移动,x和y的值跟着改变,下面我们要定义一个函数,把功能封装进去
html
<body>
<div id="app">
x: {{ position.x }} <br/>
y: {{ position.y }}
</div>
<script type="module">
// 导入onMounted, onUnmounted函数
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)
})
// 将position返回,让组件可以使用position
return position
}
const app = createApp({
setup () {
// 调用函数,并接收position让组件使用
const position = useMousePosition()
return {
position
}
},
mounted () {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
</body>
创建响应式数据API
- reactive
- toRefs
- ref
这三个函数都是创建响应式数据的,刚才的鼠标移动案例中,使用reactive有一个小问题,如果在插值表达式里面不想写
{{ position.x }}
而是写
{{ x }}
,那么要对响应式的position进行解构
js
const {
x,
y
} = useMousePosition()
这样运行发现,鼠标的值根本无法获得。这是为什么呢?
因为我们创建position响应式对象,就是创建了一个proxy对象,我们调用访问position.x和position.y就是调用proxy的getter拦截收集依赖,当x和y变化之后就调用setter进行拦截触发更新。
当我们进行解构的时候,就相当于定义了一个x和一个y变量去接收position.x和position.y,而基本类型的赋值就是把内存中的内容复制一份,这两个变量和proxy对象无关,重新赋值的时候也不会调用代理对象的setter。
那如果我们就想这么做怎么办?介绍一个新的API —— toRefs
toRefs
它可以把一个响应式对象的所有属性,也转换成响应式的。
toRefs
参数: 传入的参数必须是一个代理对象,如果position不是代理对象就会警告。
功能: 会遍历代理对象的属性,将其转化成响应式对象,然后再挂载到新创建的对象上面,将新创建的对象返回。
原理: 其内部会对代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的。value属性里面有getter和setter,getter里面返回代理对象对应属性的值,setter中给代理对象的属性赋值,所以返回的每一个属性都是响应式的。 value在插值表达式中可以省略,但是在代码中不能省略。
html
<body>
<div id="app">
x: {{ x }} <br/>
y: {{ y }}
</div>
<script type="module">
// 导入toRefs函数
import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
// 定义一个函数,将实现逻辑写进去。
function useMousePosition () {
const position = reactive({
x: 0,
y: 0
})
...
// 使用toRefs将position中的属性也转化成响应式数据
return toRefs(position)
}
const app = createApp({
setup () {
// 解构position
const {
x,
y
} = useMousePosition()
return {
x,
y
}
}
})
console.log(app)
app.mount('#app')
</script>
</body>
ref
它是将普通数据可以转换成响应式数据,和reactive不同的是,reactive是将对象转换成响应式数据,ref是可以将基本类型的数据包装成响应式对象。
ref
参数: 如果是对象,那么函数内部调用reactive会返回一个代理对象; 如果是基本类型的值,那么会创建一个value属性的对象,value属性中会有getter和setter,getter收集依赖,setter触发更新。并返回。
返回值: 如果参数是对象,返回一个代理对象 如果参数是基本类型,返回一个有value属性的对象
通过一个简单的点击按钮+1的案例说明:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点击按钮自身+1</title>
</head>
<body>
<!-- 点击按钮自身+1 -->
<div id="app">
<button @click="increase">按钮</button>
<!-- 在模板中使用可以省略value -->
<span>{{ count }}</span>
</div>
<script type="module">
import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'
function useCount () {
// 如果直接等于0不是响应式的
// const count = 0
// 调用ref是响应式数据,返回的是一个对象
// 这个对象中只有一个属性是value,value的值为0,且有getter和setter,用于拦截访问和赋值
const count = ref(0)
return {
count,
increase: () ={
count.value++
}
}
}
createApp({
setup () {
return {
// 将useCount返回的对象进行解构,直接return
...useCount()
}
}
}).mount('#app')
</script>
</body>
</html>
用法
- 第一种用法:
watch(() => count.value + 1)
传入一个获取值的函数,函数内部依赖响应式的数据,当依赖的数据发生变化之后会重新执行该函数获取数据。computed函数返回一个不可变的响应式对象,类似于使用ref创建的对象,只有一个value属性,获取计算属性的值要通过value获取,模板中使用计算属性可以省略value。
- 第二种用法:
传入一个对象,这个对象具有getter和setter,返回一个不可变的响应式对象,当获取值的时候会触发这个值的getter,当设置值的时候会触发这个值的setter
js
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
案例
- 点击按钮,添加一个代办选项
- 显示未完成数量
html
<body>
<div id="app">
<button @click="push">按钮</button>
<p>未完成: {{ activeCount }}</p>
<ul>
<li v-for="todo in todos" :key="text">{{ todo.text }} -- {{ todo.completed }}</li>
</ul>
</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,
todos,
push: () => {
todos.push({
text: '开会',
completed: false
})
}
}
}
}).mount('#app')
</script>
</body>
watch
和computed类似,我们可以在setup中使用watch函数创建一个侦听器。他的用法和之前的this.$watch和选项中的watch作用是一样的,监听响应式数据的变化,然后执行回调函数。可以获取到监听数据的新值和旧值。
Watch
参数:
- 要监听的数据(可以是一个ref,也可以是reactive返回的对象,还可以是数组。)
- 监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值。
- 选项对象,deep (深度监听)和 immediate(立即执行)
返回值: 取消监听的函数
案例
监听文本框的内容,内容更改就发送接口获取答案。
html
<body>
<div id="app">
<p>
请问一个 yes/no 的问题:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script type="module">
// https://www.yesno.wtf/api
import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'
createApp({
setup () {
const question = ref('')
const answer = ref('')
// 第一个参数vue2.x这里传的是字符串,现在可以是ref返回的对象
watch(question, async (newValue, oldValue) => {
// fetch返回的是promise对象
const response = await fetch('https://www.yesno.wtf/api')
// json返回的也是一个promise对象
const data = await response.json()
// 这里获取值要用value
answer.value = data.answer
})
return {
question,
answer
}
}
}).mount('#app')
</script>
</body>
watchEffect
vue3.0中新增函数,是watch函数的简化版本,也用来监视数据的变化。内部实现和watch调用的是同一个函数,不同的是watch没有第二个回调函数的参数。
watchEffect
参数: WatchEffect接收一个函数作为参数,监听函数内响应式数据的变化。它会立即执行一次这个函数,当数据变化之后会重新运行该函数。
返回值: 返回一个取消监听的函数
html
<body>
<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 () {
// count是一个响应式数据,初始值是0
const count = ref(0)
// watchEffect接收一个函数参数,数据变化的执行执行该函数
// 返回stop,stop是停止监听的函数
const stop = watchEffect(() => {
console.log(count.value)
})
return {
count,
stop,
// 增加,数值+1
increase: () => {
count.value++
}
}
}
}).mount('#app')
</script>
</body>
打开浏览器可以看到,每点击一次increase,count的值就+1,然后在控制台中也会打印count的数值。再点击stop之后,点击increase的时候,count的数值还在增加,但是控制台中不会再打印count的值了。
自定义指令
Vue2.x和Vue3.0的自定义指令不同,主要是自定义指令的钩子函数被重命名,Vue3.0中把钩子函数的名称和组件钩子函数的名称保持一致了,这样很容易理解,但是自定义指令钩子函数和组件钩子函数的执行方式是很不一样的。
js
// Vue2.x
Vue.directive('editingFocus', {
bind(el, binging, vnode, prevVnode) {},
inserted() {},
update() {}, // remove
componentUpdated() {},
unbind() {}
})
Vue3.0有三组,挂载mount,更新update和卸载unmount。
js
// Vue3.0
app.directive('editingFocus', {
// 挂载到DOM树
beforeMount(el, binging, vnode, prevVnode) {},
mounted() {},
beforeUpdate() {}, // new
updated() {},
beforeUnmount() {}, // new
unmounted() {}
})
而函数简写的方式两者并没有什么不同:
- Vue2.x是在bind和update的时候执行
- el 参数是指令绑定的那个元素
- binding 参数可以获取到指令对应的值,通过binding.value来获取
js
// Vue2.x
Vue.directive('editingFocus', (el, binding) => {
binding.value && el.focus()
})
- Vue3.0是在mounted和updated的时候执行。
js
// Vue3.0
app.directive('editingFocus', (el, binding) => {
binding.value && el.focus()
})