vue3中setup使用方法和技巧
ref和nextTick
在vue3中的setup里使用ref,需要先注册,再返回,并且使用.value去访问。
<template>
<div>
<progress ref="barRef" :width="progress"><progress>
</div>
</template>
<script type="text/ecmascript-6">
// setup中使用计算 监听 ref属性必须先获取
import { computed, watch, ref, nextTick } from 'vue'
export default {
name: 'player',
components: {
Progress,
},
setup() {
// 1、获取的时候要先定义,如果获取的是组件或者dom那么ref(null)
const barRef = ref(null);
// 设置一个响应式的值,必须要返回到模板
const progress = ref(100);
// 3、监听某个值 执行barRef组件中的方法
watch(fullScreen, async (newFullScreen) => {
if (newFullScreen) {
// 如果需要nextTick也必须要引用
await nextTick();
// barRef.value就可以访问到这个组件实例
// 如果想要使用progress中的数值,必须访问progress.value
barRef.value.setOffset(progress.value)
}
})
// 2、要成功访问必须在setup中返回
return {
barRef,
progress
}
}
}
</script>
当使用外部文件的时候,在外部文件中获取组件的ref,比如:
// 父组件中
<div class="slider-wrapper"
ref="sliderWrapperRef"
>
...
</div>
<script>
import useMiniSlider from './user-mini-slider'
setup() {
const { sliderWrapperRef } = useMiniSlider()
return {
sliderWrapperRef
}
}
</script>
// 在同级目录下创建user-mini-slider.js
import { ref } from 'vue'
export default function useMiniSlider() {
const sliderWrapperRef = ref(null)
// 这里如果要使用组件的实例,必须使用sliderWrapperRef.value
// 比如使用组件中的方法sliderWrapperRef.value.xx()
return {
sliderWrapperRef
}
}
setup中引用外部文件,并在外部文件中请求数据
import useSwiper from './useSwiper';
setup() {
const {
videos
} = useSwiper();
console.log(videos)
return {
videos
}
}
// useSwiper.js
import { ref, onBeforeMount} from 'vue';
import { getData } from "@/api/get-data";
export default function useSwiper() {
// 所有视频数据
const videos = ref([]);
onBeforeMount( async() => {
// 注意!赋值一定要给value赋值!
videos.value = await getData(1);
console.log(videos)
});
return {
videos
}
}
在setup中以及外部js文件使用props,emits
比如我们基于bscroll封装一个scroll组件(真实可用):
<template>
<!-- ref对象通常要加上Ref标识 -->
<div ref="rootRef">
<!-- 内容插槽 -->
<slot></slot>
</div>
</template>
<script type="text/ecmascript-6">
// https://better-scroll.github.io/docs/zh-CN/plugins/
import useScroll from './use-scroll'
import { ref } from 'vue'
export default {
name: 'scroll',
// scroll的props全都是BS配置项的属性
props: {
click: {
type: Boolean,
default: true
},
// 决定是否可以监听到向外派发的滚动事件 3就是只要滚动就派发
probeType: {
type: Number,
default: 0
}
},
// 在vue3中 定义自定义事件(向外派发),建议在组件对象中配置emits属性(代表自定义事件的名称)
emits: ['scroll'],
// setup(props)通过参数拿到props的值
// setup第二个参数是一个对象,在对象中可以拿到emit这个方法,在钩子函数中当参数穿进去
setup(props, { emit }) {
const rootRef = ref(null)
// 接收到返回的scroll实例,并将返回的实例再暴漏(return)出去,之后就可以通过scroll组件的实例,就可以访问到这个scroll变量了
// 并通过这个变量拿到BS的实例了 通过在组件上添加ref拿到组件实例
const scroll = useScroll(rootRef, props, emit)
// 一定要return一个对象包含这个ref才会有效
return {
rootRef,
scroll
}
}
}
</script>
use-scroll.js
import BScroll from '@better-scroll/core/'
// 自动探测更新插件https://better-scroll.github.io/docs/zh-CN/plugins/observe-dom.html
import ObserveDOM from '@better-scroll/observe-dom'
import { onMounted, onUnmounted, ref } from 'vue'
// 在引入后一定要use一下插件
BScroll.use(ObserveDOM)
// 切记传入的一定要是一个ref对象 因为ref对象是响应式的 会在mounted的阶段得到dom元素
// options传递进来的对bs的配置项 emit方法,外部传递进来获取向外派发的自定义事件名称
export default function useScroll(wrapperRef, options, emit) {
const scroll = ref(null)
onMounted(() => {
// debugger 如果需要调试可以打一个debugger会中断当前进程 阻止后续代码执行
// 当debugger执行的时候 内容层实际是没有的 会导致不能滚动
// 当bs2.0有了一个新特性,当dom变化的时候会自动刷新高度
// 同样的还有observe-image插件
// scroll也是一个ref对象,所以他的值要存在value上,才会被数据响应
const scrollVal = scroll.value = new BScroll(wrapperRef.value, {
observeDOM: true,
// 使用扩展运算符传入options
...options
})
// 当大于0的时候,监听scroll事件,拿到滚动的位置
if (options.probeType > 0) {
scrollVal.on('scroll', (pos) => {
// 将位置信息向外派发出去
emit('scroll', pos)
})
}
})
// 对scroll实例卸载逻辑
onUnmounted(() => {
scroll.value.destroy()
})
// 将scroll实例暴漏出去
return scroll
}
在setup中,如果使用props,emits需要手工传入:
setup(props, { emit }) {
useScroll(props, emits)
。。。
}
使用computed技巧,返回ref.value中的方法
比如我们使用上面的组件,需要在scroll组件的基础上,返回scrollRef.value中的方法:
<scroll ref="scrollRef"></scroll>
setup() {
const scrollRef = ref(null);
// 使用计算属性的技巧,返回scrollRef.value中的属性,这里返回的scroll是组件中的实例
// 当不访问计算属性的时候不会执行内部逻辑 只有当访问的时候才会执行
const scroll = computed(() => {
return scrollRef.value.scroll
})
return {
scrollRef,
scroll
}
}
切记不能这样写:
<scroll ref="scrollRef"></scroll>
setup() {
const scrollRef = ref(null);
return {
scrollRef,
/* 返回不能这样写:scroll:scrollRef.value.scroll
因为开始定义的scrollRef是null,所以scrollRef.value也是null
所以在调用的时候必须保证scroll是已经渲染的,如果没渲染就返回肯定不行
*/
scroll:scrollRef.value.scroll 错的!!
}
}
获取ref组件的DOM元素-$el
<transition-group
ref="listRef"
name="list"
tag="ul"
>
<li class="item">...</li>
</transition-group>
setup() {
const listRef = ref(null)
const index = 10;
// 获取ul中的所有li
const target2 = listRef.value.$el.children;
// 获取ul中的某一个li
const target1 = listRef.value.$el.children[index]
return {
listRef
}
}
setup中使用钩子
mounted,onUnmounted,computed
import { onMounted, onUnmounted, computed } from 'vue'
onMounted(() => {
...
})
onUnmounted(() => {
...
})
const xx = computed(() => {
return 'xxxx'
});
使用计算属性中的值的时候,要用xx.value
因为setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写。
生命周期函数有:onBeforeMount
、onMounted
、onBeforeUpdate
、onUpdated
、onBeforeUnmount
、onUnmounted
、onErrorCaptured
、onRenderTracked
、onRenderTriggered
、onActivated
、onDeactivated
。
reactive定义响应式的data数据
import { reactive } from 'vue'
export default {
setup() {
const info = reactive ({
name:'张三',
age:18
}),
}
return { info};
}
在setup函数中使用数据也无需this.xxx,而是用你定义的名字,我定义的是info(也可也换成其他)则用info.name
reactive
是 Vue3 中提供的实现响应式数据的方法。- reactive 参数必须是对象 (json / arr)
- 如果给 reactive 传递了其它对象
- 默认情况下,修改对象无法实现界面的数据绑定更新。
- 如果需要更新,需要进行重新赋值。(即不允许直接操作数据,需要放个新的数据来替代原数据)
reactive不会实现响应式数据(渲染界面数据也变化)
如果在外部js文件或者setup中访问并使用该reactive数据时候,直接访问就行,比如:
const info = reactive ({
name:'张三',
age:18
}),
function demo() {
console.log( info.name )
}
return { info, demo };
如果需要监听reactive值的变化:
const state = reactive({
count: 0,
});
watch(
() => state.count,
(newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
}
);
或者使用deep:
const state = reactive({
count: 0,
age:37
});
watch(
() => state,
(nv, ov) => { ... },
{ deep: true }
)
如果要修改reactive的值,可以单个值式的修改,但是不能整体赋值,那样会失去响应性。
import {reactive} from 'vue';
let obj = reactive({
id:1,
name:'张三',
age:18
})
// 可以单个值的修改
obj.id = 2;
obj.name = '李四';
但是不能直接赋值,这样会失去响应性(这样不行)
obj = {id:2,name:'李四'}
所以如果涉及到多个属性值的修改,应该使用Object.assign
obj = Object.assign(obj, {id:2,name:'李四'})
另附一些reactive修改的其他方法:
清空reactive定义的数组
arr.length = 0
修改reactive定义的数组
// 先清空数组再赋值,防止arr中的数据遗留
arr.length = 0 ;
Object.assign(arr,newArr)
清空reactive定义的对象
// 1、将obj中所有属性值变为null
Object.keys(obj).forEach(key => {
obj[key] = null
})
// 2、将obj变为空对象
Object.keys(obj).forEach(key => {
delete obj[key]
})
修改reactive定义的对象
// 1、如果obj和newObj的键值对不一致 先清空obj再赋值
Object.keys(obj).forEach(key => {
delete obj[key]
})
Object.assign(obj,newObj);
// 2、如果一致
Object.assign(obj,newObj);
watch
import { watch } from 'vue'
const sliderShow = computed(() => {
// !!双非转换成布尔类型
return !!xx.value
})
onMounted(() => {
watch(sliderShow, (newSliderShow, old) => {
...
})
})
如果发现watch的参数,第一次不执行,只有第二次才执行的时候,需要为watch()方法添加额外的配置项。
比如我监听路由中的参数变化:
import { watch, ref } from 'vue';
import { useRoute } from 'vue-router';
export default function useTplget() {
const route = useRoute();
const template = ref(null);
// 监听
watch(
() => route.params.id,
(newid) => {
console.log(newid)
if (!newid) {
return;
}
},
// 页面加载时自动执行一次
{
immediate:true,
deep:true // 开启深度监听 监听对象的变化等 一个值的时候不需要
}
);
return template;
}
vuex和router
在setup中使用vuex是比较繁琐,但是常用的方式也就那么几个,首先我们来构建一个vuex的store
store文件夹中包含:
- action.js
- getters.js
- index.js
- mutation.js
- state.js
action.js 这里记录一些行为函数
// 定义动作 对一个操作 可以操作多个mutations的一个封装
// 定义选择播放的动作 action第一个参数是一个对象可以直接结构 第二个参数是他的参数
// { list, index } 传入一个当前的列表 和 点击的索引
// commit是提交的意思 提交mutation中定义的函数
export function selectPlay({ commit, state }, { list, index }) {
// 点击的时候顺序播放
commit('setPlayMode', PLAY_MODE.sequence)
commit('setSequenceList', list)
// 当点击播放的时候,那么播放状态一定是true
commit('setPlayingState', true)
// 播放的时候会全屏播放
commit('setFullScreen', true)
// 当不随机播放的时候,播放列表和顺序播放列表相同
commit('setPlaylist', list)
// 设置播放索引
commit('setCurrentIndex', index)
}
// 实现随机播放 这里的参数就不需要一个对象 直接一个参数即可
// 洗牌算法https://rosettacode.org/wiki/Knuth_shuffle
export function randomPlay ({ commit }, list) {
// 点击的时候顺序播放
commit('setPlayMode', PLAY_MODE.random)
commit('setSequenceList', list)
// 当点击播放的时候,那么播放状态一定是true
commit('setPlayingState', true)
// 播放的时候会全屏播放
commit('setFullScreen', true)
// 当不随机播放的时候,播放列表和顺序播放列表相同
commit('setPlaylist', list)
// 设置播放索引,洗牌后播放第一首歌
commit('setCurrentIndex', 0)
}
// 封装切换播放模式的方法 getters也可以被解构
export function changeMode({ commit, state, getters }, mode) {
// 从而保证这个新列表中 也是播放当前正在播放的歌曲, 就不会更改当前播放的这首歌了
commit('setCurrentIndex', index)
commit('setPlayMode', mode)
}
getters.js
// 定义一些获取数据的方法
export const currentSong = (state) => {
// 这里如果获取不到值 会返回undefined会引起一些报错,所以改为无值返回一个空对象
return state.playlist[state.currentIndex] || {}
}
mutations.js
// 提交数据 对state数据做修改
const mutations = {
// 修改播放状态 第一个参数是state, 第二个参数是要修改为的值
setPlayingState(state, playing) {
state.playing = playing
},
// 设置顺序(原始)播放列表
setSequenceList(state, list) {
state.sequenceList = list
},
// 设置播放列表 当有随机播放情况的时候,playlist和sequencelist就会不同
setPlaylist(state, list) {
state.playlist = list
},
// 设置播放模式
setPlayMode(state, mode) {
state.playMode = mode
},
// 设置当前播放索引
setCurrentIndex(state, index) {
state.currentIndex = index
},
// 设置播放器全屏或者折叠模式
setFullScreen(state, fullScreen) {
state.fullScreen = fullScreen
},
setFavoriteList(state, list) {
state.favoriteList = list
},
// 给song添加歌词的属性
addSongLyric(state, { song, lyric }) {
// 使用这种方式修改sequenceList
// 这样是保留对象引用 一旦修改playlist也会发生变化 因为他们都指向了同一个对象
state.sequenceList.map((item) => {
// 相等证明是同一首歌
if (item.mid === song.mid) {
item.lyric = lyric
}
return item
})
}
}
export default mutationssstat
state.js
// 定义全局的基础数据 定义播放器的基本状态
// vuex就是一个内存级别的存储,刷新的时候就会重置
// 而本地存储就会永久存于浏览器中
import { PLAY_MODE, FAVORITE_KEY } from '@/assets/js/constant'
const state = {
// 原始播放列表 歌曲列表
sequenceList: [],
// 真正的播放列表,因为有可能歌曲是顺序 逆序 倒序
playlist: [],
// 是否正在播放
playing: false,
// 播放模式 定义到常量文件中
playMode: PLAY_MODE.sequence,
// 当前播放的歌曲
currentIndex: 0,
// 播放器的状态 全屏还是收缩
fullScreen: false,
// 歌曲收藏列表 将歌曲添加到收藏就是添加到favoriteList
// 取消收藏就是将歌曲从favoriteList中移除 维护一个已收藏的列表
// 初始数据不写死 从缓存中获取
favoriteList: []
}
export default state
index.js
// createLogger 开发环境下可以查看状态
import { createStore, createLogger } from 'vuex'
import state from './state'
import mutations from './mutations'
// 引入文件中所有的方法
import * as getters from './getters'
import * as actions from './actions'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
state: state,
mutations: mutations,
// 可以同下方这样写 也可以与上面一样写
getters,
actions,
// vuex提供了严格模式,帮忙检测state修改是否是在提交mutation的时候,如果不是就会报错,
//开启时候会深度watch所以线上模式就关闭这个模式
strict: debug,
// 插件,如果线上模式就不用插件 线下模式使用插件
plugins: debug ? [createLogger()] : []
})
在setup中使用
import { useStore } from 'vuex'
setup() {
const store = useStore()
}
在外部文件中引用
比如有user-mini-slider.js,我们先在father.vue文件中
import useMiniSlider from './user-mini-slider'
setup() {
const { sliderWrapperRef } = useMiniSlider()
return{
sliderWrapperRef
}
}
在外部的js文件中:
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
export default function useMiniSlider() {
const store = useStore()
// 从state中获取数据 注意获取的数据要为响应式的,需要使用计算属性
const fullScreen = computed(() => store.state.fullScreen)
// 修改state中的属性的值,执行的是mutations.js文件中注册的方法
store.commit('setCurrentIndex', 99)
}
获取getters中的数据
const currentSong = computed(() => store.getters.currentSong)
比如currentSong是一个对象,那么要获取这个对象中的id属性的值,需要:
currentSong.value.id
注意 只要是用computed或者ref之类得到的都是proxy,那么要获取它的真实值,必须使用.value
使用action
在action.js中添加一个方法
export function removeSong ({ commit, state }, song) {
// 从playlist和seqlist中找到并删除
// 这里必须使用.slice()创建一个副本获取
const sequenceList = state.sequenceList.slice()
const playlist = state.playlist.slice()
// 找出song在两个列表中的索引
const sequenceIndex = findIndex(sequenceList, song)
const playIndex = findIndex(playlist, song)
sequenceList.splice(sequenceIndex, 1)
playlist.splice(playIndex, 1)
let currentIndex = state.currentIndex
commit('setSequenceList', sequenceList)
commit('setPlaylist', playlist)
commit('setCurrentIndex', currentIndex)
}
function findIndex (list, song) {
return list.findIndex((item) => {
return item.id === song.id
})
}
注意,如果从store中获取数组,然后直接对数组操作会报错!
const sequenceList = state.sequenceList
这样是错误的!,必须创建一个副本,然后对副本进行增加、删除操作,然后再commit提交到store中
const sequenceList = state.sequenceList.slice() // 正确
在vue文件的setup中使用:
<span class="delete" @click.stop="removeSong(song)">
<i class="icon-delete"></i>
</span>
import { useStore } from 'vuex'
setup() {
// 执行action中的函数
function removeSong (song) {
store.dispatch('removeSong', song)
}
return { removeSong }
}
watch组件中props中的值的变化
比如一个组件中,props的值是这样:
props: {
query: String,
showSinger: {
type: Boolean,
default: true
}
}
当我们要监听props里面的query的变化的时候,我们不能这样写:
watch(props.query, (newQuery) => {
... 这样写不对,
})
watch除了可以watch一个响应式变量,也可以watch一个getter函数,要像这样写:
watch(() => props.query, (newQuery) => {
这样才对,watch一个getter函数
})
使用router
首先在setup中引入useRouter,然后在使用:
import { useRouter } from 'vue-router'
setup(props, { emit }) {
const router = useRouter();
function selectSinger (singer) {
router.push({
path: `/search/${singer.mid}`
})
}
return {
selectSinger
}
}
其语法与this.$router.push等基本一致。
如果要获取路由中的参数,就要使用route了。
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
router.push({
name: 'search',
query: {
...route.query,
...query,
},
})
}
},
}
route 对象是一个响应式对象,所以它的任何属性都可以被监听,但你应该避免监听整个 route 对象。在大多数情况下,你应该直接监听你期望改变的参数。
import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'
export default {
setup() {
const route = useRoute()
const userData = ref()
// 当参数更改时获取用户信息
watch(
() => route.params.id,
async newId => {
userData.value = await fetchUser(newId)
}
)
},
}
问题快速定位
getters
store.getters数据不更新
Vuex 4+中的getters默认是非响应式的,这意味着它们不会跟踪依赖的变化。如果getter依赖于state的某个属性,那么当这个属性变化时,getter的结果不会自动更新。
如果你在组件中通过store.getters.xx来调用getter,但没有正确地使用Vue的响应式系统,那么可能会遇到不更新的问题。比如
模板:
<ul class="layer-ul">
<li v-for="(layer, lindex) in layersData"
:key="lindex"
>
....
</li>
</ul>
错误写法
import {useStore} from 'vuex';
const store = useStore();
const layerData = store.getters.layers; // 错误!这样不是响应式
return {
layerData
}
正确写法
import {useStore} from 'vuex';
import { computed } from 'vue';
const store = useStore();
const layersData = computed(() => store.getters.layersData);
return {
layerData
}
外部js文件提交mutations
比如mutations.js中有如下方法
const mutations = {
setLayers(state, layers) {
state.layers = layers;
},
}
在外部JS文件中提交
const mutations = {
setLayers(state, layers) {
state.layers = layers;
},
}
// 提交
import {useStore} from 'vuex';
store = useStore()
store.commit('setLayers', 数据);