React Component 的数据管理
根本上来说,React 和 Vue.js 这类 web 框架是为解决 View 层面的问题而出现的。
view = render(data)
开始于页面 Page
一个页面自身是一个整体,概念上对应的就是一整段 HTML 代码(HTML + CSS + JavaScript)。
分而治之成 Component
通过把页面划分成不同的部分,可以实现分工。
分工的同时有机会让这个子部分在别的地方重用。
这是组件(Component)存在的意义。
内部的数据:状态 State
一个组件如果是动态的,那么可能需要一个地方存储数据和状态。
我们用“状态”(State)这个词专指组件用于维护自身状态的数据。
外部的数据:属性 Props
一个组件为了尽可能的重用,需要一定的灵活性。 这种灵活性意味着需要在使用这个组件的时候传入一些参数。这些参数可能是为了影响组件的逻辑,也可能是传递初始状态。
树形的结构 Tree
把一个视图(View)划分成组件(Component),一个组件又可以继续划分为子组件。如此重复。最后整体的结构是一个树形结构。
上层节点维护一组子节点,通过子节点的 props
属性传递参数,影响自己点的行为。
view = render(props, *state)
问题简化之后,就是如何解决 view = render(props, *state)
这个问题。
这里用加了星号的 *state
表示状态数据实际上是在函数内部的。一般情况下,内部的东西不会影响外部。
但因为状态的改变需要触发 view 的更新 —— render
的再次调用,所以实质上仍然可以认为 state 是一个隐含参数。
响应式 React
说一个 View 是 React 的,实际上是说
React 的含义是当外部数据 props 和内部状态 state 发生改变时,视图 View 会自动进行更新。
更新的方法之一是用新的数据重新渲染视图;难点是重新渲染的效率问题。
从函数角度看,就是用新的数据重新调用 view = render(props, *state)
。
React 框架是通过所谓的 Virtual Dom 来进行增量更新来提高重绘效率。
React on Component State
State 数据因为是在函数内部的,如何让函数内部的数据变化触发这个函数的重新执行是需要解决的关键问题。
这不是简单的递归调用,因为 State 的改变通常是异步的。
如何解决这个问题和程序原因呢所支持的特性有关。不同的解决方案导致的权责划分也不一样。
React on Component Props
一个 Component 的 props
可以认为是来自于其上层组件的 state
。
对于顶层组件,我们可以认为是 props=null
。
Component Props = State
当组件的 state 变化导致 React 发生的时候,其中的一些 state 数据作为子组件的 props
,触发子组件的 React on Props。
我们可以认为,实际上要解决的问题只有 React on State。
谁主张,谁负责
最直接的解决方案下,谁要求更新 State,谁来负责更新 View —— 当然为了简单,可以把更新试图的操作弄得简单一些。
state.count = 789; // 更新状态
redraw(); // 更新视图
“谁主张,谁负责”这个逻辑是最根本的。你可能需要更新视图,也可能不需要,自己看情况办。
对于 Web 页面来说,很多时候问题在于几乎 State 的改变总是伴随着 View 更新的需求。
setState()
setState()
是很容易折中出来的一个方案。如此以来,通过 setState()
更新状态的同时,自动触发视图的更新。
setState({count: 789});
function setState(newState){
state = newState; // 更新状态
redraw(); // 自动调用 `redraw()`
}
React on Data
从 state.count = 789
到 setState({count:789})
虽然解决了问题,但毕竟还是似乎多了个没啥实际用处的东西——对于数据更新这个主要任务来说。
注意到 setState()
之所以可以解决问题,是因为它是个函数,而函数定义的时候可以加入自己的代码。这是关键。
如果 state.count = 789
可以等效于 state.setCount(789)
并且我可以自己定义这个 setCount()
的函数就可以解决问题了。
新的 JavaScript 语法确实允许这种操作。
Object getter 和 setter
JavaScript Object 支持通过对应的函数来读取和修改成员。
let state = {
get count() {},
set count(value) {}
};
如此以来,把 redraw()
放在 set count(){}
这个函数里就一样解决问题。可以使用 state.count = 789
这种看起来仍然是直接赋值的形式了。
MobX 的核心内容就是这种思路。
自上而下的单向数据流
按照上面本来的推演,应该是
- 组件的 props 来自于父组件,自己不能修改
- 组件的 state 由自己维护,作用范围局限于自身内部
这即所谓的的单向数据流。数据该动只能从顶层组件逐级传下来。
全局数据 Global Data
问题在于,很多时候,虽然我们对视图进行了模块划分,但数据实质上都是依赖于一个全部数据。
最简单的角度看,页面是对后台数据的展示。后台数据往往作为一个整体一次性传递给前端页面。
可以说数据最初的形态常常是一个 Global Data,需要被多个 Component 使用。
这时如何把相应的数据传递给相应的 Component 是一个需要解决的问题。
集中数据的 State Store
用一个全局的状态管理器来管理就成了很自然的想法。
Redux 就是这种思路下的产物。
MobX 实际上也成了全局的 State Store
本质上说,view = render(props, *state)
变成了
view = render(store)
集中改动的 Reducer
如果把用于改变数据的代码从组件内部移动到 State Store 里,就是 Redux 的 store.dispatch()
,
它通过一个 reducer()
函数把数据改动的代码集中到一起。
State Store 的两个参数
从上面的讨论可以看出,“集中管理”数据可以说几乎是一种必然。
集中管理数据需要解决的问题是
- 数据存在哪里?
- 数据成员的索引是什么?
用函数表示就是:
access(store, index)
隐式参数的 useState()
React Hooks 引入了 useState()
这样的概念来管理状态数据。
但从形式上,useState()
可以认为是一个全局函数,访问的自然是“集中管理”的数据。
当可以省略掉 access(store, index)
的两个参数,而是隐式地确定时,就是 useState()
了。