从人类行为的角度理解状态管理

栏目: 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 而下的状态树,实现单向数据流。其工作特点是所有的行为都要经过 StorePS :其中的 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 一下呢?

其实我们直须将 mutationsactions 中的各个成员都理解成 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

在行为心理学中,其中一种行为的分类方式即是将行为分为 外显行为内隐行为 。外显行为就是我们肉眼可见的,有明确外在表现的行为;内隐行为则是外表之下,发生于机体内部的情绪变化、思维运作、激素分泌等不会彰显出来的行为。但往往我们改变大脑中的某个状态,使之显于或不显于我们的姿态的时候,这些内隐行为是不可能避免的,因为我们的大脑活动就是各类递质工作下的一系列的化学反应。

这与 ActionMutation 的关系很像,Vuex 告诉我们修改状态的唯一方法是提交 Mutation ,也就是说你不应该在 Action 中的直接修改 State ,就好像我们的外显行为总是要经过内隐行为来提交给大脑一样。

当然了,既然职责不同,角色肯定就不同,理解成 Action 是为了我们便于理解。

再次提醒,Vuex 明确告诉我们改变状态的唯一方法是提交 Mutation ,因此我们应当遵循这个原则,将 Action 中的各个响应式引用视为只读,以保证应用的逻辑性不会被破坏。(当然,直接 commit 啦,想想 mapMutations 方法!)

总结

通过一张图来梳理一下状态管理与人类行为的共通之处:

从人类行为的角度理解状态管理

PS

这不是一篇论述,状态管理也远远不止这几种模型,小生仅仅在 前端应用 及其比较 有代表性的状态管理方案 的背景下分享了这个角度,因此这个理解方式必然有一定的局限性或者是未被完全论证。如果能帮助到读者,小生就非常荣幸了。

理解状态管理的方式有很多,这只是其中一种思路,或许这种思路能在应用开发的同时也锻炼我们的逻辑思维。

同时,实际场景下的状态管理必然一个更加复杂的东西,随着应用的规模和深度越来越大,我们需要更深刻思考它,如何划分模块?如何共享模块?如何构建容器?如何提升效率?这都是需要逐步探索的,当然,最快的方式,就是在已有的状态管理范式中思考,组织、优化。

最后,要记住的是:

你可能不需要状态管理


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

现代信息检索

现代信息检索

(西班牙) Ricardo Baeza-Yates、(巴西)Berthier Ribeiro-Neto / 机械工业出版社 / 2011-3 / 78.00元

本书不仅详细介绍了信息检索的所有主要概念和技术,以及有关信息检索面的所有新变化,而且其组织使读者既可以对现代信息检索有一个全面的了解,又可以获取现代信息检索所有关键主题的详细知识。本书的主要内容由信息检索领域的代表人物Baeza-Yates和Ribeiro-Neto编写,对于那些希望深入研究关键领域的读者,书中还提供了由其他主要研究人员编写的关于特殊主题的发展现状。 与上一版相比,本版在内容......一起来看看 《现代信息检索》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具