Vue数据设计初探

409 阅读7分钟

只玩了不到几天vue写的,如果有什么不同意见,你是对的,我是错的。

基于数据驱动视图的理念,在进行开发设计时我们将数据和视图分离。 前端的数据设计分为两个部分:全局数据设计组件内数据设计。在vue中全局数据设计考虑的是vuex的数据划分构建。

数据划分原则

首先我们需要知道什么样的数据需要作为全局状态去由vuex管理?这种数据需要满足几个概念:

  • 需要跨模块共享的数据。
  • 当需要将某个模块划分成更细的几个子模块,且不是被某个子组件独占的数据 所以第一步的数据归属划分,实际上是组件划分的内容。它要求的是非[单元式]的组件独占的状态都应该作为全局状态来存放。

哪些数据我们很有必要存放在vuex中?根据这个原则我们可以得出一些例子,比如:

userName, userId, globalLanguage, ... // 一些适用于全局的配置信息
parentList,kidList, ... // 被各个组件反复使用或者请求的数据
xxx, ... // 一些在不同组件中可能会使用到的数据 

而一些单位组件内的状态,比如Input和Select组件绑定的value,某个子组件的isVisible等,就应当作为组件的内部状态管理,不必存放在全局中。

Vuex数据设计

vuex的数据设计是依据它的运行流程来决定的,这方面资料很多,就不赘述。简单来说state是数据存储核心,由getter取值,通过mutation进行回调修改。 实际上这是一个前端的类全栈模型:state是数据库,getter是sql查询语句,mutation是相应的C、U、D操作。所以数据设计我们也按照state → getter → mutation的层次来进行设计。

state设计原则:

state本质是一棵k叉树,对于它的优劣评判,我们应该将其转化为对几个显性指标的落实:

  • 尽量不存重复信息;
  • 不存有计算依赖的信息(比如A可以通过B计算得来,那它没有任何理由出现在state中);
  • 对于常见数据的访问,时间复杂度要尽量往o(1)靠近.

由此我们先来看一些基础概念和手段:

uuid在全局数据中的应用

uuid作用很简单,是生成一个全局唯一的id。它的意义在于可以给每个独立的元素赋予一个唯一索引。

比如对每一个实验对象,每一条业务线,甚至每一个前端资源,我们都可以给它一个id值,从而在getter中以byId的方式去确保准确拿取(不通过确定的唯一索引的拿取方式都是不利于维护的)。

前驱式的存储思想

state中的数据有大量的父子关系存在,假设我们有一类数据叫parent,每个parent有若干固定字段的信息,而kid又是依托于parent的子元素(相当于文件夹和文件的关系):

parent:{
	name,
	size,
	otherInfos,


	kid: [
    	{
		name,
		otherInfos,
	},{
		name,
		otherInfos,
	},
	// ...
	]
}

如果以这种父子关系直接进行存储,会带来几个问题:

  • 子元素只能通过索引之类的进行定位,如果进行CURD操作代价是昂贵的;
  • 当数据复杂时树的层数越来越深,而且必须通过父元素才能定位到子元素,维护和错误收集都将变得非常困难,可能随时面临大量的undefined一类的报错。

我们希望的是,既保留元素间的逻辑关系,又保持高效率的state访问与更新。这要求重要的数据尽量扁平化,而且不通过数组下标索引这种不可靠的方式对数据进行访问控制,我们可以有如下思路:

  • parent和kid作为同一级存储,让两种重要数据的访问互相不影响;
  • 每个元素用uuid作为对象的唯一索引,并用allIds集中管理;
  • 在子元素kid中以byParentId的形式存储父元素关系。

为什么不在parent中存byLayerId?因为它是多对一的关系,存储起来没有意义,所以在树状的state中我们应该是前驱式地存储,这样不管是需要查找某parent下的kid信息还是某kid的归属,在byParentId中都能解决了。那么不妨用这种存储方式:

parent: {
	byId: {
		'22e47488-3afb-4174-89ab-ff88225e84ee': {
			name,
			size,
			otherInfos,
		},
		'6ecaf2a8-5feb-4c33-9900-60c9cbfd033e': {
			name,
			size,
			otherInfos,
		},
	},


	allIds: ['22e47488-3afb-4174-89ab-ff88225e84ee', '6ecaf2a8-5feb-4c33-9900-60c9cbfd033e']
},


kid: {
	byId: {
		'bcadff22-0f5b-4b99-b7db-c4ff134ad205': {
			name,
			otherInfos,

		},
		'80f1a5d0-b405-4fca-b6c3-07b0ba288d11': {
			name,
			otherInfos,
		},
	},

	byParentId: {
		'22e47488-3afb-4174-89ab-ff88225e84ee': ['bcadff22-0f5b-4b99-b7db-c4ff134ad205', '80f1a5d0-b405-4fca-b6c3-07b0ba288d11'],
		'6ecaf2a8-5feb-4c33-9900-60c9cbfd033e': [],
	}

	allIds: ['bcadff22-0f5b-4b99-b7db-c4ff134ad205', '80f1a5d0-b405-4fca-b6c3-07b0ba288d11']

},

当然扁平化的存储也要考虑实际,state总不能做成只有两层的多叉树,具体如何追求深度和广度的平衡要根据业务场景中的数据使用权度来决定。

mutation设计原则:

mutation是更新state的唯一途径,在设计mutation时主要的几个注意事项如下:

  • 表意准确,逻辑划分清晰,用大写常量来描述事件类型,遵守相应的命名规范;
  • 在组件中提交变动到mutation;
  • 异步流程必须封装在action中提交给mutation,最终对state的更新必须是同步操作。

一个mutation的例子:

// mutation-types.js
const FETCH_PARENT_LIST = 'FETCH_PARENT_LIST'
// ...


// mutation/parent.js
FETCH_PARENT_LIST: {
   SUCCESS: (state, { payload }) => payload.parent.byId
}
// ...

其中要注意的点是,每当state变动了,必须操作到每一份数据。比如删除了一个kid,我们需要从kid.byId、kid.byParentId、kid.allIds中删去所有相关信息等,确保相关的状态是统一的。

getter设计原则:

getter实际上可以理解成基于state的computed属性,有时需要一些特种数据的时候,我们无法直接从state中取得一致结构的数据,这时就可以通过getter来定向组装。

比如我们需要所有kid的name属性合集,或者需要一些指定属性值的parent,或者需要带有所有子kid完整信息的parent,这时就可以通过getter来取得对应的值。

组件内数据设计

组件内的数据设计有data和props之分。与react不同的是vue还有计算属性computed来对组件状态进行进一步的区分。设计的过程我们遵循以下的原则:

  • 如果这个状态并不会发生变化,那么它不应该作为data;
  • 如果它可以由其他已有的data或props计算得出,那它应该作为computed而被使用;
  • 如果它是应当由父元素流下,那它应该作为props而被使用。

data设计的冗余臃肿,对维护是非常不利的,而且会带来大量不必要的重复计算和资源消耗。

data和props的设计区分实际上是对于单项数据流的落实,单向数据流对于MVVM开发的重要意义已经是社区公认的了。

参考资料:

组件设计思想范式,vue同样适用

单向数据流,通过简单demo理解意义