React
的核心特征是“数据驱动视图”,即UI=render(data)
,也就是说React
的视图会随着数据的变化而变化。那么,它到底如何灵活的处理数据呢?让我们揭开这层纱,透过它的四大通信方式,了解它
一、基于props
的单向数据流
组件,从概念上类似于
Javascript
函数。它接受任意的入参(即"props"),并返回用于描述页面展示内容的React
元素 —— React官方
换句话说:在React
单向数据流的前提下,props
是组件的入参,组件之间可通过入参来完成通信。
而所谓的单向数据流,就是当前组件的state
以props
的形式流动时,只能流向组件树中比自己层级更低的组件。比如在父子组件中,只能由父组件向子组件传props
,而不能反过来。
1、适用场景 —— 简单的父子组件、子父组件和兄弟组件之间的通信
基于props
的单向数据流看似条条框框,却十分灵活,它可以轻松的实现父子组件、子父组件和兄弟组件之间的通信
- 父->子的通信:父组件直接将
this.props
传入子组件 - 子->父的通信:父组件传给子组件绑定自身上下文的函数,子组件调用该函数并将要交给父组件的数据以入参的形式传入
- 兄弟组件之间共享同一个父组件,即兄弟1->兄弟2的通信可以转换为:兄弟1->父、父->兄弟2
结合上面的分析,我们手写个简单的demo1,验证父子组件、子父组件和兄弟组件之间的通信,可点击查看
2、不适用场景 —— 层层传递
对于简单的通信,基于props
串联父子和兄弟组件是很灵活的。但是,处理复杂的嵌套数据流
使用它,则弊大于利。它的弊端主要体现这两点:
- 1、中间作为桥梁的组件会引入很多不属于自己的属性
- 2、短时间内,给开发者带来庞大的工作量和代码量。拉长时间看,整个项目的维护成本也变得昂贵
二、利用“发布-订阅”模式驱动数据流
“发布-订阅”模式,最经典的莫属于浏览器的DOM
事件。它通过调用addEventListener
方法创建一个事件监听器。一旦事件被触发,其对应的监听函数被执行(被“发布”出去)从而进行通信。
1、适用场景 —— 任意组件间的通信
前面我们提到的基于props
数据流对于层层传递是要不得的,其主要问题出在“位置的限制”,而“发布-订阅”模式没有这个限制。
只要在同一个上下文里,监听事件的位置和触发事件的位置是不受限的,这个特性,恰好解决了跨层级通信的问题,适用任意组件间的通信。
2、实现一个“发布-订阅”模式
顾名思义,“发布-订阅”模式最重要的两个功能便是”发布“和”订阅“,对应的分别是事件的监听 和 事件的触发,再补充一个事件的销毁,便是一个简易的“发布-订阅”模式。它们其实都是函数,只是为了更好理解,将其具像化。下面从这三个函数切入,进一步了解它
a、 事件的监听(订阅)
on()
:负责注册事件的监听器,指定事件触发时的回调函数,即注册事件监听函数的过程,是一个“写”操作
b、 事件的触发(发布)
emit()
:负责触发事件,可通过传参使其在触发时携带数据,即触发安装在某个事件上的监听函数,是一个“读”操作
c、 事件的销毁(删除)
off()
:负责监听器的删除
结合上面的分析,我们在demo1的基础上做以下3处改动,以验证了“发布-订阅”模式驱动数据流的通信,,可点击查看
-
在Child2组件上注册事件监听器
to2Event
,指定事件触发时的回调函数handle
-
接着在Child1组件上触发
to2Event
事件,携带自身状态中需要让Child2感知的那部分数据params
-
最后执行
to2Event
事件,Child2感知到Child1
3、不适用场景 —— 庞大复杂的项目
当组件之间关系复杂之后,就会形成一种网状的依赖结构,在这种结构下,暂且不说能不能理清它们之间的关系,光是可能出现的环形依赖所造成的死循环,就已经让开发者抓狂
三、redux
基于单向数据流的redux
,组件之间的关系从“发布-订阅”模式的网状结构,调整为树状结构。在树状结构模型下,组件与组件之间只存在:父子和兄弟关系,且没有环形依赖,大大简化了关系复杂所产生的一系列问题。换句话说redux
通过提供一个统一的状态容器,使得数据能够自由、有序的在组件间流动
1、适用场景 —— 庞大复杂的项目
在复杂的项目中,redux
有着非同一般的优势,它通过让整个应用共享一个全局的Store
,且强调每一次数据更新都保证State
完全不可变,完全避免使用对象引用赋值的方式来更新状态,让整个应用具备可追溯,可回滚,可调试,能够快速来定位和解决问题
2、redux背后的架构思想 —— Flux架构
redux
的设计在很大程度上受益于Flux
架构,它是一套由脸书技术团队提出的应用架构,约束的是应用处理数据的模式,在Flux
架构中,一个应用被拆分为4个部分:
View
(视图层):可以是任何形式实现出来的用户界面,既可以是react
,也可以是vue
Action
(动作):View
发出的“消息”,它会触发应用状态的改变Dispatcher
(派发器):负责对Action
进行分发Store
(数据层):存储应用状态的“仓库”,它的变化会映射到View
上去,同时在这里定义修改状态的逻辑
a、一个典型的Flux工作流
用户与View
之间产生交互,通过View
发起一个Action
;Dispatcher
将Action
派发给Store
,通知Store
进行相应的状态更新,完成后,进一步通知View
更新界面
值得注意的是,箭头都是单向的,这也是Flux
架构的核心特点:单向数据流,保证了状态的变化是可预测的。也就是说Store
中的数据发生了变化,肯定是Dispatcher
派发Action
触发的。这也从根本避免了混乱的数据关系,使得流程变得清晰简单。
结合Flux
架构的特性,再去品味官方给出的redux
定义:
redux
是Javascript
状态容器,它提供可预测的状态管理
此时的你,想必理解可预测这三个字就更清晰了些
3、一个典型的redux工作流
redux
和Flux
在思想上确实是一脉相承,但实现层面并没有完全一致。比如Flux
中允许多个Store
存在,而redux
中只有一个Store
a、redux三大关键要素
在Flux
架构中,一个应用主要由View
、Action
、Dispatcher
和 Store
四部分组成,而redux
主要由Store
、Action
和Reducer
三部分组成
Store
:它是一个单一的数据源,而且是只读的Action
:它是对变化的描述Reducer
:是个函数,它负责对Action
进行分发和处理,更新数据给到Store
b、redux工作流
在redux
的整个工作流中,数据流是严格单向的。假设你想修改数据,你只有一个途径:派发Action
。Action
会被Reducer
读取,Reducer
会根据Action
内容的不同执行不同的计算逻辑,生成新的state
,这个新的state
将更新到Store
对象里,进而驱动View
作出相应的更新
4、从源码的视角,看redux如何工作
a、目录构成
这里略过编译等文件解析,直接进入src
文件
b、引入redux
使用redux
的第一步,即调用createStore
函数,它接收三个入参:
- reducer
- 初始状态内容
- 指定中间件
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
单纯从使用感来看,createStore
函数似乎就只做一件事:创建一个store
对象。可你不知道,其内部却涵盖了redux
主流程中核心方法的定义,下面我们拎出createStore
源码的主体逻辑,顺藤摸瓜揪出redux
源码的主流程
c、createStore源码的主体逻辑
// 接收三个入参:reducer、初始状态内容和指定中间件
function createStore(reducer, preloadedState, enhancer) {
- 前置校验
- 处理没有传入初始状态的情况
- 若 enhancer 不为空,则用 enhancer 包装 createStore
// 第一个参数和第二个参数都传 function的情况
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 此时第二个参数会被认为是 enhancer(中间件)
enhancer = preloadedState;
preloadedState = undefined;
}
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
- 定义内部变量
- currentReducer:记录当前的 reducer,因为 replaceReducer 会修改 reducer 的内容
- currentState:记录当前的 state
- currentListeners:声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件
- nextListeners:currentListeners 的快照
- isDispatching:记录当前是否正在进行 dispatch
let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false
- 定义 ensureCanMutateNextListeners 方法
- 确保 nextListeners 与 currentListeners 不指向同一个引用,即nextListeners是currentListeners 的副本,而不是其本身
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
- 定义 getState 方法
- 获取当前的状态
function getState() {
return currentState;
}
- 定义 subscribe (订阅)方法
- 用于注册 listeners (订阅监听函数)
- 它将会定义 dispatch 最后执行的 listeners 数组的内容
function subscribe(listener) {
- 前置校验
- 校验 listener 的类型:必须为function
- 校验当前是否正在进行 dispatch:禁止在 reducer 中调用 subscribe
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
- 调用ensureCanMutateNextListeners函数
- 确保 nextListeners 与 currentListeners 不指向同一个引用
ensureCanMutateNextListeners();
- 注册监听函数
- 将入参 listener 函数推入 nextListeners 数组中
nextListeners.push(listener);
- 返回取消订阅当前 listener的 unsubscribe 方法
- 定义变量 isSubscribed :用于防止调用多次 unsubscribe 函数
- 调用ensureCanMutateNextListeners函数
- 将当前的 listener 从 nextListeners 数组中删除
let isSubscribed = true;
return function unsubscribe() {
if (!isSubscribed) {
return;
}
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
currentListeners = null
};
}
- 定义 dispatch 方法
- 用于派发 action、调用 reducer 并触发订阅
function dispatch(action) {
- 前置校验
- 校验 action 的数据格式是否合法
- 校验 action 中是否有 type 属性:type 属性作为 action 的唯一标识
- 校验当前是否正在进行 dispatch:禁止套娃
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
- “上锁” - 计算新的state - “解锁”
- “上锁” :将 isDispatching 设置为 true ,标记当前已经存在 dispatch 执行流程
- 调用 reducer,计算新的 state
- “解锁” :将 isDispatching 设置为 false,允许再次进行 dispatch
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
- 触发订阅
- 定义实际执行的监听数组listeners,并赋值为 currentListeners
- currentListeners 被赋值为 nextListeners,因此 最终被执行的 listeners 和当前的 nextListeners 指向同一个引用
- 监听函数依次执行
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}
- 定义 replaceReducer 方法
- 用于替换 reducer
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.REPLACE });
return store;
}
- 初始化 state
- 当派发一个 type 为 ActionTypes.INIT 的 action ,每个 reducer 都会返回它的初始值
dispatch({ type: ActionTypes.INIT });
- observable 方法
- 它在 redux 内部使用,开发者一般不会直接接触
function observable() {
// observable 方法的实现
}
- 返回store 对象
- 将定义的方法包裹在 store 对象里返回
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
d、核心模块
通过分析了createStore
的工作逻辑,不难看出与redux
主流程强相关的两个方法分别是最为核心的dispatch
动作(dispatch
) 和 redux
自身独特的“发布-订阅”模式(subscribe
)
d-1、redux的“发布-订阅”模式:subscript
subscript
并不是一个严格必要的方法,只有在需要监听状态变化时,才会去调用,那么它是如何与主流程结合?
subscript是如何与主流程结合
- 在
store
对象创建完成后,可调用store.subscribe
来注册监听,也可调用subscript
的返回函数来解绑监听(监听函数是listener
数组维护) - 当
dispatch action
发生时,redux
会在reducer
执行完后,逐个执行listener
数组中的监听函数
subscript工作流程中的ensureCanMutateNextListeners
调用
结合上面源码的解析,我们知道ensureCanMutateNextListeners
函数是为了确保 nextListeners
与 currentListeners
不指向同一个引用。让我们思考一下:为什么会有nextListeners
和 currentListeners
两个 listener
数组呢?
在思考这个问题的时候,我们先搞清楚redux
在 订阅 和 发布 两个过程分别是如何处理listener
数组的
- 订阅过程中的listener
数组
- 在
createStore
初始化变量阶段,nextListeners
被赋值为currentListeners
的快照,此时这两者指向同一个引用
let nextListeners = currentListeners;
- 当
subscript
被调用时,ensureCanMutateNextListeners
每次都会在listener
注册前无条件执行,将其纠正为内容一致,引用不同的对象
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
- 紧接着
listener
会被注册到nextListeners
数组中去
nextListeners.push(listener)
- 返回取消订阅当前 listener的 unsubscribe 方法解绑监听
nextListeners.splice(index, 1);
- 发布过程中的listener
数组
触发订阅这个动作是由dispatch
来处理的
const listeners = (currentListeners = nextListeners);
嗯哼!注册监听操作的是nextListeners
,取消监听操作的是nextListeners
,触发订阅的也是读取nextListeners
。既然如此,为何要currentListeners
?转换下思维,假设没有currentListeners
会怎样?
- currentListeners数组用于确保监听函数执行过程的稳定性
假设我们在redux
进行如下操作
// 定义监听函数 A
function listenerA() {}
// 订阅 A,并获取 A 的解绑函数
const unSubscribeA = store.subscribe(listenerA)
// 定义监听函数 B
function listenerB() {
// 在 B 中解绑 A
unSubscribeA()
}
// 定义监听函数 C
function listenerC() {}
// 订阅 B
store.subscribe(listenerB)
// 订阅 C
store.subscribe(listenerC)
执行完成后,nextListeners
数组的内容是三个listener:
[ listenerA, listenerB, listenerC ]
当触发订阅时,即执行
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
当索引i=1
时,即执行listenerB
时,执行了unSubscribeA
这个动作
return function unsubscribe() {
if (!isSubscribed) return;
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
};
假设不存在currentListeners
,则不再执行ensureCanMutateNextListeners
,直接执行nextListeners.splice(index, 1)
那么 listenerA
会同时在 listeners
和 nextListeners
移除,因为两者指向同一个引用
[ listenerB, listenerC ]
但for
循环不会感知到这一点,继续执行i=2
,但此时listeners[2]
已经是undefined
了,也就是原本在这个索引的listenerC
被前置了,当undefined
代替 listenerC
执行时,便会引发函数异常
因此,需要一个不会被变更的、内容稳定的数组记录当前正在工作的listeners
数组,并将它与可能发生变更的nextListeners
剥离开来,来确保监听函数执行过程的稳定性
d-2、redux工作流的核心:dispatch动作
结合上面的源码,我们不难看出dispatch
的工作流刚好将action
、reducer
和store
三者串联起来。在其“打配合”的过程中,有几个点值得我们关注
- 通过“上锁”避免“套娃式”的dispatch
更准确的说,是为了避免开发者在reducer
中手动调用dispatch
。reducer
的本质是store
的更新规则,它指定了应用状态的变化如何响应action
并发送到store
- 为何要“禁止套娃”?
- 从设计的角度:作为一个“计算
state
专用函数”,它必须是“纯净”的,不应该执行除了计算之外的“脏操作” - 从执行的角度:若真的在
reducer
中手动调用dispatch
,那么dispatch
又会反过来调用reducer
,reducer
又再次调用dispatch
...这样反复相互调用,进入死循环,造成非常严重的误操作
3、redux 中间件
在这里不展开解析,下一篇进行详细探讨,待续...
4、redux 弊端
redux
往往在复杂项目中才会体现出它的优势和必要性,那么与其对冲的弊端,有哪些呢?
- 首当其中的就是它要求开发者要完全按照官方所描述的那样,写大量的
action
、reducer
这种的样板代码,会让代码行数大量膨胀,使得开发一个小功能变得非常繁琐 - 使用单一的
state
来管理数据,则要求就开发者自己去完成state
的结构设计 - 不可变数据状态管理仅仅是
redux
所强调的一种思想和要求而已,并没有提供有效避免对象引用赋值的解决方案,这就要求开发者时刻遵守这种模式,以免对不可变造成破坏 - 归根结底,
redux
对数据流的约束背后是不可忽略的成本
四、React16 Context API 维护全局状态
React16
下的Context API
能够跨越层级,换句话说就是组件树内人人都可消费数据。其设计目的是为了共享那些对于一个组件树而言是“全局”的数据,类似主题、身份认证等
1、Context API 的三大关键要素
a、React.creatContext:创建一个context
对象
// 可选择性的传入defaultValue
const AppContext = React.creatContext([defaultValue])
// context对象包含了Provider和Consumer
const { Provider, Consumer } = AppContext
b、Provider:数据的提供者
在根组件进行包裹,value
即是后续组件间流动的“数据”,可被Consumer
消费
因为context
会使用参考标识来决定何时进行渲染,可能会发生:当provider
的父组件进行重渲染时,在consumers
组件中触发意外的渲染,因为value
属性总是被赋值为新的对象,为了防止这种情况,将 value 状态提升到父节点的 state 里
this.state = {
value: {something: 'something'},
};
<Provider value={this.state.value}>
<Title/>
</Provider>
c、Consumer:数据的消费者
不仅能读取到Provider
下发的数据,还能读取到这些数据后续的更新。意味着数据在生产者和消费者之间能够及时同步。若没有对应的Provider
,则直接读取creatContext
的defaultValue
// 接收一个返回组件的函数作为子元素
<Consumer>
{value => <div>{value.title}</div>}
</Consumer>
结合上面的分析,我们在demo2的基础上添加一些全局数据,可点击查看
2、适用场景
- 组件树中有不同层级的组件需要访问同样的一批数据,即共享的数据
- 管理当前的 locale,theme,或者一些缓存数据