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 设计方式,可让咱们设计的基础组件使用方式更一致,拓展性更强,不管是开发仍是使用时思路也会更加简洁清晰。

相关文章
相关标签/搜索