最近在读修言的《深入浅出搞定 React》,笔者的文笔和文风都非常有趣,又不乏干货,重读几遍后仍收获满满,整理了下笔记分享给大家。本系列大概有 15 篇,如果觉得有帮助可以给个 star,如果发现问题请不吝在评论区指正。
数据流
React 的核心特征是 数据驱动视图,这个特征在业内有一个非常有名的函数式来表达:
UI = render(data)
UI = f(data)
React 的视图会随着数据的变化而变化,我们说的组件通信其实就是组件之间建立的数据上的连接,这背后是一套环环相扣的 React 数据流解决方案。
基于 props 的单向数据流
基于 props 传参,可以实现简单的父子,子父和兄弟组件通信,所谓单向数据流,指的就是当前组件的 state 以 props 的形式流动时,只能流向组件树中比自己层级更低的组件。React 中的单向数据流场景包括,
- 基于 props 的父子通信:父组件的 state 作为子组件的 props
- 基于 props 的子父通信:父组件传递一个绑定自身上下文的函数
- 基于 props 的兄弟组件通信:以父组件未桥梁,转换为子父 + 父子通信
以上是 props 传参比较适合处理的三种场景,如果通信需求较为复杂,基于 props 的单向数据流可能并不适合,我们需要考虑其他更灵活的方案,比如通信类问题的 “万金油”:发布-订阅模式。
利用 “发布-订阅” 模式驱动数据流
发布-订阅模式的优点在于,只要组件在同一个上下文里,监听事件的位置和触发事件的位置是不受限的,所以原理上我们可以基于发布订阅模式实现任意组件的通信,下面是一个简单的 EventEmitter,
class EventEmitter {
constructor() {
this.eventMap = {};
}
on(type, handler) {
if (!(handler instanceof Function)) {
throw new Error('event handler expect to be a function');
}
if (!this.eventMap[type]) {
this.eventMap[type] = [];
}
this.eventMap[type].push(handler);
}
emit(type, params) {
if (this.eventMap[type]) {
this.eventMap[type].forEach((handler, index) => {
handler(params);
});
}
}
off(type, handler) {
if (this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1);
}
}
}
除了上述介绍的两种方式,我们还可以使用 React 原生提供的全局通信方式 Context API。
使用 Context API 维护全局状态
Context API 是 React 官方提供的一种组件树全局通信的方式。
Context 基于生产者-消费者模式,对应到 React 中有三个关键的要素:React.createContext、Provider、Consumer。通过调用 React.createContext,可以创建出一组 Provider。Provider 作为数据的提供方,可以将数据下发给自身组件树中任意层级的 Consumer,而 Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新,
const AppContext = React.createContext();
const [Provider, Consumer] = AppContext;
<Provider value={ content: this.state.content }>
<Content />
</Provider>
<Consumer>
{value => <div>{value.content}</div>}
</Consumer>
下面是 Context 工作流的简单图解,
但是在 V16.3 之前,由于存在种种局限性,Context 并不被 React 官方提倡使用,旧的 Context 存在哪些局限呢?
-
代码不够优雅:生产者需要定义 childContextTypes 和 getChildContext,消费者需要定义 contextTypes 才能通过 this.context 访问生产者提供的数据,属性设置和 API 编写过于繁琐,很难辨别谁是 Provider,谁是 Consumer
-
数据可能无法及时同步:这个问题在 React 官方中有过介绍,如果组件提供的一个 Context 发生了变化,而中间父组件的 shouldComponentUpdate 返回了 false,那么使用到该值的后代组件不会进行更新,这违背了模式中的 “Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新” 的定义,导致数据在生产者和消费者之间可能不能及时同步。
V16.3 后新的 Context API 改进了这一点,即使组件的 shouldComponentUpdate 返回 false,它仍然可以“穿透”组件继续向后代组件进行传播,再加上更优雅的语义化声明式写法,Context 成为一种确实可行的 React 组件通信解决方案。
理解了 Context API 的前世今生,接下来我们继续串联 React 组件间通信的解决方案。
三方数据流框架的“课代表”:Redux
简单的跨层级组件通信,可以使用发布订阅模式或者 Context API 搞定,随着应用的复杂度不断提升,需要维护的状态会越来越多,组件间关系也越来越复杂,这时我们可以考虑引入三方的数据流框架,比如 Redux。
Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
简单解读一下这句话,首先 Redux 是为了 Javascript 应用而生的,也就是说它不是 React 的专利,任何框架或原生 Javascript 都可以用。我们知道状态其实就是数据,所谓状态容器,就是一个存放公共数据的仓库。
要理解可预测的状态管理,我们得先知道 Redux 是什么以及它的工作流是什么样的。
Redux 主要由三个部分组成:store、reducer 和 action。
- store 是一个只读的单一数据源
- action 是一个描述状态变化的对象
- reducer 是一个对变化进行分发和处理的纯函数
在 Redux 的整个工作过程中,数据流是严格单向的,
下面我们从编码的角度来理解 Redux 工作流,
使用 createStore 创建 store 对象
import { createStore } from 'redux';
const store = createStore(reducer, initialState, applyMiddleware());
createStore 接受三个入参:reducer、初始状态和中间件。
reducer 的作用是将新的 state 返回给 store
reducer 就是一个接受旧的状态和变化,返回一个新的状态的纯函数,没有任何副作用,
const reducer = (state, action) => newState
当我们基于 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则。
action 的作用是通知 reducer “让改变发生”
action 是一个包含自身唯一标识的对象,在浩如烟海的 store 状态库中,想要命中某个希望发生改变的 state,必须使用正确的 action 来驱动,
const action = { type: 'ACTION_TYPE', payload: {} }
dispatch 用来派发 action
action 本身只是一个对象,想要让 reducer 感知到 action,还需要派发 action 这个动作,这个动作是由 store.dispatch 完成的,
store.dispatch(action)
派发 action 后,对应的 reducer 会做出响应从而触发 store 中状态的更新
相关文章
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
(完)