Vue 中的受控與非受控組件

Vue 中的受控與非受控組件

熟悉 React 的開發者應該對「受控組件」的概念並不陌生,實際上對於任何組件化開發框架而言,均可以實現所謂的受控與非受控,Vue 固然也不例外。而且理解受控與非受控對應的需求場景,可讓咱們在設計一些基礎組件時思路更加清晰,暴露出來的組件 API 也更加合理、統一。javascript

需求

許多 UI 組件都是有狀態(stateful)的,而這個狀態是由組件外部控制仍是組件內部維護,也就對應了受控與非受控兩種模式。html

例如 Tabs 組件是很常見的一種 UI 組件,它的核心狀態就是記錄當前 active 的 Tab,而且容許用戶切換。java

不少時候咱們只但願 Tabs 能夠正確的展現 active 的內容、並在用戶操做時正常切換,不須要進行任何干預,那麼就但願 只須要傳入全部的 Tab 內容,不須要再作額外的配置。設計模式

但有的時候咱們又但願對 Tabs 的狀態有很強的控制能力,例如多個關聯的 Tabs,子級 Tabs 的內容須要根據父級 Tabs 的 active Tab 動態切換,這時候就會但願 Tabs 組件能夠暴露足夠充分的 API,來實現業務的需求。框架

所以咱們能夠用一種通用的模式,來讓任意組件的任意狀態同時兼容受控與非受控兩種模式,讓不一樣需求場景下均可以使用最合理的 API。組件化

簡化示例

咱們用一個簡單的 Tabs 實現來演示這種通用的組件 API 設計模式,簡化的部分包括:this

  • 用 index 來做爲 Tab 的惟一標識
  • Tab content 只支持字符串

能夠打開 online DEMO 配合閱讀設計

API 設計

對於 Vue 組件而言,API 設計主要指的是內部的 data, computed, methods 以及對外的 props, events。在這個示例中,咱們會用 activeIdx 做爲核心狀態,全部的 API 也都會圍繞這個狀態命名。code

非受控模式

如上文所說,非受控模式指的是使用者不須要關心控制組件的狀體,徹底交由組件內部維護。htm

所以咱們的 API 會包括:

{
  props: {
    defaultActiveIdx: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      localActiveIdx: this.defaultActiveIdx
    }
  },
  methods: {
    handleActiveIdxChange(idx) {
      this.localActiveIdx = idx;
      this.$emit("active-idx-change", idx);
    }
  }
}

localActiveIdx 是咱們用來存放 active index 的組件內 data,對於非受控模式而言,雖然不但願在外部維護狀態,可是仍有可能但願在外部決定初始狀態,因此咱們用 defaultActiveIdx 這個 props 決定 localActiveIdx 的初始值。

以後當咱們用 v-for="(tab, idx) in tabs" 指令生成全部的 Tab 時,就能夠經過 idx === localActiveIdx 的方式判斷當前 Tab 是否 active,再經過 @click="handleActiveIdxChange(idx)" 就能夠實現對 localActiveIdx 的更新。

一樣的,咱們也能夠經過 {{ tabs[localActiveIdx].content }} 展現 active Tab 的內容。

須要注意的是在 handleActiveIdxChange 的事件處理中,咱們也 emit 了 active-idx-change 這一事件,這樣能夠方便外部在不須要管理組件狀態的同時也能夠與組件狀態保持同步。例如咱們但願將 active Tab 反映在 URL 中,就能夠在外部監聽 active-idx-change 這一事件,並將當前 index 同步到路由中,在將路由中獲取到的 index 做爲 defaultActiveIdx 傳入,就能夠實現 URL 和 Tabs 的同步。

受控模式

對於受控模式來講,咱們能夠理解爲 active index 是外部傳入的 props,由外部自行維護其狀態。

所以咱們只須要添加以下 props:

props: {
  activeIdx: Number
}

因爲咱們已經有對外 emit 的事件 active-idx-change,因此外部用如下方式就能夠用一個 data 屬性 externalActiveIdx 維護對應狀態:

<tabs
  :tabs="tabs"
  :activeIdx="externalActiveIdx"
  @active-idx-change="this.externalActiveIdx = $event"
/>

固然因爲在這種模式下外部對狀態有徹底的控制權,因此在 active-idx-change 的事件處理中也能夠作更爲複雜的判斷,例如是否容許激活目標 Tab 之類的校驗。

而在 Tabs 組件內部,咱們還須要作一些小的修改。在受控模式中,咱們全部狀態相關的處理都是直接使用 localActiveIdx,而如今咱們的邏輯應該變爲「若是存在 activeIdx props,則使用,不然使用 localActiveIdx」。

爲了保證以上邏輯不會讓咱們的組件內部實現變得複雜、易錯,咱們引入一個 computed 屬性:

computed: {
  _activeIdx() {
    return this.activeIdx || this.localActiveIdx;
  }
}

這樣咱們就能夠把狀態相關的判斷改成經過 idx === _activeIdx 判斷一個 Tab 是否爲激活狀態,也經過 {{ tabs[_activeIdx].content }} 展現 active Tab 的內容。

一樣,咱們在 handleActiveIdxChange 的方法內部也能夠增長一個判斷,若是存在 props aciveIdx 則不更新 localActiveIdx

handleActiveIdxChange(idx) {
  if (this.activeIdx === undefined) {
    this.localActiveIdx = idx;
  }
  this.$emit("active-idx-change", idx);
}

在一些更復雜的組件中,可能會頻繁判斷是否爲受控模式並作不一樣的處理,這時候經過 this.activeIdx 這樣的核心狀態 props 是否傳入來判斷是否爲受控模式是一個不錯的實踐。

總結

最終咱們爲 active index 設計的完整 API 以下:

{
  props: {
    activeIdx: Number,
    defaultActiveIdx: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      localActiveIdx: this.defaultActiveIdx
    };
  },
  computed: {
    _activeIdx() {
      return this.activeIdx || this.localActiveIdx;
    }
  },
  methods: {
    handleActiveIdxChange(idx) {
      if (this.activeIdx === undefined) {
        this.localActiveIdx = idx;
      }
      this.$emit("active-idx-change", idx);
    }
  }
}

經過這種 API 設計方式,可讓咱們設計的基礎組件使用方式更一致,拓展性更強,不管是開發仍是使用時思路也會更加簡潔清晰。

相關文章
相關標籤/搜索