vue3.0 修煉手冊

前言

隨着2020年4月份 Vue3.0 beta 發佈,驚喜於其性能的提高,友好的 TS 支持(語法補全),改寫ES export寫法,利用Tree shaking 減小打包大小,Composition APICustom Renderer API 新功能拓展及其RECs 文檔的完善。固然,還有一些後續工做(vuex, vue-router, cli, vue-test-utils, DevTools, Vetur, Nuxt)待完成,當前還不穩定,正式在項目中使用(目前能夠在小型新項目中),還需在2020 Q2穩定版本以後。html

vue3

Vue3.0 的到來已只是時間問題,未雨綢繆,何不先來嚐鮮一波新特性~vue

設計動機

邏輯組合與複用

組件 API 設計所面對的核心問題之一就是如何組織邏輯,以及如何在多個組件之間抽取和複用邏輯。基於 Vue 2.x 目前的 API 咱們有一些常見的邏輯複用模式,但都或多或少存在一些問題。這些模式包括:react

  • Mixins
  • 高階組件 (Higher-order Components, aka HOCs)
  • Renderless Components (基於 scoped slots / 做用域插槽封裝邏輯的組件)

以上這些模式存在如下問題:git

  • 模版中的數據來源不清晰。舉例來講,當一個組件中使用了多個 mixin 的時候,光看模版會很難分清一個屬性究竟是來自哪個 mixinHOC 也有相似的問題。
  • 命名空間衝突。由不一樣開發者開發的 mixin 沒法保證不會正好用到同樣的屬性或是方法名。HOC 在注入的 props 中也存在相似問題。
  • 性能。HOCRenderless Components 都須要額外的組件實例嵌套來封裝邏輯,致使無謂的性能開銷。

Composition APIReact Hooks 的啓發,提供了一個全新的邏輯複用方案,且不存在上述問題。使用基於函數的 API,咱們能夠將相關聯的代碼抽取到一個 "composition function"(組合函數)中 —— 該函數封裝了相關聯的邏輯,並將須要暴露給組件的狀態以響應式的數據源的方式返回出來。這裏是一個用組合函數來封裝鼠標位置偵聽邏輯的例子:github

function useMouse() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// 在組件中使用該函數
const Component = {
  setup() {
    const { x, y } = useMouse()
    // 與其它函數配合使用
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

從以上例子中能夠看到:vue-router

  • 暴露給模版的屬性來源清晰(從函數返回);
  • 返回值能夠被任意重命名,因此不存在命名空間衝突;
  • 沒有建立額外的組件實例所帶來的性能損耗。

類型推導

3.0 的一個主要設計目標是加強對 TypeScript 的支持。
基於函數的 API 自然對類型推導很友好,由於 TS 對函數的參數、返回值和泛型的支持已經很是完備。vuex

打包尺寸

基於函數的 API 每個函數均可以做爲 named ES export 被單獨引入,這使得它們對 tree-shaking 很是友好。沒有被使用的 API 的相關代碼能夠在最終打包時被移除。同時,基於函數 API 所寫的代碼也有更好的壓縮效率,由於全部的函數名和 setup 函數體內部的變量名均可以被壓縮,但對象和 class 的屬性/方法名卻不能夠。vue-cli

Composition API

除了渲染函數 API 和做用域插槽語法以外的全部內容都將保持不變,或者經過兼容性構建讓其與 2.x 保持兼容

Vue 3.0並不像 Angular 那樣超強跨度版本,致使不兼容,而是在兼容 2.x 基礎上作改進。typescript

在這裏能夠在 2.x 中經過引入 @vue/composition-api,使用 Vue 3.0 新特性。npm

初始化項目

一、安裝 vue-cli3

npm install -g @vue/cli

二、建立項目

vue create vue3

三、項目中安裝 composition-api

npm install @vue/composition-api --save

四、在使用任何 @vue/composition-api 提供的能力前,必須先經過 Vue.use() 進行安裝

import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

setup()

Vue3 引入一個新的組件選項,setup(),它會在一個組件實例被建立時,初始化了 props 以後調用。 會接收到初始的 props 做爲參數:

export default {
  props: {
    name: String
  },
  setup(props) {
    console.log(props.name)
  }
}

傳進來的 props 是響應式的,當後續 props 發生變更時它也會被框架內部同步更新。但對於用戶代碼來講,它是不可修改的(會致使警告)。

同時,setup() 執行時機至關於 2.x 生命週期 beforeCreate 以後,且在 created 以前:

export default {
  beforeCreate() {
    console.log('beforeCreate')
  },
  setup() {
    console.log('setup')
  },
  created() {
    console.log('created')
  }
}
// 打印結果
// beforeCreate
// setup
// created

setup()this 再也不是 vue 實例對象了,而是 undefined,能夠理解爲此時機實例尚未建立。在 setup() 第二個參數是上下文參數,提供了一些 2.x this 上有用屬性。

export default {
  setup(props, context) {
    console.log('this: ', this)
    console.log('context: ', context)
  }
}
// 打印結果
// this: undefined
// context: {
//   attrs: Object
//   emit: f()
//   isServer: false
//   listeners: Object
//   parent: VueComponent
//   refs: Object
//   root: Vue
//   slots: {}
//   ssrContext: undefined
// }

相似 data()setup() 能夠返回一個對象,這個對象上的屬性將會暴露給模版的渲染上下文:

<template>
  <div>{{ name }}</div>
</template>

<script>
export default {
  setup() {
    return {
      name: 'zs'
    }
  }
}
</script>

reactive()

等價於 vue 2.x 中的 Vue.observable() 函數,vue 3.x 中提供了 reactive() 函數,用來建立響應式的數據對象。

當(引用)數據直接改變不會讓模版響應更新渲染:

<template>
  <div>count: {{state.count}}</div>
</template>

<script>
export default {
  setup() {
    const state = { count: 0 }
    setTimeout(() => {
      state.count++
    })
    return { state }
  }
}
// 一秒後頁面沒有變化
</script>

reactive 建立的響應式數據對象,在對象屬性發生變化時,模版是能夠響應更新渲染的:

<template>
  <div>count: {{state.count}}</div>
</template>

<script>
import { reactive } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({ count: 0 })

    setTimeout(() => {
      state.count++
    }, 1000)

    return { state }
  }
}
// 一秒後頁面數字從0變成1
</script>

ref()

Javascript 中,原始類型(如 StringNumber)只有值,沒有引用。若是在一個函數中返回一個字符串變量,接收到這個字符串的代碼只會得到一個值,是沒法追蹤原始變量後續的變化的。

<template>
  <div>count: {{state.count}}</div>
</template>

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    const count = 0

    setTimeout(() => {
      count++
    }, 1000)

    return { count }
  }
}
// 頁面沒有變化
</script>

所以,包裝對象 ref() 的意義就在於提供一個讓咱們可以在函數之間以引用的方式傳遞任意類型值的容器。這有點像 React Hooks 中的 useRef —— 但不一樣的是 Vue 的包裝對象同時仍是響應式的數據源。有了這樣的容器,咱們就能夠在封裝了邏輯的組合函數中將狀態以引用的方式傳回給組件。組件負責展現(追蹤依賴),組合函數負責管理狀態(觸發更新)。

ref() 返回的是一個 value reference (包裝對象)。一個包裝對象只有一個屬性:.value ,該屬性指向內部被包裝的值。包裝對象的值能夠被直接修改。

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    const count = ref(0)
    console.log('count.value: ', count.value)
    count.value++ // 直接修改包裝對象的值
    console.log('count.value: ', count.value)
  }
}
// 打印結果:
// count.value: 0
// count.value: 1
</script>

當包裝對象被暴露給模版渲染上下文,或是被嵌套在另外一個響應式對象中的時候,它會被自動展開 (unwrap) 爲內部的值:

<template>
  <div>ref count: {{count}}</div>
</template>

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    const count = ref(0)
    console.log('count.value: ', count.value)
    return {
      count // 包裝對象 value 屬性自動展開
    }
  }
}
</script>

也能夠用 ref() 包裝對象做爲 reactive() 建立的對象的屬性值,一樣屬性值 ref() 包裝對象也會模版上下文被展開:

<template>
  <div>reactive ref count: {{state.count}}</div>
</template>

<script>
import { reactive, ref } from '@vue/composition-api'

export default {
  setup() {
    const count = ref(0)
    const state = reactive({count})
    return {
      state // 包裝對象 value 屬性自動展開
    }
  }
}
</script>

Vue 2.x 中用實例上的 $refs 屬性獲取模版元素中 ref 屬性標記 DOM 或組件信息,在這裏用 ref() 包裝對象也能夠用來引用頁面元素和組件;

<template>
  <div><p ref="text">Hello</p></div>
</template>

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    const text = ref(null)
    setTimeout(() => {
      console.log('text: ', text.value.innerHTML)
    }, 1000)
    return {
      text
    }
  }
}
// 打印結果:
// text: Hello
</script>

unref()

若是參數是一個 ref 則返回它的 value,不然返回參數自己。它是 val = isRef(val) ? val.value : val 的語法糖。

isref()

檢查一個值是否爲一個 ref 對象。

toRefs()

把一個響應式對象轉換成普通對象,該普通對象的每一個 property 都是一個 ref ,和響應式對象 property 一一對應。而且,當想要從一個組合邏輯函數中返回響應式對象時,用 toRefs 是頗有效的,該 API 讓消費組件能夠 解構 / 擴展(使用 ... 操做符)返回的對象,並不會丟失響應性:

<template>
  <div>
    <p>count: {{count}}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
import { reactive, toRefs } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({
      count: 0
    })

    const increment = () => {
      state.count++
    }

    return {
      ...toRefs(state), // 解構出來不丟失響應性
      increment
    }
  }
}
</script>

computed()

computed() 用來建立計算屬性,computed() 函數的返回值是一個 ref 的實例。這個值模式是隻讀的:

import { ref, computed } from '@vue/composition-api'

export default {
  setup() {
    const count = ref(0)
    const plusOne = computed(() => count.value + 1)
    plusOne.value = 10
    console.log('plusOne.value: ', plusOne.value)
    console.log('count.value: ', count.value)
  }
}
// 打印結果:
// [Vue warn]: Computed property was assigned to but it has no setter.
// plusOne.value: 1
// count.value: 0

或者傳入一個擁有 getset 函數的對象,建立一個可手動修改的計算狀態:

import { ref, computed } from '@vue/composition-api'

export default {
  setup() {
    const count = ref(0)
    const plusOne = computed({
      get: () => count.value + 1,
      set: val => {
        count.value = val - 1
      }
    })
    plusOne.value = 10
    console.log('plusOne.value: ', plusOne.value)
    console.log('count.value: ', count.value)
  }
}
// 打印結果:
// plusOne.value: 10
// count.value: 9

watchEffect()

watchEffect() 監測反作用函數。當即執行傳入的一個函數,並響應式追蹤其依賴,並在其依賴變動時從新運行該函數:

<template>
  <div>
    <p>count: {{count}}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
import { ref, watchEffect } from '@vue/composition-api'

export default {
  setup() {
    // 監視 ref 數據源
    const count = ref(0)
    // 監視依賴有變化,馬上執行
    watchEffect(() => {
      console.log('count.value: ', count.value)
    })
    const increment = () => {
      count.value++
    }
    return {
      count,
      increment
    }
  }
}
</script>

中止偵聽。當 watchEffect 在組件的 setup() 函數或生命週期鉤子被調用時, 偵聽器會被連接到該組件的生命週期,並在組件卸載時自動中止。

在一些狀況下(好比超時就無需繼續監聽變化),也能夠顯式調用返回值以中止偵聽:

<template>
  <div>
    <p>count: {{state.count}}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
import { reactive, watchEffect } from '@vue/composition-api'

export default {
  setup() {
    // 監視 reactive 數據源
    const state = reactive({
      count: 0
    })
    const stop = watchEffect(() => {
      console.log('state.count: ', state.count)
    })
    setTimeout(() => {
      stop()
    }, 3000)
    const increment = () => {
      state.count++
    }
    return {
      state,
      increment
    }
  }
}
// 3秒後,點擊+1按鈕再也不打印
</script>

清除反作用。有時候當觀察的數據源變化後,咱們可能須要對以前所執行的反作用進行清理。舉例來講,一個異步操做在完成以前數據就產生了變化,咱們可能要撤銷還在等待的前一個操做。爲了處理這種狀況,watchEffect 的回調會接收到一個參數是用來註冊清理操做的函數。調用這個函數能夠註冊一個清理函數。清理函數會在下屬狀況下被調用:

  • 反作用即將從新執行時
  • 偵聽器被中止 (若是在 setup() 或 生命週期鉤子函數中使用了 watchEffect, 則在卸載組件時)

咱們之因此是經過傳入一個函數去註冊失效回調,而不是從回調返回它(如 React useEffect 中的方式),是由於返回值對於異步錯誤處理很重要。

const data = ref(null)
watchEffect(async (id) => {
  data.value = await fetchData(id)
})

async function 隱性地返回一個 Promise - 這樣的狀況下,咱們是沒法返回一個須要被馬上註冊的清理函數的。除此以外,回調返回的 Promise 還會被 `Vue 用於內部的異步錯誤處理。

在實際應用中,在大於某個頻率(請求 padding狀態)操做時,能夠先取消以前操做,節約資源:

<template>
  <div>
    <input type="text"
      v-model="keyword">
  </div>
</template>

<script>
import { ref, watchEffect } from '@vue/composition-api'

export default {
  setup() {
    const keyword = ref('')
    const asyncPrint = val => {
      return setTimeout(() => {
        console.log('user input: ', val)
      }, 1000)
    }

    watchEffect(
      onInvalidate => {
        const timer = asyncPrint(keyword.value)
        onInvalidate(() => clearTimeout(timer))
        console.log('keyword change: ', keyword.value)
      },
      {
        flush: 'post' // 默認'post',同步'sync','pre'組件更新以前
      }
    )

    return {
      keyword
    }
  }
}
// 實現對用戶輸入「防抖」效果
</script>

watch()

watch API 徹底等效於 2.x this.$watch (以及 watch 中相應的選項)。watch 須要偵聽特定的數據源,並在回調函數中執行反作用。默認狀況是懶執行的,也就是說僅在偵聽的源變動時才執行回調。

watch() 接收的第一個參數被稱做 「數據源」,它能夠是:

  • 一個返回任意值的函數
  • 一個包裝對象
  • 一個包含上述兩種數據源的數組

第二個參數是回調函數。回調函數只有當數據源發生變更時纔會被觸發:

watch(
  // getter
  () => count.value + 1,
  // callback
  (value, oldValue) => {
    console.log('count + 1 is: ', value)
  }
)
// -> count + 1 is: 1

count.value++
// -> count + 1 is: 2

上面提到第一個參數的「數據源」能夠是一個包含函數和包裝對象的數組,也就是能夠同時監聽多個數據源。同時,watchwatchEffect 在中止偵聽, 清除反作用 (相應地 onInvalidate 會做爲回調的第三個參數傳入),等方面行爲一致。下面用上面「防抖」例子用 watch 改寫:

<template>
  <div>
    <input type="text"
      v-model="keyword">
  </div>
</template>

<script>
import { ref, watch } from '@vue/composition-api'

export default {
  setup() {
    const keyword = ref('')
    const asyncPrint = val => {
      return setTimeout(() => {
        console.log('user input: ', val)
      })
    }

    watch(
      keyword,
      (newVal, oldVal, onCleanUp) => {
        const timer = asyncPrint(keyword)
        onCleanUp(() => clearTimeout(timer))
      },
      {
        lazy: true // 默認未false,即初始監聽回調函數執行了
      }
    )
    return {
      keyword
    }
  }
}
</script>

2.x$watch 有所不一樣的是,watch() 的回調會在建立時就執行一次。這有點相似 2.x watcherimmediate: true 選項,但有一個重要的不一樣:默認狀況下 watch() 的回調老是會在當前的 renderer flush 以後才被調用 —— 換句話說,watch()的回調在觸發時,DOM 老是會在一個已經被更新過的狀態下。 這個行爲是能夠經過選項來定製的。

2.x 的代碼中,咱們常常會遇到同一份邏輯須要在 mounted 和一個 watcher 的回調中執行(好比根據當前的 id 抓取數據),3.0watch() 默認行爲能夠直接表達這樣的需求。

生命週期鉤子函數

能夠直接導入 onXXX 一族的函數來註冊生命週期鉤子。

import { onMounted, onUpdated, onUnmounted } from '@vue/composition-api'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  },
}

這些生命週期鉤子註冊函數只能在 setup() 期間同步使用, 由於它們依賴於內部的全局狀態來定位當前組件實例(正在調用 setup() 的組件實例), 不在當前組件下調用這些函數會拋出一個錯誤。

組件實例上下文也是在生命週期鉤子同步執行期間設置的,所以,在卸載組件時,在生命週期鉤子內部同步建立的偵聽器和計算狀態也將自動刪除。

2.x 的生命週期函數與新版 Composition API 之間的映射關係:

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

注意:beforeCreatecreatedVue3 中已經由 setup 替代。

依賴注入

provideinject 提供依賴注入,功能相似 2.xprovide/inject。二者都只能在當前活動組件實例的 setup() 中調用。

可使用 ref 來保證 providedinjected 之間值的響應。

父依賴注入,做爲提供者,傳給子組件:

import { ref, provide } from '@vue/composition-api'
import ComParent from './ComParent.vue'

export default {
  components: {
    ComParent
  },
  setup() {
    let treasure = ref('傳國玉璽')
    provide('treasure', treasure)
    setTimeout(() => {
      treasure.value = '尚方寶劍'
    }, 1000)
    return {
      treasure
    }
  }
}

子依賴注入,可做爲使用者:

import { inject } from '@vue/composition-api'
import ComChild from './ComChild.vue'

export default {
  components: {
    ComChild
  },
  setup() {
    const treasure = inject('treasure')
    return {
      treasure
    }
  }
}

孫組件依賴注入,做爲使用者使用,當祖級依賴傳入的值改變時,也能響應:

import { inject } from '@vue/composition-api'

export default {
  setup() {
    const treasure = inject('treasure')
    console.log('treasure: ', treasure)
  }
}

缺點/潛在問題

新的 API 使得動態地檢視/修改一個組件的選項變得更困難(原來是一個對象,如今是一段沒法被檢視的函數體)。

這多是一件好事,由於一般在用戶代碼中動態地檢視/修改組件是一類比較危險的操做,對於運行時也增長了許多潛在的邊緣狀況(特別是組件繼承和使用 mixin 的狀況下)。新 API 的靈活性應該在絕大部分狀況下均可以用更顯式的代碼達成一樣的結果。

缺少經驗的用戶可能會寫出 「麪條代碼」,由於新 API 不像舊 API 那樣強制將組件代碼基於選項切分開來。

noodle code

基於函數的新 API 和基於選項的舊 API 之間的最大區別,就是新 API 讓抽取邏輯變得很是簡單 —— 就跟在普通的代碼中抽取函數同樣。也就是說,咱們沒必要只在須要複用邏輯的時候才抽取函數,也能夠單純爲了更好地組織代碼去抽取函數。

基於選項的代碼只是看上去更整潔。一個複雜的組件每每須要同時處理多個不一樣的邏輯任務,每一個邏輯任務所涉及的代碼在選項 API 下是被分散在多個選項之中的。舉例來講,從服務端抓取一份數據,可能須要用到 props, data(), mountedwatch。極端狀況下,若是咱們把一個應用中全部的邏輯任務都放在一個組件裏,這個組件必然會變得龐大而難以維護,由於每一個邏輯任務的代碼都被選項切成了多個碎片分散在各處。

對比之下,基於函數的 API 讓咱們能夠把每一個邏輯任務的代碼都整理到一個對應的函數中。當咱們發現一個組件變得過大時,咱們會將它切分紅多個更小的組件;一樣地,若是一個組件的 setup() 函數變得很複雜,咱們能夠將它切分紅多個更小的函數。而若是是基於選項,則沒法作到這樣的切分,由於用 mixin 只會讓事情變得更糟糕。

總結

Vue 3.0API 的調整其實並不大,熟悉 2.x 的童鞋就會有一種似曾相識的感受,過渡成本極小。更可能是源碼層面的重構,讓其更好用(從選項式到函數式,基於 typescript 重寫,強制類型檢查和提示補全),性能更強(重寫了虛擬 Dom 的實現,採用原生 Proxy 監聽)。

本文案例代碼能夠戳這裏

本文參考了:

Vue Function-based API RFC
Vue Composition API
抄筆記:尤雨溪在Vue3.0 Beta直播裏聊到了這些…完~

相關文章
相關標籤/搜索