看完上一章 初入茅廬
以後,相信你們已經對vue-next(Vue 3.0)有所瞭解了。本章帶你掌握 vue-next
函數式的API,瞭解這些的話,不管是對於源碼的閱讀,仍是當正式版發佈時開始學習,應該都會有起到必定的輔助做用。javascript
直接拷貝下面代碼,去運行看效果吧。推薦使用高版本的chrome瀏覽器,記得打開F12調試工具哦!html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://s1.zhuanstatic.com/common/js/vue-next-3.0.0-alpha.0.js"></script>
<div id="app"></div>
<script> const { ref, reactive, createApp, watch, effect } = Vue function useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } Vue.onMounted(() => { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } const App = { props: { age: Number }, // Composition API 使用的入口 setup(props, context){ console.log('props.age', props.age) // 定義響應數據 const state = reactive({name:'zhuanzhuan'}); // 使用公共邏輯 const {x,y} = useMouse(); Vue.onMounted(()=>{ console.log('當組掛載完成') }); Vue.onUpdated(()=>{ console.log('數據發生更新') }); Vue.onUnmounted(()=>{ console.log('組件將要卸載') }) function changeName(){ state.name = '轉轉'; } // 建立監視,並獲得 中止函數 const stop = watch(() => console.log(`watch state.name:`, state.name)) // 調用中止函數,清除對應的監視 // stop() // 觀察包裝對象 watch(() => state.name, (value, oldValue) => console.log(`watch state.name value:${value} oldValue:${oldValue}`)) effect(() => { console.log(`effect 觸發了! 名字是:${state.name},年齡:${props.age}`) }) // 返回上下文,能夠在模板中使用 return { // state: Vue.toRefs(state), // 也能夠這樣寫,將 state 上的每一個屬性,都轉化爲 ref 形式的響應式數據 state, x, y, changeName, } }, template:`<button @click="changeName">名字是:{{state.name}} 鼠標x: {{x}} 鼠標: {{y}}</button>` } createApp().mount(App, '#app', {age: 123}); </script>
</body>
</html>
複製代碼
組件 API 設計所面對的核心問題之一就是如何組織邏輯,以及如何在多個組件之間抽取和複用邏輯。基於 Vue 2.x 目前的 API 有一些常見的邏輯複用模式,但都或多或少存在一些問題。這些模式包括:vue
網絡上關於這些模式的介紹不少,這裏就再也不贅述細節。整體來講,以上這些模式存在如下問題:java
模版中的數據來源不清晰。舉例來講,當一個組件中使用了多個 mixin
的時候,光看模版會很難分清一個屬性究竟是來自哪個 mixin
。HOC
也有相似的問題。react
命名空間衝突。由不一樣開發者開發的 mixin
沒法保證不會正好用到同樣的屬性或是方法名。HOC
在注入的 props
中也存在相似問題。git
性能。HOC
和 Renderless Components
都須要額外的組件實例嵌套來封裝邏輯,致使無謂的性能開銷。github
從以上useMouse
例子中能夠看到:chrome
vue-next
的一個主要設計目標是加強對 TypeScript
的支持。本來指望經過 Class API
來達成這個目標,可是通過討論和原型開發,認爲 Class
並非解決這個問題的正確路線,基於 Class
的 API
依然存在類型問題。api
基於函數的 API
自然對類型推導很友好,由於 TS
對函數的參數、返回值和泛型的支持已經很是完備。更值得一提的是基於函數的 API
在使用 TS
或是原生 JS
時寫出來的代碼幾乎是徹底同樣的。數組
咱們將會引入一個新的組件選項,setup()
。顧名思義,這個函數將會是咱們 setup
咱們組件邏輯的地方,它會在一個組件實例被建立時,初始化了 props 以後調用。它爲咱們使用 vue-next
的 Composition API
新特性提供了統一的入口。
setup
函數會在 beforeCreate
以後、created
以前執行。
聲明 state
主要有如下幾種類型。
基礎類型能夠經過 ref
這個api
來聲明,以下:
const App = {
setup(props, context){
const msg = ref('hello')
function appendName(){
msg.value = `hello ${props.name}`
}
return {appendName, msg}
},
template:`<div @click="appendName">{{ msg }}</div>`
}
複製代碼
咱們知道在 JavaScript
中,原始值類型如 string
和 number
是隻有值,沒有引用的。若是在一個函數中返回一個字符串變量,接收到這個字符串的代碼只會得到一個值,是沒法追蹤原始變量後續的變化的。
所以,包裝對象的意義就在於提供一個讓咱們可以在函數之間以引用的方式傳遞任意類型值的容器。這有點像 React Hooks
中的 useRef
—— 但不一樣的是 Vue
的包裝對象同時仍是響應式的數據源。有了這樣的容器,咱們就能夠在封裝了邏輯的組合函數中將狀態以引用的方式傳回給組件。組件負責展現(追蹤依賴),組合函數負責管理狀態(觸發更新):
setup(props, context){
// x,y 可能被 useMouse() 內部的代碼修改從而觸發更新
const {x,y} = useMouse();
return { x, y }
}
複製代碼
包裝對象也能夠包裝非原始值類型的數據,被包裝的對象中嵌套的屬性都會被響應式地追蹤。用包裝對象去包裝對象或是數組並非沒有意義的:它讓咱們能夠對整個對象的值進行替換 —— 好比用一個 filter
過的數組去替代原數組:
const numbers = ref([1, 2, 3])
// 替代原數組,但引用不變
numbers.value = numbers.value.filter(n => n > 1)
複製代碼
這裏補充一下,在 基礎類型 第一個例子中你可能注意到了,雖然 setup()
返回的 msg
是一個包裝對象,但在模版中咱們直接用了 {{ msg }}
這樣的綁定,沒有用 .value
。這是由於當包裝對象被暴露給模版渲染上下文,或是被嵌套在另外一個響應式對象中的時候,它會被自動展開 (unwrap)
爲內部的值。
引用類型除了可使用 ref
來聲明,也能夠直接使用 reactive
,以下:
const App = {
setup(props, context){
const state = reactive({name:'zhuanzhuan'});
function changeName(){
state.name = '轉轉';
}
return {state, changeName, msg}
},
template:`<button @click="changeName">名字是:{{state.name}}</button>`
}
複製代碼
props
中定義當前組件容許外界傳遞過來的參數名稱:props: {
age: Number
}
複製代碼
setup
函數的第一個形參,接收 props
數據:setup(props) {
console.log('props.age', props.age)
watch(() => props.age, (value, oldValue) => console.log(`watch props.age value:${value} oldValue:${oldValue}`))
}
複製代碼
除此以外,還能夠直接經過 watch
方法來觀察某個 prop
的變更,這是爲何呢?答案很是簡單,就是 props
自己在源碼中,也是一個被 reactive
包裹後的對象,所以它具備響應性,因此在watch
方法中的回調函數會自動收集依賴,以後當 age
變更時,會自動調用這些回調邏輯。
setup
函數的第二個形參是一個上下文對象,這個上下文對象中包含了一些有用的屬性,這些屬性在 vue 2.x
中須要經過 this
才能訪問到,那我想經過 this
像在 vue2
中訪問一些內置屬性,怎麼辦?好比 attrs
或者 emit
。咱們能夠經過 setup 的第二個參數,在 vue-next
中,它們的訪問方式以下:
const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.parent
context.root
context.emit
context.refs
}
}
複製代碼
注意:==在 setup()
函數中沒法訪問到 this
==
reactive()
函數接收一個普通對象,返回一個響應式的數據對象。
等價於 vue 2.x
中的 Vue.observable()
函數,vue 3.x
中提供了 reactive()
函數,用來建立響應式的數據對象,基本代碼示例以下:
// 建立響應式數據對象,獲得的 state 相似於 vue 2.x 中 data() 返回的響應式對象
const state = reactive({name:'zhuanzhuan'});
複製代碼
const { reactive } = Vue
複製代碼
setup()
函數中調用 reactive()
函數,建立響應式數據對象:const { reactive } = Vue
setup(props, context){
const state = reactive({name:'zhuanzhuan'});
return state
}
複製代碼
template
中訪問響應式數據:template:`<button>名字是:{{name}} </button>`
複製代碼
Value Unwrapping(包裝對象的自動展開)
ref()
函數用來根據給定的值建立一個響應式的數據對象,ref()
函數調用的返回值是一個對象,這個對象上只包含一個 .value
屬性。
const { ref } = Vue
// 建立響應式數據對象 age,初始值爲 3
const age = ref(3)
// 若是要訪問 ref() 建立出來的響應式數據對象的值,必須經過 .value 屬性才能夠
console.log(age.value) // 輸出 3
// 讓 age 的值 +1
age.value++
// 再次打印 age 的值
console.log(age.value) // 輸出 4
複製代碼
setup()
中建立響應式數據:setup() {
const age = ref(3)
return {
age,
name: ref('zhuanzhuan')
}
}
複製代碼
template
中訪問響應式數據:template:`<p>名字是:{{name}},年齡是{{age}}</p>`
複製代碼
當把 ref()
建立出來的響應式數據對象,掛載到 reactive()
上時,會自動把響應式數據對象展開爲原始的值,不需經過 .value
就能夠直接被訪問。
換句話說就是當一個包裝對象被做爲另外一個響應式對象的屬性引用的時候也會被自動展開例如:
const age = ref(3)
const state = reactive({
age
})
console.log(state.age) // 輸出 3
state.age++ // 此處不須要經過 .value 就能直接訪問原始值
console.log(age) // 輸出 4
複製代碼
以上這些關於包裝對象的細節可能會讓你以爲有些複雜,但實際使用中你只須要記住一個基本的規則:只有當你直接以變量的形式引用一個包裝對象的時候纔會須要用 .value 去取它內部的值 —— 在模版中你甚至不須要知道它們的存在。
==注意:新的 ref 會覆蓋舊的 ref,示例代碼以下:==
// 建立 ref 並掛載到 reactive 中
const c1 = ref(0)
const state = reactive({
c1
})
// 再次建立 ref,命名爲 c2
const c2 = ref(9)
// 將 舊 ref c1 替換爲 新 ref c2
state.c1 = c2
state.c1++
console.log(state.c1) // 輸出 10
console.log(c2.value) // 輸出 10
console.log(c1.value) // 輸出 0
複製代碼
isRef()
用來判斷某個值是否爲 ref()
建立出來的對象;應用場景:當須要展開某個可能爲 ref()
建立出來的值的時候,例如:
const { isRef } = Vue
const unwrapped = isRef(foo) ? foo.value : foo
複製代碼
const { toRefs } = Vue
setup() {
// 定義響應式數據對象
const state = reactive({
age: 3
})
// 定義頁面上可用的事件處理函數
const increment = () => {
state.age++
}
// 在 setup 中返回一個對象供頁面使用
// 這個對象中能夠包含響應式的數據,也能夠包含事件處理函數
return {
// 將 state 上的每一個屬性,都轉化爲 ref 形式的響應式數據
...toRefs(state),
// 自增的事件處理函數
increment
}
}
複製代碼
頁面上能夠直接訪問 setup() 中 return 出來的響應式數據:
template:` <div> <p>當前的age值爲:{{age}}</p> <button @click="increment">+1</button> </div> `
複製代碼
computed()
用來建立計算屬性,computed()
函數的返回值是一個 ref
的實例。使用 computed
以前須要按需導入:
const { computed } = Vue
複製代碼
const { computed } = Vue
// 建立一個 ref 響應式數據
const count = ref(1)
// 根據 count 的值,建立一個響應式的計算屬性 plusOne
// 它會根據依賴的 ref 自動計算並返回一個新的 ref
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 輸出 2
plusOne.value++ // error
複製代碼
在調用 computed()
函數期間,傳入一個包含 get
和 set
函數的對象,能夠獲得一個可讀可寫的計算屬性,示例代碼以下:
const { computed } = Vue
// 建立一個 ref 響應式數據
const count = ref(1)
// 建立一個 computed 計算屬性
const plusOne = computed({
// 取值函數
get: () => count.value + 1,
// 賦值函數
set: val => { count.value = val - 1 }
})
// 爲計算屬性賦值的操做,會觸發 set 函數
plusOne.value = 9
// 觸發 set 函數後,count 的值會被更新
console.log(count.value) // 輸出 8
複製代碼
watch()
函數用來監視某些數據項的變化,從而觸發某些特定的操做,使用以前須要按需導入:
const { watch } = Vue
複製代碼
const { watch } = Vue
const count = ref(0)
// 定義 watch,只要 count 值變化,就會觸發 watch 回調
// watch 會在建立時會自動調用一次
watch(() => console.log(count.value))
// 輸出 0
setTimeout(() => {
count.value++
// 輸出 1
}, 1000)
複製代碼
監視 reactive
類型的數據源:
const { watch, reactive } = Vue
const state = reactive({name:'zhuanzhuan'});
watch(() => state.name, (value, oldValue) => { /* ... */ })
複製代碼
const { watch, ref } = Vue
// 定義數據源
const count = ref(0)
// 指定要監視的數據源
watch(count, (value, oldValue) => { /* ... */ })
複製代碼
監視 reactive 類型的數據源:
const { reactive, watch, ref } = Vue
onst state = reactive({ age: 3, name: 'zhuanzhuan' })
watch(
[() => state.age, () => state.name], // Object.values(toRefs(state)),
([age, name], [prevCount, prevName]) => {
console.log(age) // 新的 age 值
console.log(name) // 新的 name 值
console.log('------------')
console.log(prevCount) // 舊的 age 值
console.log(prevName) // 新的 name 值
},
{
lazy: true // 在 watch 被建立的時候,不執行回調函數中的代碼
}
)
setTimeout(() => {
state.age++
state.name = '轉轉'
}, 1000)
複製代碼
在 setup()
函數內建立的 watch
監視,會在當前組件被銷燬的時候自動中止。若是想要明確地中止某個監視,能夠調用 watch()
函數的返回值便可,語法以下
// 建立監視,並獲得 中止函數
const stop = watch(() => { /* ... */ })
// 調用中止函數,清除對應的監視
stop()
複製代碼
有時候,當被 watch
監視的值發生變化時,或 watch
自己被 stop
以後,咱們指望可以清除那些無效的異步任務,此時,watch
回調函數中提供了一個 cleanup registrator function
來執行清除的工做。這個清除函數會在以下狀況下被調用:
Template 中的代碼示例以下:
/* template 中的代碼 */
<input type="text" v-model="keywords" />
複製代碼
Script 中的代碼示例以下:
// 定義響應式數據 keywords
const keywords = ref('')
// 異步任務:打印用戶輸入的關鍵詞
const asyncPrint = val => {
// 延時 1 秒後打印
return setTimeout(() => {
console.log(val)
}, 1000)
}
// 定義 watch 監聽
watch(
keywords,
(keywords, prevKeywords, onCleanup) => {
// 執行異步任務,並獲得關閉異步任務的 timerId
const timerId = asyncPrint(keywords)
// keywords 發生了變化,或是 watcher 即將被中止.
// 取消還未完成的異步操做。
// 若是 watch 監聽被重複執行了,則會先清除上次未完成的異步任務
onCleanup(() => clearTimeout(timerId))
},
// watch 剛被建立的時候不執行
{ lazy: true }
)
// 把 template 中須要的數據 return 出去
return {
keywords
}
複製代碼
之因此要用傳入的註冊函數來註冊清理函數,而不是像 React
的 useEffect
那樣直接返回一個清理函數,是由於watcher
回調的返回值在異步場景下有特殊做用。咱們常常須要在 watcher
的回調中用 async function
來執行異步操做:
const data = ref(null)
watch(getId, async (id) => {
data.value = await fetchData(id)
})
複製代碼
咱們知道 async function
隱性地返回一個 Promise
- 這樣的狀況下,咱們是沒法返回一個須要被馬上註冊的清理函數的。除此以外,回調返回的 Promise
還會被 Vue
用於內部的異步錯誤處理。
默認狀況下,全部的 watch
回調都會在當前的 renderer flush
以後被調用。這確保了在回調中 DOM
永遠都已經被更新完畢。若是你想要讓回調在 DOM
更新以前或是被同步觸發,可使用 flush 選項:
watch(
() => count.value + 1,
() => console.log(`count changed`),
{
flush: 'post', // default, fire after renderer flush
flush: 'pre', // fire right before renderer flush
flush: 'sync' // fire synchronously
}
)
複製代碼
interface WatchOptions {
lazy?: boolean
deep?: boolean
flush?: 'pre' | 'post' | 'sync'
onTrack?: (e: DebuggerEvent) => void
onTrigger?: (e: DebuggerEvent) => void
}
interface DebuggerEvent {
effect: ReactiveEffect
target: any
key: string | symbol | undefined
type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate'
}
複製代碼
全部現有的生命週期鉤子都會有對應的 onXXX 函數(只能在 setup() 中使用):
const { onMounted, onUpdated, onUnmounted } = Vue
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
// destroyed 調整爲 unmounted
onUnmounted(() => {
console.log('unmounted!')
})
}
}
複製代碼
下面的列表,是 vue 2.x
的生命週期函數與新版 Composition API
之間的映射關係:
beforeCreate
-> setup()
created
-> setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeDestroy
-> onBeforeUnmount
destroyed
-> onUnmounted
errorCaptured
-> onErrorCaptured
provide()
和 inject()
能夠實現嵌套組件之間的數據傳遞。這兩個函數只能在 setup()
函數中使用。父級組件中使用 provide()
函數向下傳遞數據;子級組件中使用 inject()
獲取上層傳遞過來的數據。
App.vue
根組件:
<template>
<div id="app">
<h1>App 根組件</h1>
<hr />
<LevelOne />
</div>
</template>
<script> import LevelOne from './components/LevelOne' // 1. 按需導入 provide import { provide } from '@vue/composition-api' export default { name: 'app', setup() { // 2. App 根組件做爲父級組件,經過 provide 函數向子級組件共享數據(不限層級) // provide('要共享的數據名稱', 被共享的數據) provide('globalColor', 'red') }, components: { LevelOne } } </script>
複製代碼
LevelOne.vue
組件:
<template>
<div>
<!-- 4. 經過屬性綁定,爲標籤設置字體顏色 -->
<h3 :style="{color: themeColor}">Level One</h3>
<hr />
<LevelTwo />
</div>
</template>
<script> import LevelTwo from './LevelTwo' // 1. 按需導入 inject import { inject } from '@vue/composition-api' export default { setup() { // 2. 調用 inject 函數時,經過指定的數據名稱,獲取到父級共享的數據 const themeColor = inject('globalColor') // 3. 把接收到的共享數據 return 給 Template 使用 return { themeColor } }, components: { LevelTwo } } </script>
複製代碼
LevelTwo.vue
組件:
<template>
<div>
<!-- 4. 經過屬性綁定,爲標籤設置字體顏色 -->
<h5 :style="{color: themeColor}">Level Two</h5>
</div>
</template>
<script> // 1. 按需導入 inject import { inject } from '@vue/composition-api' export default { setup() { // 2. 調用 inject 函數時,經過指定的數據名稱,獲取到父級共享的數據 const themeColor = inject('globalColor') // 3. 把接收到的共享數據 return 給 Template 使用 return { themeColor } } } </script>
複製代碼
以下代碼實現了點按鈕切換主題顏色的功能,主要修改了 App.vue
組件中的代碼,LevelOne.vue
和 LevelTwo.vue
中的代碼不受任何改變:
<template>
<div id="app">
<h1>App 根組件</h1>
<!-- 點擊 App.vue 中的按鈕,切換子組件中文字的顏色 -->
<button @click="themeColor='red'">紅色</button>
<button @click="themeColor='blue'">藍色</button>
<button @click="themeColor='orange'">橘黃色</button>
<hr />
<LevelOne />
</div>
</template>
<script> import LevelOne from './components/LevelOne' import { provide, ref } from '@vue/composition-api' export default { name: 'app', setup() { // 定義 ref 響應式數據 const themeColor = ref('red') // 把 ref 數據經過 provide 提供的子組件使用 provide('globalColor', themeColor) // setup 中 return 數據供當前組件的 Template 使用 return { themeColor } }, components: { LevelOne } } </script>
複製代碼
經過 ref()
還能夠引用頁面上的元素或組件。
示例代碼以下:
<template>
<div>
<h3 ref="h3Ref">TemplateRefOne</h3>
</div>
</template>
<script> import { ref, onMounted } from '@vue/composition-api' export default { setup() { // 建立一個 DOM 引用 const h3Ref = ref(null) // 在 DOM 首次加載完畢以後,才能獲取到元素的引用 onMounted(() => { // 爲 dom 元素設置字體顏色 // h3Ref.value 是原生DOM對象 h3Ref.value.style.color = 'red' }) // 把建立的引用 return 出去 return { h3Ref } } } </script>
複製代碼
TemplateRefOne.vue
中的示例代碼以下:
<template>
<div>
<h3>TemplateRefOne</h3>
<!-- 4. 點擊按鈕展現子組件的 count 值 -->
<button @click="showNumber">獲取TemplateRefTwo中的count值</button>
<hr />
<!-- 3. 爲組件添加 ref 引用 -->
<TemplateRefTwo ref="comRef" />
</div>
</template>
<script> import { ref } from '@vue/composition-api' import TemplateRefTwo from './TemplateRefTwo' export default { setup() { // 1. 建立一個組件的 ref 引用 const comRef = ref(null) // 5. 展現子組件中 count 的值 const showNumber = () => { console.log(comRef.value.count) } // 2. 把建立的引用 return 出去 return { comRef, showNumber } }, components: { TemplateRefTwo } } </script>
複製代碼
TemplateRefTwo.vue
中的示例代碼:
<template>
<div>
<h5>TemplateRefTwo --- {{count}}</h5>
<!-- 3. 點擊按鈕,讓 count 值自增 +1 -->
<button @click="count+=1">+1</button>
</div>
</template>
<script> import { ref } from '@vue/composition-api' export default { setup() { // 1. 定義響應式的數據 const count = ref(0) // 2. 把響應式數據 return 給 Template 使用 return { count } } } </script>
複製代碼
這個函數不是必須的,除非你想要完美結合 TypeScript
提供的類型推斷來進行項目的開發。
這個函數僅僅提供了類型推斷,方便在結合 TypeScript
書寫代碼時,能爲 setup()
中的 props
提供完整的類型推斷。
import { createComponent } from 'vue'
export default createComponent({
props: {
foo: String
},
setup(props) {
props.foo // <- type: string
}
}
複製代碼
以上就是 vue-next(Vue 3.0) API,相信你們已經能夠靈活運用了吧。
那麼你們必定很好奇 vue-next
響應式的原理,下一章vue-next(Vue 3.0)之 爐火純青
帶你解密。