來自 Vue 3.0 的 Composition API 嚐鮮

image

前段時間,Vue 官方釋出了 Composition API RFC 的文檔,我也在收到消息的第一時間上手嚐鮮。javascript

雖然 Vue 3.0 還沒有發佈,可是其處於 RFC 階段的 Composition API 已經能夠經過插件 @vue/composition-api 進行體驗了。接下來的內容我將以構建一個 TODO LIST 應用來體驗 Composition API 的用法。vue

本文示例的代碼: https://github.com/jrainlau/v...

1、Vue 2.x 方式構建應用。

這個 TODO LIST 應用很是簡單,僅有一個輸入框、一個狀態切換器、以及 TODO 列表構成:java

image

你們也能夠在這裏體驗。react

藉助 vue-cli 初始化項目之後,咱們的項目結構以下(僅討論 /src 目錄):git

.
├── App.vue
├── components
│   ├── Inputer.vue
│   ├── Status.vue
│   └── TodoList.vue
└── main.js

/components 裏文件的命名不難發現,三個組件對應了 TODO LIST 應用的輸入框、狀態切換器,以及 TODO 列表。這三個組件的代碼都很是簡單就不展開討論了,此處只討論核心的 App.vue 的邏輯。github

  • App.vue
<template>
  <div class="main">
    <Inputer @submit="submit" />
    <Status @change="onStatusChanged" />
    <TodoList
      :list="onShowList"
      @toggle="toggleStatus"
      @delete="onItemDelete"
    />
  </div>
</template>

<script>
import Inputer from './components/Inputer'
import TodoList from './components/TodoList'
import Status from './components/Status'

export default {
  components: {
    Status,
    Inputer,
    TodoList
  },

  data () {
    return {
      todoList: [],
      showingStatus: 'all'
    }
  },
  computed: {
    onShowList () {
      if (this.showingStatus === 'all') {
        return this.todoList
      } else if (this.showingStatus === 'completed') {
        return this.todoList.filter(({ completed }) => completed)
      } else if (this.showingStatus === 'uncompleted') {
        return this.todoList.filter(({ completed }) => !completed)
      }
    }
  },
  methods: {
    submit (content) {
      this.todoList.push({
        completed: false,
        content,
        id: parseInt(Math.random(0, 1) * 100000)
      })
    },
    onStatusChanged (status) {
      this.showingStatus = status
    },
    toggleStatus ({ isChecked, id }) {
      this.todoList.forEach(item => {
        if (item.id === id) {
          item.completed = isChecked
        }
      })
    },
    onItemDelete (id) {
      let index = 0
      this.todoList.forEach((item, i) => {
        if (item.id === id) {
          index = i
        }
      })
      this.todoList.splice(index, 1)
    }
  }
}
</script>

在上述的代碼邏輯中,咱們使用 todoList 數組存放列表數據,用 onShowList 根據狀態條件 showingStatus 的不一樣而展現不一樣的列表。在 methods 對象中定義了添加項目、切換項目狀態、刪除項目的方法。整體來講仍是很是直觀簡單的。vue-cli

按照 Vue 的官方說法,2.x 的寫法屬於 Options-API 風格,是基於配置的方式聲明邏輯的。而接下來咱們將使用 Composition-API 風格重構上面的邏輯。element-ui

2、使用 Composition-API 風格重構邏輯

下載了 @vue/composition-api 插件之後,按照文檔在 main.js 引用便開啓了 Composition API 的能力。小程序

  • main.js
import Vue from 'vue'
import App from './App.vue'
import VueCompositionApi from '@vue/composition-api'

Vue.config.productionTip = false
Vue.use(VueCompositionApi)

new Vue({
  render: h => h(App),
}).$mount('#app')

回到 App.vue,從 @vue/composition-api 插件引入 { reactive, computed, toRefs } 三個函數:微信小程序

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

僅保留 components: { ... } 選項,刪除其餘的,而後寫入 setup() 函數:

export default {
  components: { ... },
  setup () {}
}

接下來,咱們將會在 setup() 函數裏面重寫以前的邏輯。

首先定義數據

爲了讓數據具有「響應式」的能力,咱們須要使用 reactive() 或者 ref() 函數來對其進行包裝,關於這兩個函數的差別,會在後續的章節裏面闡述,如今咱們先使用 reactive() 來進行。

setup() 函數裏,咱們定義一個響應式的 data 對象,相似於 2.x 風格下的 data() 配置項。

setup () {
    const data = reactive({
      todoList: [],
      showingStatus: 'all',
      onShowList: computed(() => {
        if (data.showingStatus === 'all') {
          return data.todoList
        } else if (data.showingStatus === 'completed') {
          return data.todoList.filter(({ completed }) => completed)
        } else if (data.showingStatus === 'uncompleted') {
          return data.todoList.filter(({ completed }) => !completed)
        }
      })
    })
}

其中計算屬性 onShowList 通過了 computed() 函數的包裝,使得它能夠根據其依賴的數據的變化而變化。

接下來定義方法

setup() 函數裏面,對以前的幾個操做選項的方法稍加修改便可直接使用:

function submit (content) {
      data.todoList.push({
        completed: false,
        content,
        id: parseInt(Math.random(0, 1) * 100000)
      })
    }
    function onStatusChanged (status) {
      data.showingStatus = status
    }
    function toggleStatus ({ isChecked, id }) {
      data.todoList.forEach(item => {
        if (item.id === id) {
          item.completed = isChecked
        }
      })
    }
    function onItemDelete (id) {
      let index = 0
      data.todoList.forEach((item, i) => {
        if (item.id === id) {
          index = i
        }
      })
      data.todoList.splice(index, 1)
    }

與在 methods: {} 對象中定義的形式所不一樣的地方是,在 setup() 裏的方法不能經過 this 來訪問實例上的數據,而是經過直接讀取 data 來訪問。

最後,把剛剛定義好的數據和方法都返回出去便可:

return {
      ...toRefs(data),
      submit,
      onStatusChanged,
      toggleStatus,
      onItemDelete,
    }

這裏使用了 toRefs()data 對象包裝了一下,是爲了讓它的數據保持「響應式」的,這裏面的原委會在後續章節展開。

重構完成後,發現其運行的結果和以前的徹底一致,證實 Composition API 是能夠正確運行的。接下來咱們來聊聊 reactive()ref() 的問題。

3、響應式數據

咱們知道 Vue 的其中一個賣點,就是其強大的響應式系統。不管是哪一個版本,這個核心功能都貫穿始終。而說到響應式系統,每每離不開響應式數據,這也是被你們所津津樂道的話題。

回顧一下,在2.x版本中 Vue 使用了 Object.defineProperty() 方法改寫了一個對象,在它的 getter 和 setter 裏面埋入了響應式系統相關的邏輯,使得一個對象被修改時可以觸發對應的邏輯。在即將到來的 3.0 版本中,Vue 將會使用 Proxy 來完成這裏的功能。爲了體驗所謂的「響應式對象」,咱們能夠直接經過 Vue 提供的一個 API Vue.observable() 來實現:

const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}
上述代碼引用自 官方文檔

從代碼能夠看出,經過 Vue.observable() 封裝的 state,已經具有了響應式的特性,當按鈕被點擊的時候,它裏面的 count 值會改變,改變的同時會引發視圖層的更新。

回到 Composition API,它的 reactive()ref() 函數也是爲了實現相似的功能,而 @vue/composition-api 插件的核心也是來自 Vue.observable()

function observe<T>(obj: T): T {
  const Vue = getCurrentVue();
  let observed: T;
  if (Vue.observable) {
    observed = Vue.observable(obj);
  } else {
    const vm = createComponentInstance(Vue, {
      data: {
        $$state: obj,
      },
    });
    observed = vm._data.$$state;
  }

  return observed;
}
節選自 插件源碼

在理解了 reactive()ref() 的目的以後,咱們就能夠去分析它們的區別了。

首先咱們來看兩段代碼:

// style 1: separate variables
let x = 0
let y = 0

function updatePosition(e) {
  x = e.pageX
  y = e.pageY
}

// --- compared to ---

// style 2: single object
const pos = {
  x: 0,
  y: 0
}

function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}

假設 xy 都是須要具有「響應式」能力的數據,那麼 ref() 就至關於第一種風格,單獨地爲某個數據提供響應式能力;而 reactive() 則至關於第二種風格,給一整個對象賦予響應式能力。

可是在具體的用法上,經過 reactive() 包裝的對象會有一個坑。若是想要保持對象內容的響應式能力,在 return 的時候必須把整個 reactive() 對象返回出去,同時在引用的時候也必須對整個對象進行引用而沒法解構,不然這個對象內容的響應式能力將會丟失。這麼提及來有點繞,能夠看看官網的例子加深理解:

// composition function
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return pos
}

// consuming component
export default {
  setup() {
    // reactivity lost!
    const { x, y } = useMousePosition()
    return {
      x,
      y
    }

    // reactivity lost!
    return {
      ...useMousePosition()
    }

    // this is the only way to retain reactivity.
    // you must return `pos` as-is and reference x and y as `pos.x` and `pos.y`
    // in the template.
    return {
      pos: useMousePosition()
    }
  }
}

舉一個不太恰當的例子。「對象的特性」是賦予給整個「對象」的,它裏面的內容若是也想要擁有這部分特性,只能和這個對象捆綁在一塊,而不能單獨拎出來。

可是在具體的業務中,若是沒法使用解構取出 reactive() 對象的值,每次都須要經過 . 操做符訪問它裏面的屬性會是很是麻煩的,因此官方提供了 toRefs() 函數來爲咱們填好這個坑。只要使用 toRefs()reactive() 對象包裝一下,就可以經過解構單獨使用它裏面的內容了,而此時的內容也依然維持着響應式的特性。

至於什麼時候使用 reactive()ref(),都是按照具體的業務邏輯來選擇。對於我我的來講,會更傾向於使用 reactive() 搭配 toRefs() 來使用,由於通過 ref() 封裝的數據必須經過 .value 才能訪問到裏面的值,寫法上要注意的地方相對更多一些。

4、Composition API 的優點及擴展

Vue 其中一個被人詬病得很嚴重的問題就是邏輯複用。隨着項目愈加的複雜,能夠抽象出來被複用的邏輯也愈加的多。可是 Vue 在 2.x 階段只能經過 mixins 來解決(固然也能夠很是繞地實現 HOC,這裏再也不展開)。mixins 只是簡單地把代碼邏輯進行合併,若是須要對邏輯進行追蹤將會是一個很是痛苦的過程,由於繁雜的業務邏輯裏面每每很難一眼看出哪些數據或方法是來自 mixins 的,哪些又是來自當前組件的。

另一點則是對 TypsScript 的支持。爲了更好地進行類型推斷,雖然 2.x 也有使用 Class 風格的 ts 實現方案,但其冗長繁雜和依賴不穩定的 decorator 的寫法,並不是一個好的解決方案。受到 React Hooks 的啓發,Vue Composition API 以函數組合的方式完成邏輯,天生就適合搭配 TypeScript 使用。

至於 Options API 和 Composition API 孰優孰劣的問題,在本文所展現的例子中實際上是比較難區分的,緣由是這個例子的邏輯實在是太過簡單。可是若是深刻思考的話不難發現,若是項目足夠複雜,Composition API 可以很好地把邏輯抽離出來,每一個組件的 setup() 函數所返回的值都可以方便地被追蹤(好比在 VSCode 裏按着 cmd 點擊變量名便可跳轉到其定義的地方)。這樣的能力在維護大型項目或者多人協做項目的時候會很是有用,通用的邏輯也能夠更細粒度地共享出去。

關於 Composition API 的設計理念和優點能夠參考官網的 Motivation 章節

若是腦洞再開大一點,Composition API 可能還有更酷的玩法。

  • 對於一些第三方組件庫(如 element-ui),除了能夠提供包含了樣式、結構和邏輯的組件以外,還能夠把部分邏輯以 Composition API 的方式提供出來,其可定製化和玩法將會更加豐富。
  • reactive() 方法能夠把一個對象變得響應式,搭配 watch() 方法能夠很方便地處理 side effects:

    import { reactive, watch } from 'vue'
    
    const state = reactive({
      count: 0
    })
    
    watch(() => {
      document.body.innerHTML = `count is ${state.count}`
    })

    上述例子中,當響應式的 state.count 被修改之後,會觸發 watch() 函數裏面的回調。基於此,也許咱們能夠利用這個特性去處理其餘平臺的視圖更新問題。微信小程序開發框架 mpvue 就是經過魔改 Vue 的源碼來實現小程序視圖的數據綁定及更新的,若是擁有了 Composition API,也許咱們就能夠經過 reactive()watch() 等方法來實現相似的功能,此時 Vue 將會是位於數據視圖中間的一層,數據的綁定放在 reactive(),而視圖的更新則統一放在 watch() 當中進行。

5、小結

本文經過一個 TODO LIST 應用,按照官網的指導完成一次對 Composition API 的嚐鮮式探索,學習了新的 API 的用法並討論了當中的一些設計理念,分析了當中的一些問題,最後腦洞大開對立面的用法進行了探索。因爲相關資料較少且 Composition API 仍在 RFC 階段,因此文章當中可能會有難以免的謬誤,若是有任何的意見和見解都歡迎和我交流。

相關文章
相關標籤/搜索