setup執行的時機
- 在beforeCreate之前執行(一次),此時組件對象還沒創建;
- this是undefined,不能通過this來訪問data/computed/methods/props;
- 其實所有的composition API相關回調函數中也都不可以;
setup的返回值
- 一般都返回一個對象:為模板提供數據,也就是模板中可以直接使用此對象中的所有屬性/方法
- 返回對象中的屬性會與data函數返回對象合並成為組件對象的屬性
- 返回對象中的方法會與methods中的方法合並成功組件對象的方法
- 如果有重名,setup優先
- 注意:一般不要混合使用:methods中可以訪問setup提供的屬性和方法,但在setup方法中不能訪問data和methods;setup不能是async函數:因為返回值不再是return的對象,而不是promise,模板看不到return對象中的屬性數據
setup參數
- setup(props,context)/setup(props,{attrs,slots,emit})
- props:包含props配置聲明且傳入了所有屬性的對象
- attrs:包含沒有在props配置中聲明的屬性的對象,相當於this.$attrs
- slots:包含所有傳入的插槽內容的對象,相當於this.$slots
- emit:用來分發自定義事件的函數,相當於this.$emit
演示代碼
- 父組件
<template>
<h2>App</h2>
<p>msg: {{msg}}</p>
<button @click="fn('--')">更新</button>
<child :msg="msg" msg2="cba" @fn="fn"/>
</template>
<script lang="ts">
import {
reactive,
ref,
} from 'vue'
import child from './child.vue'
export default {
components: {
child
},
setup () {
const msg = ref('abc')
function fn (content: string) {
msg.value += content
}
return {
msg,
fn
}
}
}
</script>
- 子組件
<template>
<div>
<h3>{{n}}</h3>
<h3>{{m}}</h3>
<h3>msg: {{msg}}</h3>
<h3>msg2: {{$attrs.msg2}}</h3>
<slot name="xxx"></slot>
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import {
ref,
defineComponent
} from 'vue'
export default defineComponent({
name: 'child',
props: ['msg'],
emits: ['fn'], // 可選的, 聲明了更利於程序員閱讀, 且可以對分發的事件數據進行校驗
data () {
console.log('data', this)
return {
// n: 1
}
},
beforeCreate () {
console.log('beforeCreate', this)
},
methods: {
// update () {
// this.n++
// this.m++
// }
},
// setup (props, context) {
setup (props, {attrs, emit, slots}) {
console.log('setup', this)
console.log(props.msg, attrs.msg2, slots, emit)
const m = ref(2)
const n = ref(3)
function update () {
// console.log('--', this)
// this.n += 2
// this.m += 2
m.value += 2
n.value += 2
// 分發自定義事件
emit('fn', '++')
}
return {
m,
n,
update,
}
},
})
</script>
reactive與ref細節
- 是vue3的composition API中最重要的響應式API
- ref用來處理基本類型的數據,reactive用來處理對象(遞歸深度響應式)
- 如果用ref對象/數組,內部會自動將對象/數組轉換為reactive的代理對象
- ref內部:通過給value屬性添加getter/setter來實現對數據的劫持
- reactive內部:通過使用Proxy來實現對對象內部所有數據的劫持,並通過Reflect操作對象內部數據
- ref的數據操作:在js中要.value,在模板中不需要(內部解析模板時會自動添加.value)
<template>
<h2>App</h2>
<p>m1: {{m1}}</p>
<p>m2: {{m2}}</p>
<p>m3: {{m3}}</p>
<button @click="update">更新</button>
</template>
<script lang="ts">
import {
reactive,
ref
} from 'vue'
export default {
setup () {
const m1 = ref('abc')
const m2 = reactive({x: 1, y: {z: 'abc'}})
// 使用ref處理對象 ==> 對象會被自動reactive為proxy對象
const m3 = ref({a1: 2, a2: {a3: 'abc'}})
console.log(m1, m2, m3)
console.log(m3.value.a2) // 也是一個proxy對象
function update() {
m1.value += '--'
m2.x += 1
m2.y.z += '++'
m3.value = {a1: 3, a2: {a3: 'abc---'}}
m3.value.a2.a3 += '==' // reactive對對象進行了深度數據劫持
console.log(m3.value.a2)
}
return {
m1,
m2,
m3,
update
}
}
}
</script>
計算屬性和監視屬性
<template>
<h2>App</h2>
fistName: <input v-model="user.firstName"/><br>
lastName: <input v-model="user.lastName"/><br>
fullName1: <input v-model="fullName1"/><br>
fullName2: <input v-model="fullName2"><br>
fullName3: <input v-model="fullName3"><br>
</template>
<script lang="ts">
/*
計算屬性與監視
1. computed函數:
與computed配置功能一致
只有getter
有getter和setter
2. watch函數
與watch配置功能一致
監視指定的一個或多個響應式數據, 一旦數據變化, 就自動執行監視回調
默認初始時不執行回調, 但可以通過配置immediate為true, 來指定初始時立即執行第一次
通過配置deep為true, 來指定深度監視
3. watchEffect函數
不用直接指定要監視的數據, 回調函數中使用的哪些響應式數據就監視哪些響應式數據
默認初始時就會執行第一次, 從而可以收集需要監視的數據
監視數據發生變化時回調
*/
import {
reactive,
ref,
computed,
watch,
watchEffect
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
// 只有getter的計算屬性
const fullName1 = computed(() => {
console.log('fullName1')
return user.firstName + '-' + user.lastName
})
// 有getter與setter的計算屬性
const fullName2 = computed({
get () {
console.log('fullName2 get')
return user.firstName + '-' + user.lastName
},
set (value: string) {
console.log('fullName2 set')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
}
})
const fullName3 = ref('')
/*
watchEffect: 監視所有回調中使用的數據
*/
/*
watchEffect(() => {
console.log('watchEffect')
fullName3.value = user.firstName + '-' + user.lastName
})
*/
/*
使用watch的2個特性:
深度監視
初始化立即執行
*/
watch(user, () => {
fullName3.value = user.firstName + '-' + user.lastName
}, {
immediate: true, // 是否初始化立即執行一次, 默認是false
deep: true, // 是否是深度監視, 默認是false
})
/*
watch一個數據
默認在數據發生改變時執行回調
*/
watch(fullName3, (value) => {
console.log('watch')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
})
/*
watch多個數據:
使用數組來指定
如果是ref對象, 直接指定
如果是reactive對象中的屬性, 必須通過函數來指定
*/
watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
console.log('監視多個數據', values)
})
return {
user,
fullName1,
fullName2,
fullName3
}
}
}
</script>
生命周期
與2.x版本生命周期相對應的組合API
。beforeCreate=>使用setup()
。create=>使用setup()
。beforeMount=>onBeforeMount
。mounted=>onMounted
。beforeUpdate=>onBeforeUpdate
。updated=>onUpdated
。beforeDestroy=>onBeforeUnmount
。destroyed=>onUnmounted
。errorCaptured=>onErrorCaptured
toRefs
- 把一個響應式對象轉換成普通對象,該普通對象的每個property都是一個ref
- 應用:當從合成函數返回響應式對象時,toRefs非常有用,這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
- 問題:reactive對象取出的所有屬性值都是非響應式的
- 解決:利用toRefs可以將一個響應式reactive對象的所有原始屬性轉換為響應式的ref屬性
<template>
<h2>App</h2>
<h3>foo: {{foo}}</h3>
<h3>bar: {{bar}}</h3>
<h3>foo2: {{foo2}}</h3>
<h3>bar2: {{bar2}}</h3>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
將響應式對象中所有屬性包裝為ref對象, 並返回包含這些ref對象的普通對象
應用: 當從合成函數返回響應式對象時,toRefs 非常有用,
這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
*/
export default {
setup () {
const state = reactive({
foo: 'a',
bar: 'b',
})
const stateAsRefs = toRefs(state)
setTimeout(() => {
state.foo += '++'
state.bar += '++'
}, 2000);
const {foo2, bar2} = useReatureX()
return {
// ...state,
...stateAsRefs,
foo2,
bar2
}
},
}
function useReatureX() {
const state = reactive({
foo2: 'a',
bar2: 'b',
})
setTimeout(() => {
state.foo2 += '++'
state.bar2 += '++'
}, 2000);
return toRefs(state)
}
</script>
ref獲取元素
- 利用ref函數獲取組件中的標簽元素
- 功能需求:讓輸入框自動獲取焦點
<script lang="ts">
import { onMounted, ref } from 'vue'
/*
ref獲取元素: 利用ref函數獲取組件中的標簽元素
功能需求: 讓輸入框自動獲取焦點
*/
export default {
setup() {
const inputRef = ref<HTMLElement|null>(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return {
inputRef
}
},
}
</script>
自定義hook函數
與vue2中mixins作用差不多,使用方法是直接在頁面中引用即可
import { ref,onMounted } from "vue";
export default function() {
const x = ref(0);
const y = ref(1);
const clickHandler = (event:MouseEvent) => {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('click',clickHandler);
})
return {
x,
y
}
}
toRefs的使用
可以將響應式的對象轉換成一個個ref變量
const person = reactive({
name: 'guozhiqiang',
age: '20',
sex: '男',
tag: '帥哥'
})
const a = toRefs(person);//返回一個ref變量組成的數組
a.tag.value = '大帥哥';
console.log(a.tag.value);//大帥哥
console.log(person.tag.value)//大帥哥
ref獲取標簽元素
const InputText = ref<HTMLElement | null>();//獲取ref為InputText的元素
shallowReactive與shallowRef
淺拷貝與深拷貝的區別,shallowReactive只有對象第一層的數據是響應式的,可以引起視圖更新
shallowRef其實就是特殊的shallowReactive,也是只監聽第一層數據
這兩個適用於層數較深的對象但只需要對象第一層需要響應式的需求,可以減少瀏覽器開銷
const person = shallowReactive({
name: 'guozhiqiang',
age: '20',
sex: '男',
tag: '帥哥',
job: {
name: 'web 開發工程師'
}
})
person.name = 'shuaige';//數據更新時頁面會變化
person.job.name = 'aa';//數據更新時頁面不會變化
//shallowRef生成非遞歸響應數據,只監聽第一層數據的變化
let state= shallowRef({
gf: {
f: {
s: {
d: 4
},
c: 3
},
b: 2
},
a: 1
})
readOnly 和 shallowReadonly
用於設置數據只讀,readonly和shallowReadonly的區別就是shallowReadonly只有第一層數據只讀,其他數據可改變
const person = readonly({
name: 'guozhiqiang',
age: '20',
sex: '男',
job: {
name: 'web 開發工程師'
}
})
person.name = 'a'//錯誤,目標代理對象為只讀
const person2 = shallowreadonly({
name: 'guozhiqiang',
age: '20',
sex: '男',
job: {
name: 'web 開發工程師'
}
})
person.name = 'a'//錯誤,只有第一層數據為只讀
person.job.name = 'a'//可以
toRaw 和 markRaw
- toRaw:
作用:將一個由reactive生成的響應式對象轉為普通對象。
使用場景:用於讀取響應式對象對應的普通對象,對這個普通對象的所有操作,不會引起頁面更新。
- markRaw:
作用:標記一個對象,使其永遠不會再成為響應式對象。
應用場景:
有些值不應被設置為響應式的,例如復雜的第三方類庫等。
當渲染具有不可變數據源的大列表時,跳過響應式轉換可以提高性能。
let data = {
name: "張三",
age: 18,
likeFood: {
fruits: {
apple: "蘋果",
},
},
}
// 將普通對象變為響應對象
let person = reactive(data);
// 將響應對象變為普通對象
let p = toRaw(person)
//標記p,永遠不能成為響應對象
markRaw(p)
toRef的使用
ref:是復制,修改響應式數據不會影響以前的數據,數據發生變化,界面就會自動更新。
toRef:是引用,修改響應式數據會影響以前的數據,數據發生變化,界面就會不會更新。
應用場景
如果想讓響應式數據和以前的數據關聯起來,並且更新響應式數據之后還不想更新UI,那么就可以使用toRef
setup() {
const state = reactive({
age:5,
money:100
})
//把響應式數據state對象中的某個屬性age變成了ref對象了
const age = toRef(state,'age')
//把響應式對象中的某個屬性使用了ref進行包裝,變成了一個ref對象
const money = ref(state.money)
}
customRef的使用
自定義hook防抖的函數,value傳入的數據,將來數據的類型不確定,所以,用泛型delay防抖的間隔事件,默認是200毫秒
function userDebounceRef<T>(value: T,delay = 200){
let timeOutId:number
return customRef((track,trigger)=>{
return {
get() {
//告訴Vue追蹤數據
track()
return value
}
set(newValue:T) {
//清除定時器
cleatTimeout(timeOutId)
//開啟定時器
timeOutId = setTimeout(()=>{
value = newVlaue
//告訴Vue更新界面
trigger()
},delay)
}
}
})
export default defineComponent({
name:'App',
setup() {
const keyword = userDebounceRef('abs',500)
}
return {
keyword,
}
})
Provide 和 inject實現祖孫級組件通信
在setup中使用
provide('color','red');
const w = inject('color');
響應式數據的判斷
//isRef: 檢查一個值是否為一個ref對象
console.log(isRef({}))
//isReactive: 檢查一個值是否為一個reactive對象
console.log(isReactive({}))
//isReadonly: 檢查一個值是否為一個readonly對象
console.log(isReadonly({}))
//isProxy: 檢查一個值是否為reactive和readonly代理的對象
console.log(isProxy({}))
手寫組合api
shallowReactive 和 Reactive
const reactiveHandler = {
get(target: any,prop: any):any {
if(prop === 'is_reactive') return true;
const result = Reflect.get(target,prop);
console.log('攔截了讀取數據',prop,target);
return result;
},
set(target: any,prop: any,val:any):any {
const result = Reflect.set(target,prop,val);
console.log('攔截了讀取數據',prop,target);
return result;
},
deleteProperty(target: any, prop: any):any {
const result = Reflect.deleteProperty(target,prop);
console.log('攔截了刪除屬性',prop,target);
return result;
}
}
function shallowReactive<T>(target:T):T {
if(target && typeof target === 'object') {
console.log(1);
return new Proxy(target,reactiveHandler);
} else {
return target;
}
}
function reactive<T>(target: T):T {
if(target && typeof target === 'object') {
if(target instanceof Array) {
target.forEach((item,index) => {
target[index] = reactive(item);
})
} else {
Object.keys(target).forEach(key => {
target[key] = reactive(target[key]);
})
}
return new Proxy(target,reactiveHandler);
} else {
return target;
}
}
export {
shallowReactive,
reactive
}
shallowReadonly 和 readonly
const readonlyHandler = {
get(target: any,prop: any):any {
if(prop ==== 'is_readonly') return true;
const result = Reflect.get(target,prop);
console.log('攔截了讀取數據',prop,target);
return result;
},
set(target: any,prop: any,val:any):any {
console.log('只讀不能設置屬性',prop,target);
return true;
},
deleteProperty(target: any, prop: any):any {
console.log('只讀不能刪除屬性',prop,target);
return true;
}
}
function shallowReadonly<T>(target:T):T {
if(target && typeof target === 'object') {
return new Proxy(target,readonlyHandler);
} else {
return target;
}
}
function readonly<T>(target: T):T {
if(target && typeof target === 'object') {
if(target instanceof Array) {
target.forEach((item,index) => {
target[index] = readonly(item);
})
} else {
Object.keys(target).forEach(key => {
target[key] = readonly(target[key]);
})
}
return new Proxy(target,readonlyHandler);
} else {
return target;
}
}
export {
shallowReadonly,
readonly
}
shallowRef 和 ref
function shallowRef(target) {
return {
is_shallowRef: true,
_value: target,
get value(){
console.log('劫持到了讀取');
return this._value
},
set value(val) {
console.log('劫持到了設置')
this._value = val;
}
}
}
function ref(target) {
target = reactive(target);
return {
isRef: true,
_value: target,
get value(){
console.log('劫持到了讀取');
return this._value
},
set value(val) {
console.log('劫持到了設置')
this._value = val;
}
}
}
export {
shallowRef,
ref
}
isRef 和 isReactive 和 isReadonly
function isRef(target) {
return target&&target.isRef;
}
function isReactive(target) {
return target&&target.isReactive;
}
function isReadonly(target) {
return target&&target.isReadonly;
}
export {
isReactive,
isRef,
isReadonly
}
Fragment(片斷)
- 在vue2中,組件必須有一個根標簽
- 在vue3中, 組件可以沒有根標簽,內部會將多個標簽包含在一個Fragment標簽中
- 減少標簽層級,減小內存占用
<Fragment>
<div>111</div>
<div>222</div>
</Fragment>
Teleport(瞬移)
- Teleport提供了一種干凈的方法,讓組件的html在父組件界面外的特定標簽插入顯示
index.html
中
<div id="app"></div>
<div id="teleport-target"></div>
<script type="module" src="/src/main.js"></script>
src/components/HelloWorld.vue
中,添加如下,留意 to 屬性跟上面的 id 選擇器一致
<button @click="showToast" class="btn">打開 toast</button>
<!-- to 屬性就是目標位置 -->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一個 Toast 文案</div>
</div>
</teleport>
import { ref } from 'vue';
export default {
setup() {
// toast 的封裝
const visible = ref(false);
let timer;
const showToast = () => {
visible.value = true;
clearTimeout(timer);
timer = setTimeout(() => {
visible.value = false;
}, 2000);
}
return {
visible,
showToast
}
}
}
效果如下
-
可以看到,我們使用 teleport 組件,通過 to 屬性,指定該組件渲染的位置與
<div id="app"></div>
同級,也就是在 body 下,但是 teleport 的狀態 visible 又是完全由內部 Vue 組件控制 -
與 Vue components 一起使用 —— modal
-
如果
包含 Vue 組件,則它仍將是 父組件的邏輯子組件 -
接下來我們以一個 modal 組件為例
<div id="app"></div>
<div id="teleport-target"></div>
+ <div id="modal-container"></div>
<script type="module" src="/src/main.js"></script>
<teleport to="#modal-container">
<!-- use the modal component, pass in the prop -->
<modal :show="showModal" @close="showModal = false">
<template #header>
<h3>custom header</h3>
</template>
</modal>
</teleport>
- JS 核心代碼如下:
import { ref } from 'vue';
import Modal from './Modal.vue';
export default {
components: {
Modal
},
setup() {
// modal 的封裝
const showModal = ref(false);
return {
showModal
}
}
}
-
在這種情況下,即使在不同的地方渲染 Modal,它仍將是當前組件(調用 Modal 的組件)的子級,並將從中接收 show prop
-
這也意味着來自父組件的注入按預期工作,並且子組件將嵌套在 Vue Devtools 中的父組件之下,而不是放在實際內容移動到的位置
-
看實際效果以及在 Vue Devtool 中