快速開始
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
- 腳手架 vue-cli
npm install -g @vue/cli # OR yarn global add @vue/cli
vue create hello-vue3
# select vue 3 preset
vite
Vite,一個基于瀏覽器原生 ES imports 的開發服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,完全跳過了打包這個概念,服務器隨起隨用。同時不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會隨著模塊增多而變慢。針對生產環境則可以把同一份代碼用 rollup 打包。雖然現在還比較粗糙,但這個方向我覺得是有潛力的,做得好可以徹底解決改一行代碼等半天熱更新的問題。
https://juejin.cn/post/6928175048163491848
簡陋的實現 vite http://www.lxweimin.com/p/88cad1a63faf
new Vue()與createApp() 創建實例
- vue2
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')
- vue3
import { createApp } from 'vue';
import App from './App.vue'
createApp(App).mount('#app')
一個新的全局 API:createApp
調用 createApp 返回一個應用實例,這是 Vue 3 中的新概念
應用實例暴露當前全局 API 的子集,經驗法則是,任何全局改變 Vue 行為的 API 現在都會移動到應用實例上,以下是當前全局 API 及其相應實例 API 的表
2.x 全局 API | 3.x 實例 API (app ) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | removed |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Fragment
vue3不會再像vue2一樣需要手動添加一個根結點
vue2中 如果你創建一個Vue組件,那么它只能有一個根節點。 這意味著不能創建這樣的組件:
<template>
<div>Hello</div>
<div>World</div>
</template>
原因是代表任何Vue組件的Vue實例需要綁定到一個單一的DOM元素中。唯一可以創建一個具有多個DOM節點的組件的方法就是創建一個沒有底層Vue實例的功能組件。 結果發現React社區也遇到了同樣的問題。他們想出的解決方案是一個名為 Fragment 的虛擬元素。它看起來差不多是這樣的:
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
盡管Fragment看起來像一個普通的DOM元素,但它是虛擬的,根本不會在DOM樹中呈現。這樣我們可以將組件功能綁定到一個單一的元素中,而不需要創建一個多余的DOM節點。 目前你可以在Vue 2中使用vue-fragments庫來使用Fragments,而在Vue 3中,你將會在開箱即用!
Teleport
Vue 鼓勵我們通過將 UI 和相關行為封裝到組件中來構建 UI。我們可以將它們嵌套在另一個內部,以構建一個組成應用程序 UI 的樹。
然而,有時組件模板的一部分邏輯上屬于該組件,而從技術角度來看,最好將模板的這一部分移動到 DOM 中 Vue app 之外的其他位置。
一個常見的場景是創建一個包含全屏模式的組件。在大多數情況下,你希望模態的邏輯存在于組件中,但是模態的定位很快就很難通過 CSS 來解決,或者需要更改組件組合。
比如下面:
html
<body>
<div style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<modal-button></modal-button>
</div>
</div>
</body>
modal-button
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個父節點下呈現 HTML,而不必求助于全局狀態或將其拆分為兩個組件。
因此,一旦我們單擊按鈕打開模式,Vue 將正確地將模態內容渲染為 body 標簽的子級。(解決了fixed彈窗中 有用出現 全屏的彈窗這種問題)
v-model 雙向綁定
綁定響應式的props屬性寫法
父
<text-document
v-bind:title="title"
v-on:update:title="title = $event"
></text-document>
子
this.$emit('update:title', newTitle)
vue2 使用.sync修飾符來簡化父組件的綁定
<text-document v-bind:title.sync="doc.title"></text-document>
vue3中則將.sync封裝到了v-model之中
<text-document v-model:title="doc.title" />
多個 v-model 綁定
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
const app = Vue.createApp({})
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})
響應式
vue2
Object.defineProperty(obj, prop, descriptor)
vue3
new Proxy(target, handler);
組合式API 與 選項式
Vue3 是向下兼容 Vue2 API 的,但是 Vue3 中提供了一種全新的 Composition API
一個大型組件,有很多關注點,比如一個功能要實現a, 會在data、computed、methods、watch加入代碼。 后面增加功能b, 又會在data、computed、methods、watch中加入代碼。這樣一直加下去。 就很有很多邏輯關注點;
為了修改功能a,我們可能要從data ,跳轉到methods,然后又跳轉到其他methods,computed等,這樣增加了理解和維護難度。
如果我們能夠將與同一個邏輯關注點相關的代碼配置在一起會更好。而這正是組合式 API 使我們能夠做到的。
組合式 API 基礎
既然我們知道了為什么,我們就可以知道怎么做。為了開始使用組合式 API,我們首先需要一個可以實際使用它的地方。在 Vue 組件中,我們將此位置稱為 setup
。
我們可以在外面建了多個邏輯 a.js b.js c.js 里面實現不同的邏輯(data,methods, watch, computed,mounted, updated, provide, inject等都在各自的js總完成), 然后再setup 中引入對應js執行就行。
name: 'App',
components: {
HelloWorld
},
setup(props, context){
console.log('props', props);
console.log('context', context);
}
}
執行時機
setup(props, context) {
console.log('setup');
},
beforeCreate(){
console.log('beforeCreate');
},
created(){
console.log('created');
}
可以看出 是在beforeCreate之前執行。執行 setup 時,組件實例尚未被創建,所以不能使用this訪問實例。(methods這些也就不能使用了)
setup 參數
- props
- context
Props
setup 函數中的第一個參數是 props。正如在一個標準組件中所期望的那樣,setup 函數中的 props 是響應式的,當傳入新的 prop 時,它將被更新。
但是,因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性。
上下文
傳遞給 setup 函數的第二個參數是 context。context 是一個普通的 JavaScript 對象,它暴露三個組件的 property:
export default {
setup(props, context) {
// Attribute (非響應式對象) 沒用props傳遞的屬性
console.log(context.attrs)
// 插槽 (非響應式對象)
console.log(context.slots)
// 觸發事件 (方法)
console.log(context.emit)
// 暴露
console.log(context.expose)
}
}
setup 生命周期鉤子
選項式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
這些函數接受一個回調函數,當鉤子被組件調用時將會被執行:
// MyBook.vue
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
響應性基礎
聲明響應式狀態
要為 JavaScript 對象創建響應式狀態,可以使用 reactive 方法:
import { reactive } from 'vue'
// 響應式狀態
const state = reactive({
count: 0
})
return{state}
創建獨立的響應式值作為 refs
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
return { count }
template中 不用 使用value
<div>{{count}}</div>
訪問響應式對象
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
return {state, count }
toRef
可以用來為源響應式對象上的 property 性創建一個 ref
。然后可以將 ref 傳遞出去,從而保持對其源 property 的響應式連接
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
響應式狀態解構 toRefs
將響應式對象轉換為普通對象,其中結果對象的每個 property 都是指向原始對象相應 property 的ref
。
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
// ref 和 原始property “鏈接”
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
computed
使用 getter 函數,并為從 getter 返回的值返回一個不變的響應式 ref 對象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
watchEffect
為了根據響應式狀態自動應用和重新應用副作用,我們可以使用 watchEffect 方法。它立即執行
傳入的一個函數,同時響應式追蹤其依賴,并在其依賴變更時重新運行該函數
。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
watchEffect停止監聽
當 watchEffect
在組件的 setup() 函數或生命周期鉤子被調用時,偵聽器會被鏈接到該組件的生命周期,并在組件卸載時自動停止。
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
watchEffect 清除副作用
有時副作用函數會執行一些異步的副作用,這些響應需要在其失效時清除 (即完成之前狀態已改變了) 。所以偵聽副作用傳入的函數可以接收一個 onInvalidate 函數作入參,用來注冊清理失效時的回調。當以下情況發生時,這個失效回調會被觸發:
- 副作用即將重新執行時
- 偵聽器被停止 (如果在 setup() 或生命周期鉤子函數中使用了 watchEffect,則在組件卸載時)
比如: 假設我們現在用一個用戶ID去查詢用戶的詳情信息,然后我們監聽了這個用戶ID, 當用戶ID 改變的時候我們就會去發起一次請求,但是如果在請求數據的過程中,我們的用戶ID發生了多次變化,那么我們就會發起多次請求,而最后一次返回的數據將會覆蓋掉我們之前返回的所有用戶詳情。這不僅會導致資源浪費, watchEffect 我們就可以做到
<template>
<div> count:{{count}}</div>
<div @click="fn">click</div>
</template>
<script>
import { watchEffect, ref } from 'vue'
export default {
setup() {
const count = ref(2)
watchEffect((onInvalidate) => {
console.log(count.value, '副作用1111')
const token = setTimeout(() => {
console.log(count.value, '副作用22222')
// 發送請求
}, 4000)
onInvalidate(() => {
// 4 秒之內改變 清除副作用
// token 是 上一下 watchEffect 中 返回的token
clearTimeout(token)
})
})
function fn() {
count.value++
}
return {
fn,
count,
}
},
}
</script>
用戶 點擊4秒內 再次點擊 會取消上一次的 請求 ,如果4秒內 沒有點擊,再發送請求
副作用刷新時機
Vue 的響應性系統會緩存副作用函數,并異步地刷新它們,這樣可以避免同一個“tick” 中多個狀態改變導致的不必要的重復調用。在核心的具體實現中,組件的 update 函數也是一個被偵聽的副作用。當一個用戶定義的副作用函數進入隊列時,默認情況下,會在所有的組件 update 前執行;
flush: post; 在組件 update之后執行
// 在組件更新后觸發,這樣你就可以訪問更新的 DOM。
// 注意:這也將推遲副作用的初始運行,直到組件的首次渲染完成。
const count = ref(0)
watchEffect(
() => {
console.log('watchEffect', count.value)
},
{
// flush: 'pre',
// flush: 'post',
// flush: 'sync',
}
)
onBeforeUpdate(() => {
console.log('組件更新')
})
setTimeout(() => {
count.value++
}, 1000)
return {
count,
}
},
默認 pre ; watchEffect 比 組件更新 先打印
post: watchEffect 比 組件更新 后打印
sync 強制效果始終同步觸發, 然而這時低效的,很少需要
watch
watch
API 完全等同于組件偵聽器 property。watch
需要偵聽特定的數據源,并在回調函數中執行副作用。默認情況下,它也是惰性的,即只有當被偵聽的源發生變化時才執行回調
。
與 watchEffect 比較,watch
允許我們:
- 懶執行副作用;
- 更具體地說明什么狀態應該觸發偵聽器重新運行;
- 訪問偵聽狀態變化前后的值
// 直接偵聽ref
const count = ref(0)
watch(count, (count, prevCount) => {
console.log('watch1', count, prevCount)
})
setTimeout(() => {
count.value = 22
}, 2000)
// 直接監聽 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
console.log('watch2', count, prevCount)
}
)
setTimeout(() => {
state.count = 33
}, 2000)
// 監聽多個數據源
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // logs: ["John",""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
// 監聽響應式對象
const numbers = reactive([1, 2, 3, 4])
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers)
}
)
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]
// immediate deep
const state = reactive({ count: 0, obj: {name: 'zs'} })
watch(
() => state,
(count, prevCount) => {
console.log('watch2', state.obj.name)
},
{
immediate: true, // 初始 立即執行
deep: true //深 監聽 沒有 deep 不會答應 watch2
}
)
setTimeout(() => {
state.obj.name = 'ls'
}, 2000)
watchEffect與watch 區別
- watchEffect 不需要指定監聽的屬性,他會
自動收集依賴
, 只要我們回調中引用到了響應式的屬性
, 就達到了監聽效果,而 watch 只能監聽指定的屬性
而做出變更(v3開始可以同時指定多個)。 - watch可以獲取到
新值與舊值
(更新前的值),而 watchEffect 是拿不到的。 - watchEffect如果存在的話,在
組件初始化的時候就會執行一次
用以收集依賴(與computed同理),而后收集到的依賴發生變化,這個回調才會再次執行,而 watch 不需要,因為他一開始就指定了依賴
。 - watchEffect會
返回一個用于停止這個監聽的函數
提供/注入
我們也可以在組合式 API 中使用 provide/inject。兩者都只能在當前活動實例的 setup()
期間調用。
父
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation) // 注入修改方法
}
}
</script>
子
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
其他響應式API
readonly
接受一個對象 (響應式或純對象) 或 ref 并返回原始對象的只讀代理。只讀代理是深層的:任何被訪問的嵌套 property 也是只讀的。
isProxy
檢查對象是否是由 reactive 或 readonly 創建的 proxy。
isReactive
檢查對象是否是由 reactive 創建的響應式代理。
如果該代理是 readonly 創建的,但包裹了由 reactive 創建的另一個代理,它也會返回 true。
isReadonly
檢查對象是否是由 readonly 創建的只讀代理。
toRaw
返回 reactive 或 readonly 代理的原始對象。這是一個“逃生艙”,可用于臨時讀取數據而無需承擔代理訪問/跟蹤的開銷,也可用于寫入數據而避免觸發更改。不建議保留對原始對象的持久引用。請謹慎使用。
markRaw
標記一個對象,使其永遠不會轉換為 proxy。返回對象本身。
shallowReactive
創建一個響應式代理,它跟蹤其自身 property 的響應性,但不執行嵌套對象的深層響應式轉換 (暴露原始值)。
shallowReadonly
創建一個 proxy,使其自身的 property 為只讀,但不執行嵌套對象的深度只讀轉換 (暴露原始值)。
unref
如果參數是一個 ref,則返回內部值,否則返回參數本身。這是 val = isRef(val) ? val.value : val 的語法糖函數。
isRef
檢查值是否為一個 ref 對象。
customRef
創建一個自定義的 ref,并對其依賴項跟蹤和更新觸發進行顯式控制。它需要一個工廠函數,該函數接收 track 和 trigger 函數作為參數,并且應該返回一個帶有 get 和 set 的對象。
<template>
<input v-model="text" />
{{text}}
</template>
<script>
import {customRef} from 'vue'
export default {
setup() {
function useDebouncedRef(value, delay = 500) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}
})
}
return {
text: useDebouncedRef('hello'),
}
},
}
</script>
shallowRef
創建一個跟蹤自身 .value 變化的 ref,但不會使其值也變成響應式的。
triggerRef
手動執行與 shallowRef 關聯的任何副作用。
<script>
import { shallowRef, ref, watchEffect, triggerRef } from 'vue'
export default {
setup() {
// 會打印 兩次
// const shallow = ref({
// greet: 'Hello, world',
// })
// watchEffect(() => {
// console.log(shallow.value.greet)
// })
// shallow.value.greet = 'Hello, universe'
// 在triggerRef后才會打印第二次
const shallow = shallowRef({
greet: 'Hello, world',
})
watchEffect(() => {
console.log(shallow.value.greet)
})
shallow.value.greet = 'Hello, universe'
setTimeout(() => {
triggerRef(shallow)
}, 1500);
},
}
</script>