@gyyin
2020-01-12T00:01:13.000000Z
字数 7486
阅读 422
慕课专栏
小溪汇聚江河,河流汇聚成湖泊。在前端开发中,数据就像水流一样从顶层组件流入无数子组件中。
前面我们介绍了 React 的一些用法,可以看到组件化让代码复用变得更加方便,但也带来了一个问题,那就是组件之间有数据共享该怎么做?
比如现在有个筛选组件和列表组件,当在筛选组件中选择一些筛选信息之后,列表组件需要根据这些筛选信息展示筛选过后的数据。
这样就需要把筛选组件中选择的筛选信息传给列表组件,也就涉及到了组件通信。
尤其是在组件嵌套深入、业务复杂的应用中,组件之间通信比较多。这引发了一个令 React 开发者头疼的问题,就是在 React 中如何做状态管理?
接下来这三篇文章将会详细介绍 React 中的状态管理,这篇起到抛砖引玉的作用,后面两篇将会深入到 Redux 和 Mobx 的原理来带领大家搞定 React 状态管理。
在我们开发中,涉及到的组件通信一般有下面这几种:
1. 父组件向子(孙)组件传值
2. 子(孙)组件向父组件传值
3. 兄弟组件之间传值
父子组件之间传值是最简单的一种。父组件向子组件传值,只需要把这个值通过 props 传给子组件就行了。这个值可以是任何类型的,原始类型、引用类型都可以当 props 传给子组件。
function Parent() {
return <Child name="child" />
}
function Child(props) {
return <h1>{props.name}</h1>
}
除了父组件向子组件传值,还有子组件向父组件传值,那么该怎么传呢?常见的场景是在 Modal 或者 Switch 组件中,父组件需要知道这些组件内部的开关状态。
这里涉及到两种方法,一种是通过一个 发布-订阅
模式来传值。在父组件做订阅,在子组件中合适的时机发布,这样就可以进行传值。
// 一个简单的发布订阅
let PubSub = {
callbackLists: {},
trigger(eventName, data) {
let callbackList = this.callbackLists[eventName]
if (!this.callbackList) {
return
}
for (let i = 0; i < this.callbackList.length; i++) {
this.callbackList[i](data)
}
},
on(eventName, callback) {
if (!this.callbackLists[eventName]) {
this.callbackLists[eventName] = []
}
this.callbackLists[eventName].push(callback)
}
}
class Parent extends Component {
state = {
status: 'close'
}
componentDidMount() {
PubSub.on('onOpenChange', (status) => {
this.setState({
status: status ? "open" : "close"
})
})
}
render() {
return (
<>
<Child />
<span>{this.state.status}</span>
</>
)
}
}
class Child extends Component {
state = {
open: false
}
changeStatus = () => {
this.setState({
open: !this.state.open
}, () => {
PubSub.trigger('onOpenChange', this.state.open);
});
}
render() {
<div>
<button onClick={this.changeStatus}></button>
</div>
}
}
当然这只是一种非官方的做法,这种方法很容易造成组件通信混乱,因为没办法很清楚地看出来组件依赖的订阅方和发布方在哪里。
除了这种方法,还有一种更常用的做法,即把函数当做 props 传给子组件,子组件在适当的时候调用这个函数,将父组件需要的值当做参数传给这个函数。
class Parent extends Component {
constructor() {
super();
this.state = {
status: 'close'
}
}
onOpenChange = (status) => {
this.setState({
status: status ? "open" : "close"
})
}
render() {
return (
<>
<Child onOpenChange={this.onOpenChange} />
<span>{this.state.status}</span>
</>
)
}
}
class Child extends Component {
constructor(props) {
super(props);
this.state = {
open: false
}
}
changeStatus = () => {
this.setState({
open: !this.state.open
}, () => {
this.props.onOpenChange(this.state.open);
});
}
render() {
return (
<div>
<button onClick={this.changeStatus}>点击切换状态</button>
</div>
)
}
}
在点击按钮的时候,会执行父组件传来的 onOpenChange 函数,父组件从 onOpenChange 拿到状态之后更新到 state 中,进而触发重新渲染。
看到这里,相信你也知道父组件怎么和孙组件通信了吧?父组件向孙组件传值,可以将 props 一层层传下去。
function Parent() {
return <Child name="parent" />
}
function Child(props) {
return <GrandSon name={props.name} />
}
function GrandSon(props) {
return <h1>{props.name}</h1>
}
而孙组件向父组件传值也是同样的道理,将需要执行的函数一层层传下去。在孙组件中执行这个函数,将需要的数据当做参数传过去。
function Parent() {
const onChange = (e) => {
console.log(e.target.value);
}
return <Child onChange={onChange} />
}
function Child(props) {
return <GrandSon onChange={props.onChange} />
}
function GrandSon(props) {
return <input onChange={props.onChange} />
}
上面的例子只是嵌套两层就已经比较麻烦了,但是如果组件嵌套过深,就要一层层传给目标子组件,这样会非常麻烦。
因此,为了解决这个问题,React 还提供了一个 Context API,可以实现跨组件通信。
通过 createContext 创建一个 Context 对象,这个对象提供了 Provider 和 Consumer 两个属性。
每一个 Context 对象都会返回一个 Provider
组件,它允许消费组件来订阅 context 的变化。
Provider
会提供一个 value 属性,将这个 value 传给消费组件。
每一个 Context 对象也会返回一个 Consumer
组件,它用来响应 context 的变化,这是一个 render props
的模式。
const context = createContext('');
function Parent() {
return (
<context.Provider value="parent">
<Child />
</context.Provider>
)
}
function Child(props) {
return <GrandSon />
}
function GrandSon(props) {
return (
<context.Consumer>
{
(value) => {
return <h1>{value}</h1>
}
}
</context.Consumer>
)
}
关于 Context 更详细的用法,在后面的《详解 React16 新特性》一文中会进行介绍。
兄弟组件通信稍微麻烦一点儿,除了上面的 发布-订阅
模式,如果想通信,就必须借助共同的父组件这个“中间桥梁”。
以一开始说的筛选表单和列表的通信为例,列表需要获取到筛选的信息,这里就要借助到两者的共同父组件。
首先要了解一件事,那就是组件之间的通信,一般都是伴随着响应某种用户的操作。比如用户选择了所有的筛选项后点击查询按钮,或者在用户每次修改选项的时候去主动查询。
我们就以用户点击查询后,将筛选信息传给列表,列表根据筛选信息调用接口进行查询为例:
class Filters extends Component {
constructor(props) {
super(props);
this.state = {
filters: {}
}
}
// 点击查询
onSearch() {
this.props.onSearch && this.props.onSearch(this.state.filter);
}
// 用户选择新的筛选项
onfilterChange(filter) {
this.setState({
filters: {
...this.state.filters,
...filter
}
})
}
render() {}
}
class List extends Component {
constructor(props) {
super(props);
this.state = {
list: []
}
}
// 组件接收新的 filter 的时候更新 state
componentWillReceiveProps(nextProps) {
if (nextProps.filters !== this.props.filters) {
this.search(nextProps.filters);
}
}
// 查询新的列表
search(filters) {
fetch('/getList', {
filters
})
.then((data) => data.json())
.then((data) => this.setState({
list: data.list
}))
}
render() {
return this.state.list.map(item => {
return <div>{item.content}</div>
})
}
}
首先创建两个组件,组件 Filters 对外暴露方法 onSearch
,当用户点击查询的时候就会调用外部传来的 onSearch
方法。
组件 List 接收一个 filters
对象作为 props,当传来新的 filters
的时候就会去调用接口去查询新的列表数据,然后渲染出来。
所以兄弟组件之间的通信关键在于父组件,父组件就像粘合的胶水一样,会将两个组件粘合起来。
class App extends Component {
constructor(props) {
super(props)
this.state = {
filters: {}
}
}
onSearch = (filters) => {
this.setState({
filters
})
}
render() {
return (
<div className="main">
<Filters onSearch={onSearch} />
<List filters={filters} />
</div>
)
}
}
当然,这个组件的设计并不好,比如 List 组件应该是纯展示组件,只需要接收需要展示的数据就够了,调用接口筛选应该放到 App 组件中去做。这个例子只是为了让大家比较清晰地知道兄弟组件之间是如何传值的。
如果组件嵌套比较多,需要通信的组件也比较多的话,你会发现最终的组件数据流动变成了这样:
上面的数据流动方向是从最顶层组件向下面的组件,形成了一个“单向数据流”。
在开始讲解状态管理前,我们先来了解一下现代前端框架都做了些什么。
以 Vue 为例子,在刚开始的时候,Vue 官网首页写的卖点是数据驱动、组件化、MVVM 等等(现在首页已经改版了)。
那么数据驱动的意思是什么呢?不管是原生 JS 还是 jQuery,他们都是通过直接修改 DOM 的形式来实现页面刷新的。
而 Vue/React 之类的框架不是粗暴地直接修改 DOM,而是通过修改 data/state 中的数据,实现了组件的重新渲染。也就是说,他们封装了从数据变化到组件渲染这一个过程。
原本我们用 jQuery 开发应用,除了要实现业务逻辑,还要操作 DOM 来手动实现页面的更新。尤其是涉及到渲染列表的时候,更新起来非常麻烦。
var ul = document.getElementById("todo-list");
$.each(todos, function(index, todo) {
var li = document.createElement('li');
li.innerHTML = todo.content;
li.dataset.id = todo.id;
li.className = "todo-item";
ul.appendChild(li);
})
所以后来出现了 jQuery.tpl 和 Underscore.template 之类的模板,这些让操作 DOM 变得容易起来,有了数据驱动和组件化的雏形。
<script type="text/template" id="tpl">
<ul id="todo-list">
<% _.each(todos, function(todo){ %>
<li data-id="<%=todo.id%>" class="todo-item">
<%= todo.content %>
</li>
<% }); %>
</ul>
</script>
如果说用纯原生 JS 或者 jQuery 开发页面是原始农耕时代,那么 React/Vue 等现代化框架则是自动化的时代。
有了前端框架之后,我们不需要再去关注怎么生成和修改 DOM,只需要关心页面上的这些数据以及流动。
所以如何管理好这些数据流动就成了重中之重,这也是我们常说的“状态管理”。
前面讲了很多例子,可状态管理到底要管理什么呢?在我看来,状态管理的一般就是这两种数据。
1. 展示数据
展示数据很容易理解,这个一般是通过网络请求来从服务端获取到的数据,比如列表数据,通常是为了和服务端数据保持一致。
2. 状态数据
状态数据则是和状态有关,例如蒙层的开关状态、页面的 loading 状态、单(多)选项的选中状态等等,这些数据都是和用户交互息息相关的。
结合上面的例子,如果想要对应用的数据流进行管理,是不是可以将所有的状态放到顶层组件中呢?
将数据按照业务或者组件来划分,将多个组件共享的数据放到一个单独的地方,这样就形成了一个大的 store。
这个大的 store 可以放到顶层组件中维护,也可以放到顶层组件之外来维护。
父组件将组件依赖的数据以及修改数据的方法一层层传给子组件。
我们可以将 state 按照组件来划分,现在这个 state 就是整个应用的 store。将修改 state 的方法放到 events 里面,按照和 state 一样的结构来组织,最后将其传入各自对应的子组件中。
class App extends Component {
constructor(props) {
this.state = {
global: {},
headerProps: {},
bodyProps: {
sidebarProps: {},
cardProps: {},
tableProps: {},
modalProps: {}
},
footerProps: {}
}
this.events = {
header: {
changeHeaderProps: this.changeHeaderProps
},
footer: {
changeFooterProps: this.changeFooterProps
},
body: {
sidebar: {
changeSiderbarProps: this.changeSiderbarProps
}
}
}
}
changeHeaderProps(props) {
this.setState({
headerProps: props
})
}
changeFooterProps() {}
changeSiderbarProps() {}
...
render() {
const {
headerProps,
bodyProps,
footerProps
} = this.state;
const {
header,
body,
footer
} = this.events;
return (
<div className="main">
<Header {...headerProps} {...header} />
<Body {...bodyProps} {...body} />
<Footer {...footerProps} {...footer} />
</div>
)
}
}
我们可以看到,这种方式可以很完美地解决子组件之间的通信问题。只需要修改对应的 state 就行了,App 组件会在 state 变化后重新渲染,子组件接收新的 props 后也跟着渲染。
这种模式还可以继续做一些优化,比如结合 Context 来实现向深层子组件传递数据。
const context = createContext({});
class App extends Component {
...
render() {
return (
<div className="main">
<context.Provider value={...this.state, ...this.events}>
<Header />
<Body />
<Footer />
</context.Provider>
</div>
)
}
}
class Header extends Component {
render() {
<context.Consumer>
{
(value) => {
...
}
}
</context.Consumer>
}
}
如果你已经接触过 Redux 这个状态管理库,你会惊奇地发现,把 App 组件中的 state 移到外面,这不就是 Redux 了吗?
没错,Redux 的核心原理也是这样,在组件外部维护一个 store,在 store 修改的时候会通知所有被 connect 包裹的组件进行更新。这个例子可以看做 Redux 的一个雏形。
关于 Redux 的用法以及更详细的实现,下一节将会带大家一起去深究。