从人类行为的角度理解状态管理
栏目: JavaScript · 发布时间: 6年前
内容简介:人从出生到死亡走的这段路程,称为生命周期。应用从启动到关闭经历的这段过程,也称为生命周期——因此这是一个仿生概念,基于相应结构的应用也会有与人类相似的行为特点。初接触时,我们会为如何去更好的在这个过程中去实践
人从出生到死亡走的这段路程,称为生命周期。
应用从启动到关闭经历的这段过程,也称为生命周期——因此这是一个仿生概念,基于相应结构的应用也会有与人类相似的行为特点。
初接触时,我们会为如何去更好的在这个过程中去实践 状态管理 焦头烂额,纠结于不同架构的各个节点对应的职责,特别是在涉及异步和副作用的处理的过程中,很难快速找到一个“最佳实践”。那么,既然应用具有仿生设计,我们自然可以从基于作为人类的自身的角度去理解它。
状态(State)
先来看一张图:
从上图可以看到,如果将一些过程和行为抽象出来,人与 App 是具有高度的相似性的。
其中:
Stage | Human | App |
---|---|---|
Born/Startup | 新生儿出现,有了人类初始特征,大脑开始工作后,便逐渐的产生 本能、认知、意识、反应、情绪 等元素,它们便是我们的 初始状态 。 | 注册进程或线程,静态资源加载,支撑应用的各个要素就绪,根据缓存或预置规则定义应用的 初始状态 |
EE/FID | 早期教育,为投身到更复杂的环境层层递进的准备 | 获取初始数据,为更定制化的运行策略做准备 |
WL/RI | 不断学习、工作、修身、社交, 处理事件 | 在运行过程中与后台交互、与用户交互、视图与状态的同步、与其他应用的交互, 处理事件 |
Retirement/Unmount | 退休、处理各项工作时的羁绊、夕阳红、遗产处理 | 卸载服务、处理副作用、也启动一些服务(最令人发指的)、缓存等资源的处理 |
从书面意思理解:
状态是人或事物表现出来的形态。是指现实(或虚拟)事物处于生成、生存、发展、消亡时期或各转化临界点时的形态或事物态势。
简单来说,就是 任一对象在特定状况下的存在形态 。 具体到人,可以是情绪、职业、资产等。 具体到应用,可以是一些布尔值、状态码、具体数据等。
由这些可以对对象进行描述的单元结合,就构成人或应用。
全局状态(Global State)
- 在人类的角度来看,我们刚形成胚胎便会有了 性别、肤色、瞳色 等基因决定的特征,我们因而为人,这些生理特征体现在我们生命历程中的任何一个时刻。
- 对于应用来说,静态资源被运行环境执行的过程,就好比胚胎的生长过程,然后到了初始化状态容器(Store)的时候,便开始获取初始数据,这些数据就包含了一系列初始状态,它们可以是 可用性、登录状态、角色策略、颜色主题、语言环境 等等。
在人身上,本能、认知等因素往往是伴随一生的,他们的有效性是覆盖到所有其他情形下的,比如我吃饭的时候不知道美国总统是特朗普,那么我上厕所睡觉的时候同样不会知道;有一天我在吃饭的时候得知了这个消息,那么从此我上厕所睡觉的时候同样也知道了。
在应用中,登录状态、颜色主题、语言环境等也有这样的特点。在一个应用周期内,每一次修改这些状态,都是会应用到全局的,至少是主体同步。
对于这些状态,我们统称为 全局状态
局部状态(Module/Feature/Partial)
我们上厕所的时候,一般会向”抽纸盒“发起请求,然后拿几张纸,这是在如厕时的”后事“预备状态;而我们在大街上则不会同样拿着纸准备擦屁股,我们可能因为口渴拿着水,因为购物拿着包袋。
在应用里,具有不同职责的页面展示的内容也不同,我们不会没事儿在首页展示用户的优惠券详情,也不会没事儿在课程详情页展示用户余额。
在这些具有不同职责的场景下“独有”的状态,我们称为 局部状态
全局状态和局部状态的划分
两种状态的特点其实很好理解,其实它们的划分才是难点。
状态本身就具有“全局性”,因为状态一旦拿到,那么不管是否是在对应场景,它都存在于本次生命周期中,它随时可能在新需求来到的时候被其他场景需要,而你不一定总是能够事先知晓。
当然了,像用户信息、登录时间、语言环境等因素是很好区分的,但更多更细的状态是否需要放到全局,或者说由公共性更高的模块来管理,就很难一次性下定论了。
因此界定某个状态的类型,并不是一蹴而就的,而是要在长期的迭代中进行总结。
对于人来说,这个问题不算是个问题,因为我们拥有强大的复杂问题处理能力,而计算机几乎是没有这样的能力的,它们处理问题的方式都是人为定义的,即便 AI、ML 等技术蓬勃如今,也远远达不到人类的思维水平。
我们以一个真实应用里的一些实现为例:
这是 WPS精品课产品的移动端 Web App。
以职责划分
第一张图中,在两个功能不同的 页面(场景) 中都出现了 分类 这一数据形式,并且数据是一致的,也就是说,这两个页面出现了公共状态。而这两个公共状态总是覆盖全局的,那么我们就应当将它们提升到更高一层的状态模块中。
注意,提升到更高一层并不意味着提升到 global 的级别。有时候,可能分类并不是一个简单的数据,它可能是根据不同的用户策略进行展示的,对于不同的用户级别,分类可能会呈现多态(比如普通用户看不到 VIP 专属的类别)。从前后端交互的角度来看,分类相关的接口往往也是独立于其他数据的。因此,当分类具有了一定的复杂性和具体规则,它应当有属于自己的管理单元,使得数据的吞吐和处理有更加清晰的思路,而不是去破坏性的影响全局状态的职责。
就像人类在左右脑的统一调配下,有视觉中枢、听觉中枢、运动中枢。如果它们产生了紊乱,使得脑功能失调,人就会出现各种各样的问题,如少儿多动症、认知障碍等。
以路由划分
为什么是一个圆圈呢?
其实这个页面虽然常见,但在状态管理中,它的确比较特殊,因为它既是一个路由单元,又是一个状态单元。如果说一个页面通常是由多个状态组合而成的,那么“我的”页面可能就只需要一个状态就够了——即用户状态。它往往包含了用户基本信息,信用卡信息,功能定制等——是的,往往我们就把它们放在 global 中。当然根据应用的类型和复杂度不同,用户信息也可能划分成若干单元,因此,如此形式的 按路由划分 ,其实是 按职责划分 的一个变种。
然而,还有一种情况就不同了。比如某些活动型页面,它们可能只包含一些运营内容,有一套自己的逻辑和交互,独立于任何其他的页面,但页面本身的生命周期或许只有几个星期甚至几天,这时候可能就没有必要为其设计和维护一个状态单元了,得不偿失。
好比一个人要出国旅游一段时间,立马给手机开了一系列便捷的境外服务,但回国后往往就立马停掉了,而不是为这些长期用不到的东西付费。
改变状态
我们已经探讨了关于 状态 的一些基础内容,现在问题来到了如何对状态进行“改查”。
首先,状态是 对象 在 特定环境 和 具体时机 下的某种存在表现,随着环境和时机的改变,它便会发生相应的更新。
我们拿“时间”举例,它是最客观最不可阻挡的状态流。
对于人类,在一个时间单元内(指,年、月、日等)我们会根据具体的时间点调整我们自身的状态——睡觉、起床、工作、小憩等等;
对于应用,最常见的就是一些即时服务的开关。比如某购物 App,白天一直到晚上九点会有针对会员的“一小时送达”的服务,但过了这个时间点,这个服务便进入休眠状态。
那么从外部条件改变到对象自身的状态更改,中间经历了什么呢?
我们来看几个当下炙手可热的前端数据流模型:
So You See!
这里面似乎有一个恒定的范式:
Action - Update state
Store 和 State
Store
是状态中心,而 State
就是这里面的一个个状态集合。
在 Flux 和 Redux 的模型中,我们可以显式的看到 Store
节点,而 Mobx 和 Vuex 里这个节点似乎由 State
代替了。这个是由于两种风格不同的状态声明方式导致的,这里以最常见的 Redux 和 Vuex 的 Store
构建方式为例:
可以看到,源于 Flux 思想的 Redux 的 Store
声明过程更像是将各个独立的状态单元(Reducer,详见后文)整合(combine)在一起,形成一个自 Store
而下的状态树,实现单向数据流。其工作特点是所有的行为都要经过 Store
。 PS :其中的 Action Handlers
的实际形式其实是 switch
语法下的一个个模式,并非具体函数或方法,这里只是根据其职责进行了类比理解。
而 Vuex 呢,其实也是源于 Flux 的,但它吸取了 Redux 样板代码繁琐的“教训”,将 combine
的过程用声明的方式规避了,同时将状态单元细分成各个 module
,每个 module
包含了一套 State
和对应的规则,比起 Redux 来说,是一种“高类聚、低耦合”的方案,节省了一些声明和管理状态的成本。工作特点眼下就是各个 module
各司其职,只影响自己的 State
。
然而,Vuex 在实际的工作过程中,其实还是由 Store
作为中心进行分发,只是其构建方式让我们觉得 Store
并没有被总是调起。
总的来说, Store
的地位如同我们的大脑,我们的任何决策、行为都会经过大脑进行评估、加工。但随着某种刺激的不断触发,其对应的反应行为也会出现得越来越快,等到形成相对固定的范式的时候,我们可能就感觉不到思维在这个过程中的行动了,体现为“反应快”。 对于普通应用开发来说,我们则可以直接定义这种“范式”,这更有利于我们整体上的把握应用的规则,强化和优化应用的逻辑。良好的状态管理实践会让应用更加高效,也更好维护。
Action
在上面的几种数据流模型中,在对状态进行修改前,都会经过一个叫 Action 的节点,这个节点我们可以理解成 行为 。
Action 即是向 Store
发起更新请求的最小单元。
它的结构通常是:
// pureObject const myAction = { type: 'GO_TO_BED', payload: Medicine.Estazolam } // functional const myFunctionalAction = arg => { let payload // TODO return { type: 'GO_TO_BED', payload } } 复制代码
其中:
- type: 对这个行为的描述,Store 根据这个字段去寻找对应的处理方案
- payload?:荷载,携带实现该行为要使用的一些数据
这个比较好理解,要做一件事,得先明确这是什么事,如果有需要还要带上相应的东西。比如:大便要带纸;而小便可能带,也可能不带;只是去洗手就什么都不用带了。
可见, Action
最终只是一个对象,那它如何传递给 Store
呢?
Dispatch
我们完成一个“刺激——反应”的时候,通常先是神经末梢收到接收刺激,然后大脑得到神经末梢发来的信息,做出反应。在这个过程中,携带信息的介质被称为神经递质,它活动在突触之间。
而在各类应用状态管理的模型中,通常都会有一个 dispatch
方法,它就声明在 Store
上,负责调用各个 Action
,然后由 Store
上对应的分发机制进行处理。同时,异步 Action
的实现,即是将这个方法作为参数传给对应的 ActionCreator
,然后等到异步工作流完成后,将最终的 Action
传递给 Store
。例如构建一个 redux-thunk
中的异步 Action
:
const asyncAction = id => { // 集成 redux-thunk 后,redux 会将 dispatch 等一系列方法传递给 actionCreator 返回的函数,供异步工作完成后 actionCreator 能配合 Redux 进行工作 return dispatch => { fetch('/getData?id=' + id) .then(response => response.json()) .then(data => { dispatch({ type: 'SET_DATA', payload: data }) }) } } 复制代码
Reducer / Mutation
现在到了更新状态的时候了,简单抽象出来就是 newState = updatedState
,不难理解,主要看下实现。
在 Flux 和 Mobx 的模型中,对状态的修改比较直接,不多赘述,那么“矫情”一些的 Redux 和 Vuex 是如何实践的呢。
我们从其实现上分别说明它们的作用
Reducer
先来看一个简单 Reducer 实现:
function myReducer (state = { age: 1 }, action) { switch (action.type) { case 'HappyBirthDay': return { age: ++state.age } default: return Object.assign({}, state) } } 复制代码
Reducer 的工作方式是,接收一个 Action
,然后在 switch 流中匹配 action.type
,做出相应处理,然后返回一个 新的对象 。其源码可以看 这里
为什么是新的对象呢?
因为 Redux 是一个实践 函数式编程(FP) 理念的库。函数式编程有个要素就是——纯函数不能有 副作用 ,而副作用简单概括来说就是 对该函数内部环境以外的变量进行了修改、销毁等操作 。
回过头来,在 Redux 中,Reducer 原则上就是一个纯函数。
这有什么意义呢?
答案是 数据不可变 ,它也是函数式编程中的一个要点。
函数式编程认为 可变 和 共享 是“万恶之源”,原数据的更新只能通过返回新的数据。否则随意修改的数据可能让应用产生难以预料的问题,而“共享”加“可变”带来的副作用更是容易容易让我们得到错误并且难以捕获的内容。
Mutaion
Vuex 是在 Mutation
中修改状态的,其代码一般如下:
// module export default { //... mutations: { SET_DATA (state, payload) { state.data = payload } }, actions: { async getData ({ commit }, payload) { const res = await api.data.get(payload.id)() commit('SET_DATA', res.data) } } } // component export default { mounted () { store.dispatch('getData', this.id) } } 复制代码
其中,触发一个 Action
依然是通过 dispatch
方法,然而,修改状态为什么需要 commit
一下呢?
其实我们直须将 mutations
和 actions
中的各个成员都理解成 Action
,因为你也可以直接在 Store
上调用 commit
来修改状态。 commit
的职责相当简单,就是修改本地状态。
而 Action
的职责在于可以实现异步流和 Action流 (在action中dispatch又一个(可以是自己)action),最后提交到 Mutation
中来修改状态。但从源码来看,其实 Vuex 同样赋予了 Action
改变状态的能力,它将 State
作为第一个参数的其中一个属性传递给了 Action
,其目的是为了你可以使用 State
上的状态和数据,原则上这是只读的,但结合 MVVM 的特点,你在这里修改它,同样也会引起视图的改变。
源码里是这样写的:
function registerMutation (store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { // store 会调用 commit 方法来启用这个 mutation,并且只传入了本地 state 和一系列荷载 handler.call(store, local.state, payload) }) } function registerAction (store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { // action 就比较厉害了,这么多... let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) } 复制代码
可见,你甚至可以不顾一切的在 Action
中修改全局的 State
。
在行为心理学中,其中一种行为的分类方式即是将行为分为 外显行为 和 内隐行为 。外显行为就是我们肉眼可见的,有明确外在表现的行为;内隐行为则是外表之下,发生于机体内部的情绪变化、思维运作、激素分泌等不会彰显出来的行为。但往往我们改变大脑中的某个状态,使之显于或不显于我们的姿态的时候,这些内隐行为是不可能避免的,因为我们的大脑活动就是各类递质工作下的一系列的化学反应。
这与 Action
和 Mutation
的关系很像,Vuex 告诉我们修改状态的唯一方法是提交 Mutation
,也就是说你不应该在 Action
中的直接修改 State
,就好像我们的外显行为总是要经过内隐行为来提交给大脑一样。
当然了,既然职责不同,角色肯定就不同,理解成 Action
是为了我们便于理解。
再次提醒,Vuex 明确告诉我们改变状态的唯一方法是提交 Mutation
,因此我们应当遵循这个原则,将 Action
中的各个响应式引用视为只读,以保证应用的逻辑性不会被破坏。(当然,直接 commit 啦,想想 mapMutations
方法!)
总结
通过一张图来梳理一下状态管理与人类行为的共通之处:
PS
这不是一篇论述,状态管理也远远不止这几种模型,小生仅仅在 前端应用 及其比较 有代表性的状态管理方案 的背景下分享了这个角度,因此这个理解方式必然有一定的局限性或者是未被完全论证。如果能帮助到读者,小生就非常荣幸了。
理解状态管理的方式有很多,这只是其中一种思路,或许这种思路能在应用开发的同时也锻炼我们的逻辑思维。
同时,实际场景下的状态管理必然一个更加复杂的东西,随着应用的规模和深度越来越大,我们需要更深刻思考它,如何划分模块?如何共享模块?如何构建容器?如何提升效率?这都是需要逐步探索的,当然,最快的方式,就是在已有的状态管理范式中思考,组织、优化。
最后,要记住的是:
你可能不需要状态管理
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 从运维的角度理解 memcached
- 从运维的角度理解memcached
- 从源码角度深入理解Glide(下)
- 从工程的角度,重新理解 Python
- 以“一个公式” 的角度理解原型链
- 从 Vue 源码角度来理解虚拟 DOM
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
现代信息检索
(西班牙) Ricardo Baeza-Yates、(巴西)Berthier Ribeiro-Neto / 机械工业出版社 / 2011-3 / 78.00元
本书不仅详细介绍了信息检索的所有主要概念和技术,以及有关信息检索面的所有新变化,而且其组织使读者既可以对现代信息检索有一个全面的了解,又可以获取现代信息检索所有关键主题的详细知识。本书的主要内容由信息检索领域的代表人物Baeza-Yates和Ribeiro-Neto编写,对于那些希望深入研究关键领域的读者,书中还提供了由其他主要研究人员编写的关于特殊主题的发展现状。 与上一版相比,本版在内容......一起来看看 《现代信息检索》 这本书的介绍吧!