mixin
如果组件之间存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
mixin提供一种灵活的方式,来分发vue组件中可复用的功能
一个mixin对象可以包含任何组件选项(option)
局部混入
a组件
data() {
return {
msg: 'hhh'
}
}
b组件
data() {
return {
msg: 'hhh'
}
}
存在可复用逻辑
混入
export const a_b_minxin = {
data() {
return {
msg: 'hhh'
}
}
}
a组件、b组件可以这样写
import { a_b_minxin } from './mixins'
mixin: ['a_b_minxin']
不止data选项,其它选项和生命周期也可以抽取。
全局混入
如果希望混入的对象放进所有组件呢?
使用app的api
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mixin({
想混入的的内容
})
app.mount('#app')
合并规则
当混入的对象中的选项和组件对象中的选项出现冲突
分情况:
- 如果是data函数返回值对象,会保留组件自身的数据
- 如果是生命周期钩子函数,会被合并到数组中,都会被调用
- 其它选项,比如methods、components等等,将会合并为同一个对象,如果对象的key相同,会取组件对象的键值对
options API的弊端
同一个逻辑分散
- 实现某个功能时,这个功能对应的代码逻辑会被拆分到各个选项中
- 当组件变得更大,更复杂时,逻辑关注点的列表会增长,同一个功能的逻辑就会被拆分的很分散
比如一个计数器功能
<h2>{{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
data() {
return {
counter: 0
}
},
methods: {
increment() {
counter++
},
decrement() {
counter--
}
}
你看,计数器功能的逻辑被拆分到data和methods里面了。一个功能的逻辑被拆分了你可觉得没什么,但是如果多个功能的逻辑都被拆分了,可读性非常不好。
如果某一天你想修改某个逻辑,那你可有的忙啦,不是吗?
setup()
如果我们能将同一个逻辑关注点相同的代码收集在一起,不是更好么?
这就是Composition API想做的事
Vue Composition API也有人叫VCA
setup()是组件的一个选项,用来替代(methods、computed、watch、data、生命周期等等)
参数
setup(props, context) {}
props
父组件传过来的属性
如果setup中需要使用,拿到props参数即可
context
也叫SetupContext,包含3个属性:
- attrs:所有非prop的属性(如id、class等等这些属性)
- slots:父组件传递过来的插槽(这以渲染函数返回时才有用)
- emit:组件内部需要发射事件时会用到(因为不能访问this,所以不可以通过this.$emit发射事件)
返回值
setup既然是个函数,那它也可以有返回值。它的返回值用来做什么呢?
可以在模板template中使用
可以通过setup返回值来替代data选项
<h2>{{ msg }}</h2>
setup() {
return {
msg: 'hhh'
}
}
你就会看到hhh的标题~
不可以使用this
setup并没有绑定this,this并没有指向当前组件实例(之前官网说明的原因是:执行setup之前,组件实例还没有被创建出来。这个说法是错误的,后来已修改。组件实例创建出来后,才执行setup的,但是this并没有绑定当前组件实例)
setup的调用发生在data、property、computed或methods被解析之前,所以他们无法在setup中被获取
(vue2在methods、生命周期中可以拿到this,指向的是当前组件实例)
获取ref
在vue2,我们要想获取一个元素或组件的信息,是这样的
通过this.$refs
<h2 ref="h">哈哈哈</h2>
methods: {
foo() {
console.log(this.$refs.h)
}
}
但是vue3的setup里面是不能通过this获取当前组件实例的,所以不能通过this.$refs获取组件或元素的信息
那怎么获取?
<h2 ref="title">哈哈哈</h2>
import { ref } from 'vue'
setup() {
const title = ref(null)
return {
title
}
}
当h2节点挂载完成,就可以通过title.value获取到h2的信息,组件亦是如此
顶层写法
后面补充~
Reactive API
响应式
在setup里面定义的变量,不是响应式的,尽管你修改该变量的值,在界面上也不会响应。
date里的变量可以做到响应式的原因是:vue内部将data里面内部的变量经过reactive() 函数的处理
其实界面上拿到的变量时reactive()处理过的返回值
<h2>{{ counter }}</h2>
<button @click="increment">+</button>
setup() {
let counter = 100
const increment = () => {
counter++
}
return {
counter,
increment
}
}
这样虽然可以在界面上显示,但是当你点击+号时,界面并没有响应counter的变化(实际上counter已经变了)
你需要将counter传进reactive函数
<h2>{{ state.counter }}</h2>
<button @click="increment">+</button>
import { reactive } from 'vue'
setup() {
const state = reactive({
counter: 100
})
const increment = () => {
state.counter++
}
return {
state,
increment
}
}
其它API
isProxy()
检查对象是否是由reactive或readonly创建的proxy
isReactive()
- 检查对象是否由reactive创建的proxy
- 如果该proxy是由readonly创建的,但是包裹了由reactive创建的另一个proxy,它也会返回true
const readonlyObj = readonly(reactiv(obj1))
isRactive(obj)// true
isReadonly()
检查对象是否是由readonly创建的proxy
toRaw()
返回reactive或readonly代理的原始对象(不建议保留对原始对象的持久引用,谨慎使用)
shallowReactive()
创建一个proxy,它跟踪其自身property的响应式,但不执行嵌套对象的深层响应式转换(深层还是原生对象)
什么意思呢?
const obj = reactive({
name: 'zsf',
friend: {
name: 'hhh'
}
})
obj.friend.name = 'lll'
reactive() 对一个对象进行响应式转换是彻底的:如果该对象内部嵌套着对象(不管多少层),都会一律变成响应式
但是如果你希望只是对象最外层的属性是响应式的,就可以使用shadowReactive()啦
shallowReadonly()
创建一个proxy,使其自身的property为只读,但不执行嵌套对象的深度只读转换(深度还是可读,可写的)
类似shallowReactive() ,只有对象最外层的属性是只读的
Ref API
reactive()对传入的类型是有限制的,它要求我们必须传入的是一个对象或数组类型,如果传基本数据类型(String、Number、Boolean)会报一个警告
如果你觉得要通过state.counter使用有点麻烦,只是想单纯地对counter这个基本类型实现响应式
那就使用Ref API吧
ref自动解包
ref()会返回一个可变的响应式对象,该对象作为一个响应式的引用维护这它内部的值
它内部的值是在ref的value属性中被维护的
<h2>{{ value }}</h2>
<button @click="increment">+</button>
import { ref } from 'vue'
setup() {
let counter = ref(100)
const increment = () => {
counter.value++
}
return {
counter,
increment
}
}
这样counter就变成了一个ref的可响应式引用
理论上使用counter.value才可以使用counter的值,但vue为了我们开发方便,它做了这么一件事:
当我们在template模板中使用ref对象,它会自动进行解包
换句话说:template模板中使用时直接counter就行
这时你可能想问:setup中可不可以也这样使用呢?
不可以。上面说到,当我们在template模板中使用ref对象,它会自动进行解包。
所以这样的用法只能在template模板中使用,别的地方想拿到原本counter的值(100),就要counter.value
浅层解包
ref对象的解包是浅层的
普通对象
如果最外层包裹的是普通对象(假设叫obj,obj包裹counter),然后在template中通过obj.counter使用,是不会解包的。
<h2>{{ obj.counter }}</h2>
<button @click="increment">+</button>
import { ref } from 'vue'
setup() {
let counter = ref(100)
const obj = {
counter
}
const increment = () => {
counter.value++
}
return {
counter,
increment
}
}
如果这样写,你会发现报错了。你得obj.counter.value才行
reactive可响应式对象
但是,如果最外层包裹reactive可响应式对象,那么内容的ref是可以解包的
<h2>{{ reactiveObj.counter }}</h2>
<button @click="increment">+</button>
import { ref, reactive } from 'vue'
setup() {
let counter = ref(100)
const reactiveObj = reactive({
counter
})
const increment = () => {
counter.value++
}
return {
counter,
reactiveObj,
increment
}
}
这样写是没什么问题的,但是开发中不推荐。
其它API
toRefs()
如果使用es6的解构语法,对reactive() 返回的对象进行解构赋值,不再是响应式
<h2>{{ name }}-{{ age }}</h2>
<button @click="increment">+</button>
import { reactive } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 100
})
let { name, age } = info
const increment = () => {
age++
}
return {
name,
age,
increment
}
}
当你点击+号,你会发现屏幕上age没变(解构后不再是响应式啦)
这里的let { name, age } = info就相当于
let name = 'zsf'
let age = 100
我们已经知道,在setup中只是简单的声明变量,就不是响应式的。
但是开发中要是这么一个需求:解构后依然是响应式,怎么办?
使用toRefs()
<h2>{{ name }}-{{ age }}</h2>
<button @click="increment">+</button>
import { reactive, toRefs } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 100
})
let { name, age } = toRefs(info)
const increment = () => {
age.value++
}
return {
name,
age,
increment
}
}
let { name, age } = toRefs(info)做的事情是:name = ref.name, age = ref.age
相当于用name和age分别指向是响应式的ref.name、ref.age
注意:age.value才能拿到age的值,template里只使用age是因为vue自动解包了(上面讲过了)
toRefs()使info.age和ref.age建立了链接,任何一个修改都会引起另外一个变化,所以上面increment函数里面的age.value换成info.age也行
当你再点击+号,屏幕上的age就会变啦
toRef()
与toRefs()不同,toRefs()是将所有的属性变成对应的ref对象
而toRef()是转换一个属性
import { reactive, toRef } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 100
})
let age = toRef(info, 'age')
return {
age
}
}
unref()
如果想获取一个ref引用中的value,也可以通过unref()
如果参数是个ref,则返回内部值,否则返回参数本身
unref()其实是 val = isRef(val) ? val.value : val语法糖函数
isRef()
判断是否是一个ref对象
shalldowRef()
创建一个浅层的ref对象
triggerRef()
手动触发和shalldowRef() 相关联的副作用
<h2>{{ name }}</h2>
<button @click="increment">+</button>
import { shallowRef, triggerRef } from 'vue'
setup() {
const info = shallowRef({
name: 'zsf',
friend: {
name: 'hhh'
}
})
const changeInfo = () => {
info.value.name = 'lll'
triggerRef(info)
}
return {
info,
changeInfo
}
}
shalldow()产生了浅层ref对象这个副作用,如果想消去这个副作用,使用triggerRef()
shalldow()和triggerRef()结合使用,其实就是ref() 的功能
customRef()
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制
- 它需要一个工厂函数,该函数接受track函数和trigger函数作为参数
- 并且返回一个带有get和set的对象
function my_ref() {
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
value = newValue
trigger()
}
}
})
}
一个对象,当使用时track() 收集依赖,设置时trigger() 更新
老人言
能用ref就用ref,reactive不方便代码抽离~
readonly()
通过reactive或者ref可以获取到一个响应式的对象,但是在某些情况下,可以将该响应式的对象传给其它地方使用,但是不希望被修改
vue3提供了readonly()
readonly()会返回原生对象的只读代理(依然是个Proxy,proxy的set()被劫持,且不能对其进行修改)
原理
它内部的原理大概是这样的
const obj = {
name: 'zsf'
}
const objProxy = new Proxy(obj, {
get(target, key) {
return target[key]
},
set(taget, key, value) {
警告:不允许修改!
}
})
objProxy.name = 'hhh'// 警告:不允许修改!
基本使用
<button @click="update">修改</button>
import { readonly } from 'vue'
setup() {
const obj = {
name: 'zsf'
}
const proxyObj = readonly(obj)
const update = () => {
proxyObj.name = 'hhh'
}
return {
update
}
}
当你点击修改,就会出现警告!
computed()
setup是如何替换掉option API中的computed的呢?
使用computed()
computed() 返回值是一个ref对象
<h2>{{ fullName }}</h2>
<button @click="changeName">改名</button>
import { ref, computed } from 'vue'
setup() {
const firstName = ref('z')
const lastName = ref('sf')
cosnt fullName = computed(() => {
return firstName.value + '' + lastName.value
})
const changeName = () => {
firstName.value = 'h'
}
return {
fullName,
changeName
}
}
watch
setup是如何替换掉option API中的watch的呢?
- watchEffect() 自动收集响应式数据的依赖
- watch() 需要手动指定侦听的数据源
watchEffect()
- 参数:接收一个函数,默认立即执行一次
- 返回值: 是个函数
<h2>{{ name }}-{{ age }}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
const name = ref('zsf')
const age = ref(18)
const changeName = () => name.value = 'hhh'
const changeAge = () => age.value++
watchEffect(() => {
console.log(name.vlaue, age.value)
})
return {
name,
age,
changeName,
changeAge
}
}
当你点击这两个按钮,使收集的依赖发生变化,会执行watchEffect()
原理
默认立即执行一次的过程中,它会收集用了哪些可响应式的对象(也就是收集依赖)
只有收集的依赖发生变化时,watchEffect() 传入的函数才会再次执行
停止侦听
如果想在某些情况下停止侦听呢?
比如上面的例子,当年龄到25不再侦听,可以使用watchEffect()的返回值函数,调用它即可
<h2>{{ age }}</h2>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
const age = ref(18)
const changeAge = () => {
age.value++
if(age.value > 25) {
stop()
}
}
const stop = watchEffect(() => {
console.log(name.vlaue, age.value)
})
return {
age,
changeAge
}
}
清除副作用
什么是清除副作用呢?
在开发中我们需要在侦听函数中发起网络请求,但在网络请求还没到达时,停止了侦听器,或者侦听器侦听函数再次被执行了
那么上一次的网络请求应该被取消掉,这个时候就可以清除上一次的副作用了
watchEffect第一个参数接收一个函数(假设叫foo),而foo的参数,又接收一个函数foo1,取消操作一般foo1里面执行
<h2>{{ age }}</h2>
<button @click="changeAge">修改age</button>
import { ref, watchEffect } from 'vue'
setup() {
const age = ref(18)
const changeAge = () => {
age.value++
}
const stop = watchEffect((foo) => {
foo(() => {
// 取消操作
})
console.log(name.vlaue, age.value)
})
return {
age,
changeAge
}
}
执行时机
<h2 ref="title">哈哈哈</h2>
import { ref } from 'vue'
setup() {
const title = ref(null)
return {
title
}
}
当h2节点挂载完成,就可以通过title.value获取到h2的信息,组件亦是如此
但是,setup执行发生在节点挂载之前,现在的title.value是null
要是想拿到title.value的值呢?
使用watchEffect()的第二个参数,等节点挂载完成再执行watchEffect()的回调。这样,title.value就有值啦
import { ref, watchEffect } from 'vue'
setup() {
const title = ref(null)
watchEffect(() => {
console.log(title.value)
}, {
flush: 'post'
})
return {
title
}
}
watch()
与watchEffect相比,watch允许我们:
- 惰性执行副作用(第一次不会直接执行);
- 更具体说明当哪些状态发生改变时,才触发侦听器的执行
- 可以访问侦听器变化前后的值
侦听单个数据源
参数1-数据源,有两种类型:
- 一个getter函数,但是该函数必须引用一个reactive或ref对象
- 一个可响应式对象,ref或者是reactive对象(常用的是ref对象)
参数2-新旧值
传入getter函数
<h2>{{ info.name }}</h2>
<button @click="changeName">修改name</button>
import { reactive, watch } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 18
})
watch(() => info.name, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
info.name = 'hhh'
}
return {
changeName,
info
}
}
传入可响应式对象
import { reactive, watch } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 18
})
watch(info, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
info.name = 'hhh'
}
return {
changeName
}
}
传入reactive对象的话,oldValue和newValue拿到的值是reactive创建出来的Proxy对象的value
传入ref对象的话,oldValue和newValue拿到的值才是value值的本身(原生对象的value)
import { ref, watch } from 'vue'
setup() {
const name = ref('zsf')
watch(name, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
name.value = 'hhh'
}
return {
changeName
}
}
如果你想让传入reactive对象时,oldValue和newValue也是普通对象的值
你可以对info解构(变成普通对象了),然后再return,变成一个getter函数
import { reactive, watch } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 18
})
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
info.name = 'hhh'
}
return {
changeName
}
}
侦听多个数据源
import { ref, reactive, watch } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 18
})
const name = ref('zsf')
watch([info, name], (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
info.name = 'hhh'
}
return {
changeName
}
}
这样oldValue和newValue分别就会多个值而已
深度侦听
默认
将可响应式对象解构之后不是深度侦听了
import { reactive, watch } from 'vue'
setup() {
const info = reactive({
name: 'zsf',
age: 18
})
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
const changeName = () => {
info.name = 'hhh'
}
return {
changeName
}
}
要想解构之后继续深度侦听,得传第三个参数
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log(oldValue, newValue)
}, {
deep: true
})
立即执行
如果想一开始就执行一次侦听器
同样在第三个参数,immediate: true就行
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log(oldValue, newValue)
}, {
deep: true,
immediate: true
})
生命周期钩子
setup是如何替换声明周期钩子的呢?
通过onX() 这种API,其中X就是updated、mounted等等这些生命周期
import { onCreated, onUpdated, onMounted } from 'vue'
setup() {
onUpdated(() => {
console.log('updated')
})
其它同理
}
注意:
- created和beforeCreated没有对应的API,如果想在这两个生命周期执行回调函数,直接放setup里即可;
- 同一个生命周期可以出现多次
provide/inject
使用
父组件
import { provide } from 'vue'
setup() {
const name = 'zsf'
let counter = 100
provide('name', name)
provide('counter', counter)
}
子孙组件
<h2>{{ name }}-{{ counter }}</h2>
import { inject } from 'vue'
setup() {
const name = inject('name')
const counter = inject('counter')
return {
name,
counter
}
}
用法其实和vue2差不多,不过是通过api的方式在setup里面执行
搭配readonly
当父组件传过来的是可响应式对象,那子孙组件改变该对象,父组件也会受影响,这不符合单向数据流
所以父组件在提供数据时,应该限制只读
这不就是readonly() 的发挥作用的时候了嘛~
import { ref, provide, readonly } from 'vue'
setup() {
const name = ref('zsf')
let counter = ref(100)
provide('name', readonly(name))
provide('counter', readonly(counter))
}
如果父组件的某个数据确实需要改,子孙组件应该是通过发射事件的形式,通知父组件去修改
hook案例
计数器
vue2写法
<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
data() {
return {
counter: 0
}
},
computed: {
doubleCounter() {
return this.counter * 2
}
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
计数器逻辑被分散到data、methods、computed里面了
想利用mixin复用这逻辑时,是不方便的,而且也有可能出现命名冲突的问题
vue3写法
Home.vue
<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
import { ref, computed } from 'vue'
setup() {
const counter = ref(0)
const doubleCounter = computed(() => counter.value * 2)
const increment = () => counter.value++
const decrement = () => counter.value--
return {
counter,
doubleCounter,
increment,
decrement
}
}
你看,同一个逻辑的代码都聚集在一起,维护起来不是方便许多吗?
还可以将这部分抽离出来
建一个hook目录,目录下新建一个useCounter.js
import { ref, computed } from 'vue'
export default function() {
const counter = ref(0)
const doubleCounter = computed(() => counter.value * 2)
const increment = () => counter.value++
const decrement = () => counter.value--
return {
counter,
doubleCounter,
increment,
decrement
}
}
Home.vue就可以这样写了
<h2>当前计数:{{ counter }}</h2>
<h2>计数+2:{{ doubleCounter }}</h2>
<button @click="increment">+1</button>
<button @click="increment">-1</button>
import useCounter from './hook/useCounter.js'
setup() {
const { counter, doubleCounter, increment,decrement } = useCounter()
return {
counter,
doubleCounter,
increment,
decrement
}
}
这样,一个逻辑的代码都聚集在一起,可读性好,而且,要是想复用useCounter() 不是很方便了吗?
修改网页标签页名字
需求
- 参数 网页名
- 返回值 网页名的ref对象,当网页名再次修改时,document.title = 网页名重新执行
Home.vue
import useTitle from './hook/useTitle.js'
setup() {
const titleRef = useTitle('zsf')
setTimeout(() => {
titleRef.value = 'hhh'
}, 2000)
}
useTitle.js
import { ref, watch } from 'vue'
export default function(title = 'default') {
const titleRef = ref(title)
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
}
return titleRef
这样你会发现,一开始网页名为zsf,2秒后变成hhh
监听滚动位置
需求
- 右下角实时显示滚动位置
Home.vue
<div class="scroll">
<div class="scroll-x">scrollX:{{ scrollX }}</div>
<div class="scroll-y">scrollY:{{ scrollY }}</div>
</div>
import useScrollPosition from './hook/useScrollPosition.js'
setup() {
const { scrollX, scrollY } = useScrollPosition()
return {
scrollX,
scrollY
}
}
.scroll {
position: fixed;
right: 30px;
bottom: 30px;
}
为了能滚动,尽量让容器宽高超过设备,这里就不写了~
useScrollPosition.js
import { ref } from 'vue'
export default function() {
const scrollX = ref(0)
const scrollY = ref(0)
document.addEventListener('scroll', () => {
scrollX.value = window.scrollX
scrollY.value = window.scrollY
})
return {
scrollX,
scrollY
}
}
使用缓存
需求
- 只传key,取value
- 传key-vlaue,保存
- 当value变化,重新执行保存
useLocalStorage.js
import { ref, watch } from 'vue'
export default function(key, value) {
const data = ref(value)
// 传key-value,保存到缓存;只传key,取value
if(value) {
window.localStorage.setItem(key, JSON.stringify(value))
} else {
data.value = JSON.parse(window.localStorage.getItem(key))
}
// 当value变化,重新执行保存到缓存
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue))
})
return data
}
优秀的编码习惯
Home.vue
import useScrollPosition from './hook/useScrollPosition.js'
import useTitle from './hook/useTitle.js'
import useCounter from './hook/useCounter.js'
setup() {
...
}
当需要导入的模块很多时,可能可读性不够好
这时需要在hook目录下新建一个index.js(统一的导出出口)
index.js
import useScrollPosition from './useScrollPosition.js'
import useTitle from './useTitle.js'
import useCounter from './useCounter.js'
export {
useScrollPosition,
useTitle,
useCounter
}
Home.vue就可以这样写了
import {
useScrollPosition,
useTitle,
useCounter
} from './hook'
利用webpack对路径的解析特点(导入省略目录下的index.js),并且某种程度抽取了路径(现在在Home.vue只要写一份),代码看起来更简洁~