一、vue3中setup函数的参数
# 有两个参数 props 和 context
## setup和props
1. props依然需要注册使用 props:['xx']
2. setup中用props接收传递进来的响应式属性 setup(props){}
3. 在执行setup函数的时候没有绑定this,所以在setup中没有this
4. setup中返回(return)的内容可以直接在视图中渲染
5. setup函数只有在组件第一次加载的时候才执行一次,当组件重新渲染不会再次执行
- 销毁后再重新加载属于第一次加载逻辑
6. 我们会把vue2中 `data /mehods /computed/watch/filters...`这些optionsAPI
- 全部聚合在setup函数中处理,且基于vue3中提供的各种api,实现函数式编程!!
## context有三个属性:
attrs: 所有的非prop的attribute
slots: 父组件传递进来的插槽
emit: 组件内部需要发出事件时用emit
二、vue3的各种响应式api
1.使用ref实现计数器功能
<template>
<div class="app">
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
// 默认定义的数据不是响应式数据
// let counter = 100;
let counter = ref(100);
// 普通方法直接return即可
const increment = () => {
counter.value++;
};
const decrement = () => {
counter.value--;
};
return {
counter,
increment,
decrement
}
}
}
</script>
2.setup定义数据
<template>
<div class="app">
<h2>message: {{ message }}</h2>
<button @click="changeMessage">修改message</button>
<p>--------------------------</p>
<h2>账号:{{ username }}</h2>
<h2>密码:{{ password }}</h2>
<button @click="changeAccount">修改账号</button>
<p>--------------------------</p>
<!-- <h2>当前计数reactive写法:{{ counter.counter }}</h2> -->
<h2>当前计数ref写法:{{ counter }}</h2>
<button @click="increment">+1</button>
<p>--------------------------</p>
<h2>当前计数-template表达式中修改counter:{{ counter }}</h2>
<!-- counter已经解包,不需要写.value -->
<button @click="increment">+1</button>
<button @click="counter++">+1</button>
<p>--------------------------</p>
<!-- 使用时不需要加.value,修改时需要加.value 才可以 -->
<h2>当前计数-浅层解包:{{ info.counter }}</h2>
<button @click="info.counter.value++">+1</button>
</div>
</template>
<script>
import { reactive, ref, toRefs } from 'vue';
export default {
setup() {
// 1.定义普通的数据,可以在template中正常使用
// 缺点:不是响应式的
let message = 'Hello World';
const changeMessage = () => {
// message不是响应式的,点击按钮,数据改变,视图不会渲染
message = '你好啊,Hezi';
}
// 2.定义响应式数据
/**
* 2.1 reactive函数:定义复杂的数据
* + 返回的是Proxy代理对象
* + 基于account.xxx去操作对应的状态
* + 基于ES6中的Proxy实现数据劫持
* + 尝试把其解构返回 return {...account} => return { username: 'wx',password: '123456'}这样不可以,里面没有劫持
* + 解决办法:可以基于 toRefs 函数,把account状态中的每一项都变为单独的 RefImpl 对象,在视图中可以直接用对象里面的属性值不用account.xxx了
* + toRef(account.password) 只处理单个的数据
* + toRefs(account) 多个非响应式变为响应式对象
*/
const account = reactive({
username: 'wx',
password: '123456'
});
// 点击会修改数据并渲染
// 比Object.defineProperty好用在于:对于数据或者并未初始化的对象成员,都可以随意修改值,而且具备响应式的效果
const changeAccount = () => {
account.username = 'Hezi';
}
// 2.2 ref函数:定义简单类型的数据,也可以定义复杂类型的数据
// 用reactive函数,视图中需要counter.counter才可以显示
// 如果传入一个基本值,会报一个警告
// const counter = reactive({
// counter: 0
// });
/**
* 基于ref(初始值)构建响应式数据(状态)
* + 返回 RefImpl对象
* + 具备value属性可以获取和设置状态值 RefImpl.value
* + 而且value属性进行了set和get数据劫持(Object.defineProperty)
* + 在模板视图中渲染的时候,我们无需 RefImpl.value,因为自动渲染的就是它的value值
*/
const counter = ref(0);
const increment = () => {
counter.value++;
}
// 3.ref是浅层解包
const info = {
counter
}
return {
message,
changeMessage,
// account, 使用的时候account.xxx
...toRefs(account),
changeAccount,
counter,
increment,
info
}
}
}
</script>
ref 和 reactive的应用场景
<template>
<div class="app">
<form>
账号:<input type="text" v-model="account.username"><br />
密码:<input type="password" v-model="account.password">
</form>
<form>
账号:<input type="text" v-model="username"><br />
密码:<input type="password" v-model="password">
</form>
</div>
</template>
<script>// @ts-nocheck
import { ref, reactive, onMounted } from 'vue'
export default {
setup() {
// 定义响应式数据 reactive / ref
// 强调:ref也可以定义复杂的数据
const info = ref({});
// 1.reactive的应用场景
// 1.1 条件一: reactive应用于本地的数据
// 1.2 条件二:多个数据之间是有关系的(聚合的数据,组合在一起有特定的作用)
const account = reactive({
username: "wx",
password: "123456"
});
const username = ref('wx');
const password = ref('123456');
// 2. ref的应用场景,其他的基本都用ref
// 2.1 定义本地的 一些简单的数据
const message = ref("Hello world");
// 2.2 定义从网络中获取的数据也是使用ref
const musics = ref([]);
onMounted(() => {
const serverMusics = ["海阔天空", "小苹果", "月光"];
musics.value = serverMusics;
})
return {
account,
username,
password,
musics
}
}
}
</script>
3.setup其他函数
单向数据流
<template>
<div class="app">
<h2>app:{{ info }}</h2>
<show-info :info="info" :roInfo="roInfo" @changeInfoName="changeInfoName"
@changeRoInfoName="changeRoInfoName"></show-info>
</div>
</template>
<script>
import { reactive, readonly, isProxy, isReactive } from 'vue';
import ShowInfo from './ShowInfo.vue';
export default {
components: {
ShowInfo
},
setup() {
// 本地定义多个数据,都需要传递给子组件
const info = reactive({
name: 'wx',
age: 18,
height: 1.88
});
const changeInfoName = (payload) => {
info.name = payload;
}
// 1.readonly
/**
* readonly创建的数据也是响应式的
* readonly会返回的Proxy代理对象不允许修改
* + roInfo对象是不允许被修改的
* + 当info被修改时,readonly返回的roInfo也会被修改
* + 但是不能去修改readonly返回的RoInfo
* 原理:readonly返回的对象的setter方法被劫持
*/
const roInfo = readonly(info);
const changeRoInfoName = (payload) => {
info.name = payload;
}
// 2.isProxy:检测对象是否由reactive 或 readonly创建的 Proxy
console.log(isProxy(roInfo));// true
// 3.isReactive:检测对象是否由reactive创建的响应式代理
// + 如果代理是readonly创建的,但包裹了由reactive创建的另一个代理,它也会返回true
const obj1 = reactive({ name: 'wx' });
const obj2 = readonly({ name: 'Hezi' });
const obj3 = readonly(obj1);
console.log(isReactive(obj1), isReactive(obj2)); //true false
console.log(isReactive(obj3));// true
// 4.isReadonly:检查对象是否由 readonly创建的只读代理
console.log(obj2);// true
// 5.toRaw 返回 reactive 或 readonly代理的原始对象(谨慎使用),调试
// const info = Proxy({});
// import { toRaw } from 'vue'
// toRaw(info);
// 6.shallowReactive:创建一个响应式代理,不对属性进行深层响应式转换
// 7.shallowReadonly:创建一个Proxy,深层属性可读,可写
return {
info,
changeInfoName,
roInfo,
changeRoInfoName
}
}
}
</script>
子组件:ShowInfo.vue
<template>
<div class="app">
<h2>showInfo:{{ info }}</h2>
<!--
子组件直接修改props
+ 可行的,但是不规范
+ 单向数据流(规范)
+ 子组件拿到的数据只能使用,不能修改
+ 如果要修改应该将事件传递出去让父组件修改
+ 因为这个数据在父组件中可能被多个子组件使用
-->
<!-- <button @click="info.name = 'kobe'">showInfo按钮:</button> -->
<!-- 正确的做法:符合单向数据流-->
<button @click="showInfoClick">showInfo按钮:</button>
<p>---------------------------------</p>
<h2>使用reandonly数据:{{ roInfo }}</h2>
<!-- 报警告: target is readonly -->
<!-- <button @click="roInfo.name = 'kobe'">showInfo按钮-readonly:</button> -->
<button @click="showInfoRoClick">showInfo按钮-readonly:</button>
</div>
</template>
<script>
export default {
props: {
// reactive data
info: {
type: Object,
default: () => {}
},
// readOnly data
roInfo: {
type: Object,
default: () => {}
},
},
emits:["changeInfoName", "changeRoInfoName"],
setup(props, { emit }){
const showInfoClick = () => {
emit('changeInfoName', 'kobe');
}
const showInfoRoClick = () =>{
emit('changeRoInfoName', 'Hezi');
}
return {
showInfoClick,
showInfoRoClick
}
}
}
</script>
4.ref其他函数补充
<template>
<div class="app">
<h2>info:{{ info.name }} - {{ info.age }}</h2>
<h2>单独显示:{{ name }}-{{ age }}-{{ height }}</h2>
<!-- age是响应式的 -->
<button @click="age++">修改age</button>
<!-- height是响应式的 -->
<button @click="height = 1.70">修改height</button>
</div>
</template>
<script>
import { reactive, toRefs, toRef, shallowRef, triggerRef } from 'vue';
export default {
setup() {
const info1 = reactive({
name: 'wx',
age: 18,
height: 1.80
});
// reactive被解构后会失去响应式
// toRefs:多个非响应式变为响应式对象
const { name, age } = toRefs(info1);
// toRef:只处理单个的数据
const height = toRef(info1, "height");
// unref:获取一个ref引用中的value
// => val = isRef(val) ? val.value : val
// isRef:是否是一个ref对象
// shallowRef:创建一个浅层的ref对象
// triggerRef:手动触发和shallowRef相关联的副作用
const info2 = shallowRef({ name: "Hezi" });
const changeInfo = () => {
info2.value.name = "wx"
// 手动触发
triggerRef(info2);
}
return {
info: info1,
name,
age,
height
}
}
}
</script>
5.setup中的computed
<template>
<div class="app">
<h2>{{ names.firstname + " " + names.lastName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ firstname + " " + lastName }}</h2>
<button @click="setFullname">设置fullname</button>
</div>
</template>
<script>
import { ref, reactive, computed, toRefs } from 'vue';
export default {
setup() {
/**
计算属性
+ 语法:computed([getter函数])
+ 返回ComputedRefImpl对象,和RefImpl对象类似,都是操作其value属性...
+ 这样获取的计算属性值是只读的,当我们尝试去修改的时候,报警告:computed value is readonly
*/
// 1.定义数据
const names = reactive({
firstname: 'kobe',
lastName: 'bryant'
})
// const fullName = computed(() => {
// return names.firstname + " " + names.lastName;
// })
// fullName.value= "He zi" computed value is readonly
const fullName = computed({
set: function (newValue) {
const tempNames = newValue.split(" ")
names.firstname = tempNames[0];
names.lastName = tempNames[1];
},
get: function () {
return names.firstname + " " + names.lastName;
}
})
const setFullname = () => {
fullName.value = "He zi"
}
const { firstname, lastName } = toRefs(names)
return {
names,
fullName,
firstname,
lastName,
setFullname
}
}
}
</script>
6.setup中使用ref引入元素组件
父组件:App.vue
<template>
<div class="app">
<h2 ref="titleRef">我是标题</h2>
<button ref="btnRef">按钮</button>
<p>---------------------</p>
<show-info ref="showInfoRef"></show-info>
<button @click="getElement">获取元素</button>
</div>
</template>
<script>
import { onMounted, ref } from 'vue';
import ShowInfo from './ShowInfo.vue';
export default {
components: {
ShowInfo
},
setup() {
const titleRef = ref()
const btnRef = ref()
const showInfoRef = ref()
// console.log(titleRef.value); undefined
const getElement = () => {
console.log(titleRef.value);//<h2>我是标题</h2>
console.log(btnRef.value);//<button>按钮</button>
console.log(showInfoRef.value);//Proxy {…} 组件实例
console.log(showInfoRef.value.showInfoFoo());//showInfo foo function 可以获取组件实例中的方法
}
onMounted(()=>{
console.log(titleRef.value);
})
return {
titleRef,
btnRef,
getElement,
showInfoRef
}
}
}
</script>
子组件:ShowInfo.vue
<template>
<div class="showInfo">
<h2>showInfo</h2>
</div>
</template>
<script>
export default {
setup() {
function showInfoFoo() {
console.log("showInfo foo function");
}
return {
showInfoFoo
}
}
}
</script>
7.setup中的Provide 和 inject
父组件
<template>
<div class="app">
<show-info></show-info>
</div>
</template>
<script>
import { ref, provide } from 'vue';
import ShowInfo from './ShowInfo.vue';
export default {
components: {
ShowInfo
},
setup() {
const name = ref('wx')
// 共享的是响应式数据时,父组件改变,子组件也会改变,反之则不行
provide("name", name)
provide("age", "18")
}
}
</script>
子组件
<template>
<div class="app">
showInfo:{{ name }}-{{ age }}-{{ height }}
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
// 如果用的是options api,需要手动解包,加.value
// composition api 自动解包
const name = inject("name")
const age = inject("age")
// 没有共享的时候给一个默认值
const height = inject("height", 1.80)
return {
name,
age,
height
}
}
}
</script>
8.setup中的侦听器
watch
<template>
<div class="app">
{{ message }}
<button @click="changeMessage">修改message</button>
<button @click="info.friend.name = 'Hezi'">修改info</button>
</div>
</template>
<script>
import { ref, watch, reactive } from 'vue';
export default {
setup() {
// 定义数据
const message = ref("Hello world")
const info = reactive({
name: 'wx',
age: 18,
friend: {
name: 'kobe'
}
})
// watch监听器:监听现有状态改变,触发对应函数执行,第一次不会执行
//监听ref某一个状态直接监听
watch(message, (newValue, oldValue) => {
console.log(newValue, oldValue);//Hello App Hello world
})
const changeMessage = () => {
message.value = 'Hello App'
}
// 监听reactive某一个状态要写成一个函数,监听整个reactive状态新值和老值是同一个引用
watch(info, (newValue, oldValue) => {
console.log(newValue, oldValue);
console.log(newValue === oldValue);//true
})
// watch默认深度侦听
watch(() => info.friend.name, (newValue, oldValue) => {
console.log(newValue, oldValue);// Hezi kobe
})
// 如果结构成一个普通的对象就要手动深度监听,返回的也是普通对象
watch(() => ({ ...info }), (newValue, oldValue) => {
console.log(newValue, oldValue);
}, {
immediate: true,
deep: true
})
// 第一次立即执行,监听单个源
watch(() => info.friend.name, (newValue, oldValue) => {
console.log(newValue, oldValue);// kobe undefined => Hezi kobe
}, {
immediate: true
})
return {
message,
info,
changeMessage
}
}
}
</script>
watchEffect
<template>
<div class="app">
<h2>当前计数:{{ counter }}-{{ name }}</h2>
<button @click="counter++">修改counter</button>
<button @click="name = 'Hezi'">修改name</button>
</div>
</template>
<script>
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const counter = ref(0)
const name = ref('wx')
/**
* watchEffect传入的函数默认会执行
* 在执行过程中,会自动收集依赖(依赖哪些响应式的数据)
* 只有在收集的依赖发生改变时,watchEffect传入的函数才会再次执行
*/
const stopWatch = watchEffect(() => {
// 修改counter或者name都会使得watchEffect函数执行
console.log('------', counter.value, name.value);
if (counter.value >= 10) {
// 停止监听
stopWatch();
}
})
return {
counter,
name
}
}
}
</script>
三、hooks练习
1.useCounter抽取
app页面:
<template>
<div class="app">
<div>app</div>
<p>-----------------</p>
<!-- 计数器 -->
<home></home>
<p>-----------------</p>
<about></about>
</div>
</template>
<script>
import Home from './views/Home.vue'
import About from './views/About.vue'
export default {
components: {
Home,
About
},
setup() {
}
}
</script>
about页面 和 home页面:
<template>
<div class="about">
<h2>about计数:{{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import useCounter from '../hooks/useCounter.js'
import useTitle from '../hooks/useTitle';
export default {
setup() {
// 修改标题
useTitle('关于')
return {
...useCounter()
}
}
}
</script>
useCounter.js
import { ref } from 'vue'
export default function useCounter() {
const counter = ref(0)
function increment() {
counter.value++;
}
function decrement() {
counter.value--;
}
return {
counter,
increment,
decrement
}
}
2.useTitle
app页面:
<template>
<div class="app">
<div>
app:
<button @click="changeTitle">修改title</button>
</div>
<p>----点击按钮显示 home or about 页面----</p>
<button @click="currentPage = 'home'">home</button>
<button @click="currentPage = 'about'">about</button>
<p>----显示页面----</p>
<component :is="currentPage"></component>
</div>
</template>
<script>
import { ref } from 'vue'
import Home from './views/Home.vue'
import About from './views/About.vue'
import useTitle from './hooks/useTitle'
export default {
components: {
Home,
About
},
setup() {
const currentPage = ref('home')
function changeTitle() {
useTitle("app title")
}
return {
currentPage,
changeTitle
}
}
}
</script>
home页面
<template>
<div class="home">
<h2>home计数:{{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<p>---在home页面点击按钮修改标题---</p>
<button @click="popularClick">首页-流行</button>
<button @click="hotClick">首页-热门</button>
<button @click="songClick">首页-歌单</button>
</div>
</template>
<script>
import useCounter from '../hooks/useCounter';
import useTitle from '../hooks/useTitle';
export default {
setup() {
// 修改标题
const title = useTitle('首页')
function popularClick(){
title.value="首页-流行"
}
function hotClick(){
title.value="首页-热门"
}
function songClick(){
title.value="首页-歌单"
}
return {
...useCounter(),
popularClick,
hotClick,
songClick
}
}
}
</script>
useTitle.js
import { watch, ref } from "vue"
export default function useTitle(titleValue) {
// document.title = title;
// 定义ref的引用数据
const title = ref(titleValue)
watch(title, (newValue, oldValue)=>{
document.title = newValue
}, {
immediate: true
})
// 返回ref的值
return title
}
四、setup语法糖
优点:
app页面
<script setup>
// 1.所有编写在顶层的代码和引入的对象或组件都是暴露给template使用
import { ref, onMounted } from 'vue'
import ShowMessage from './ShowMessage.vue'
// 2.定义响应式数据
const message = ref("hello World")
// 3.定义绑定的函数
function changeMessage() {
message.value = "你好,Hezi"
}
function messageBtnClick(payload) {
console.log(payload);//showMessage内部发生了点击
}
// 4.获取组件实例
const showMessageRef = ref()
onMounted(() => {
showMessageRef.value.foo() //foo function
})
</script>
<template>
<div class="app">
<div>message:{{ message }}</div>
<button @click="changeMessage">修改message</button>
<show-message name="wx" :age="18" @messageBtnClick="messageBtnClick" ref="showMessageRef"></show-message>
</div>
</template>
ShowMessage页面
<template>
<div class="showMessage">
<div>showMessage:{{ name }}-{{ age }}</div>
<button @click="showMessageBtnClick">showMessageBtn</button>
</div>
</template>
<script setup>
defineProps({
name: {
type: String,
default: ""
},
age: {
type: Number,
default: 0
}
})
const emits = defineEmits(["messageBtnClick"])
function showMessageBtnClick() {
emits("messageBtnClick", "showMessage内部发生了点击")
}
function foo() {
console.log('foo function');
}
// 暴露函数
defineExpose({
foo
})
</script>