React
React 是一个用于构建用户界面的 JavaScript 库。
- 声明式。以声明式编写 UI,通过数据的状态转移来动态更新 UI。
- 组件化。构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI。通过 Javascript 组件保持状态与 DOM 分离。
- 一次学习。React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。
组件(Component)
通过 JSX 我们可以声明式的定义 UI 组件。组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。组件代码表现形式可以是一种函数或者是对象。
可以根据单一功能原则来判定组件的范围,也就是说,一个组件原则上只能负责一个功能。UI(或者说组件结构)大多数时候会与数据模型一一对应,它们都会倾向于遵守相同的信息结构。
状态(Props & State)
状态代表某一时刻的数据快照,伴随时间的推移状态也在发生变化,组件会渲染输出不同的 DOM UI。
我们将组件想象成一个函数,props
是传递给组件的(类似于函数的形参),而 state
是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。
状态完全由 React 控制的组件称为受控组件,由非 React 控制的称为非受控组件,例如由原生 DOM 控制等。
组件遵循单向的数据流,**所以我们绝不能修改输入的 props。**像纯函数一样工作的组件称为纯组件(PureComponent
),纯组件通过浅比较 props 和 state 来确定是否需要重新渲染组件,同样的输入始终返回相同的结果。
除了使用 props
来传递状态控制组件渲染,通过修改内部的 state
也会触发组价重新渲染,内部的 state 通过调用专有的函数触发更新,对 state 的修改可能会被 React 合并成一个调用延迟更新。
有时候我们的组件需要对外部的输入作出响应,通常我们是通过事件回调暴露给外部信息。
一个包含这些元素的示例组件:
// 输入 props
function Form(props) {
// 内部 state
const [content, setContent] = useState(‘’);
return (
<form onSubmit={() => props.onSubmit({ content })}> // 事件回调
<input type="text" name="firstName"
onChange={ (event) => setName(event.target.value) } /> // 内部状态修改
<button type="submit">Submit</button>
</form>
);
}
共享及复用
当我们的组件越来越多时,组件间需要共享状态的情形也越来越频繁,这时我们可以将共享状态向上提升以复用状态:
或者当我们的状态及转换越来越复杂时,使用全局的 store Redux 也将是一个更好的选择;
除了向上提升达到复用,还可以通过其它模式达到复用:
使用上下文 Context 为组件间共享数据,而无需为每层组件手动添加 props 并自上而下传递,非常适用于某些类型的属性:(例如:地区偏好,UI 主题)。
使用 Render Props 将一个组件封装的状态或行为共享给其他需要相同状态的组件。
Render Props 使用一个值为函数的 prop 在组件间共享代码,该函数返回 React 元素,组件内部通过调用此函数来实现自己的渲染逻辑。 并不一定非要使用名为 render 的 prop,也可以使用 children,JSX 的 children 就是元素内部包裹的内容。
{/* 使用 render prop 传递内部封装状态给组件 */} <DataProvider render={data => ( <h1>Hello {data.target}</h1> )}/> {/* 使用 children prop 传递内部封装状态给组件 */} <DataProvider> { data => <h1>Hello {data.target}</h1> } </DataProvider>
使用 React Hook
Hook 使你在无需修改组件结构的情况下复用状态逻辑。 Hook 使用函数式编程使我们的组件更加纯粹干净,将纯函数外的内部状态和副作用都分离出来,并使用相应的 Hook 函数代理,它还由一些方便使用的其它钩子函数。
除了复用状态,通过一些模式也可以实现复用组件逻辑:
高阶组件(Higher-Order Components - HOC)
高阶组件是参数为组件,返回值为新组件的函数。 有时候不同组件间会有通用的处理逻辑或某种特性,我们可以将它提到高阶组件中而保持原始组件的简单,例如一些副作用:日志记录、数据读取写入、权限控制等等。高阶组件为组件提供某种特性而不侵入组件,并最大化可组合性。
Redux
我们之前提到 Redux 可以帮我们统一管理共享状态,随着 JavaScript 单页应用程序的需求变得越来越复杂,我们的代码必须管理比以往更多的状态。此状态可以包括服务器响应和缓存数据,以及尚未持久化到服务器的本地创建的数据。 UI 状态的复杂性也在增加,因为我们需要管理活动路由,选中的选项卡、加载指示器、分页控件等。但如何在大型应用中合理的管理状态并让状态的变化更有可预测性,Redux 给出了自己的答案。
Redux 可以用三个基本的原则来描述:
- 单一数据源
你整个应用程式的 state,被储存在一个树状物件放在唯一的 store 里面。
State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改
为了描述 action 如何改变 state 树 ,你需要编写 reducers。
单向数据流
- state:驱动应用的真实数据源头
- view:基于当前状态的 UI 声明性描述
- actions:根据用户输入在应用程序中发生的事件,并触发状态更新
接下来简要介绍 “单向数据流(one-way data flow)”:
- 用 state 来描述应用程序在特定时间点的状况
- 基于 state 来渲染出 View
- 当发生某些事情时(例如用户单击按钮),我们会 dispatch 一个 action,通过 reducer 函数根据发生的事情进行更新 state,生成新的 state
- 基于新的 state 重新渲染 View
动画的方式来表达数据流更新:
使用
内部我们可以使用 Redux Toolkit configureStore
API 创建一个 Redux store;
Redux state 由 reducer 函数来更新;
- Reducers 总是通过复制现有状态值,并更新副本来不可变地生成新状态
- Redux Toolkit
createSlice
函数为您生成“slice reducer”逻辑切片,并让您编写“mutable 可变”代码,内部自动将其转变为安全的不可变更新 - 这些切片化 reducer 函数被添加到
configureStore
中的reducer
字段中,并定义了 Redux store 中的数据和状态字段名称
React 组件使用 useSelector
钩子从 store 读取数据;
- 选择器函数接收整个
state
对象,并且返回需要的部分数据 - 每当 Redux store 更新时,选择器将重新运行,如果它们返回的数据发生更改,则组件将重新渲染
React 组件使用 useDispatch
钩子 dispatch action 来更新 store;
createSlice
将为我们添加到切片的每个 reducer 函数生成 action creator 生成器函数- 在组件中调用
dispatch(someActionCreator())
来 dispatch action - Reducers 将运行,检查此 action 是否相关,并在适当时返回新状态
异步逻辑
就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。
但是,如果您希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux 中间件 的用武之地。它们扩展了 store,并允许您:
- dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
- 暂停、修改、延迟、替换或停止 dispatch 的 action
- 编写可以访问
dispatch
和getState
的额外代码 - 教
dispatch
如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替
Redux 有多种异步中间件,每一种都允许您使用不同的语法编写逻辑。最常见的异步中间件是 redux-thunk
,它可以让你编写可能直接包含异步逻辑的普通函数。Redux Toolkit 的 configureStore
功能默认自动设置 thunk 中间件,我们推荐使用 thunk 作为 Redux 开发异步逻辑的标准方式。
早些时候,我们看到了Redux 的同步数据流是什么样子。当我们引入异步逻辑时,我们添加了一个额外的步骤,中间件可以运行像 AJAX 请求这样的逻辑,然后 dispatch action。这使得异步数据流看起来像这样:
Thunk middleware 并不是 Redux 处理异步 action 的唯一方式:
- 你可以使用 redux-promise 或者 redux-promise-middleware 来 dispatch Promise 来替代函数。
- 你可以使用 redux-observable 来 dispatch Observable。
- 你可以使用 redux-saga 中间件来创建更加复杂的异步 action。
计算并记忆衍生数据
useSelector
使用严格 ===
的引用比较来比较其结果,因此只要选择器结果是新的引用,组件就会重新渲染!所以使用 map 等产生新引用的方法时要格外注意;
Reselect
或者我们可以使用 Reselect 库提供的 createSelector
创建可记忆的(Memoized)、可组合的 selector 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。该库已经包含在 Redux Toolkit 中。
selector 不仅可以接收 Redux store state 作为参数,也可以接收 props。在依赖的 props 和 state 不发生变化的情况下,计算结果会被缓存到该函数的内存中。
⚠️ 当我们组合多个选择器来构建新组合器时,所有“输入选择器”都应该接受相同类型的参数,这一点很重要。
const selectItems = state => state.items const selectItemId = (state, itemId) => itemId const selectItemById = createSelector( [selectItems, selectItemId], (items, itemId) => items[itemId] ) // 使用 state 和参数 42 调用 const item = selectItemById(state, 42) /* 内部,Reselect 会使用该参数调用每一个输入 selector: const firstArg = selectItems(state, 42); const secondArg = selectItemId(state, 42); const result = outputSelector(firstArg, secondArg); return result; */
proxy-memoize
proxy-memoize
是一个相对较新的记忆选择器库,它使用独特的实现方法。它依赖 ES6 Proxy
对象来跟踪尝试读取嵌套值,然后在以后的调用中仅比较嵌套值以查看它们是否已更改。在某些情况下,这可以提供比重新选择更好的结果。
一个很好的例子是派生一系列 todo 描述的选择器:
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
不幸的是,如果内部的任何其他值 state.todos
发生变化,这将重新计算派生数组,例如切换 todo.completed
标志。派生数组的内容是相同的,但是由于输入 todos
数组发生了变化,它必须计算一个新的输出数组,并且它有一个新的引用。
相同的选择器 proxy-memoize
可能如下所示:
import memoize from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
与 Reselect 不同,proxy-memoize
它可以检测到只有字段正在被访问,并且只有在其中一个字段发生更改 todo.text
时才会重新计算其余字段。
它还有一个内置 size
选项,可让您为单个选择器实例设置所需的缓存大小。
它与 Reselect 有一些权衡和不同之处:
- 所有值都作为单个对象参数传入
- 它要求环境支持 ES6
Proxy
对象(不支持 IE11) - 它更神奇,而 Reselect 更明确
- 基于
Proxy
的跟踪行为有一些边缘情况 - 它比较新未被广泛使用
综上所述,Redux 官方鼓励考虑将 proxy-memoize
其用作 Reselect 的可行替代方案。
使用技巧
不可变更新
在 React 或者 Redux 中,为了更快速的对比状态变更或保证状态的可预测性,我们一般不直接修改原始的 state,而是以返回副本的形式实现不可变更新。但是对于一个层级比较深的对象的修改和拷贝代码会显得特别繁琐,但是一些库为我们实现了简单易用的语法糖,可以简介的实现不可变更新。这些库包含:immutability-helper、object-path-immutable,或者我们也可以使用 ES6 的解构语法。
将 Redux action 动作暴露给外部
有时候我们想隐藏 action 的分发操作给外部使用,这个组件可能完全不知道 Redux 的存在,所以它也不会拿到 dispatch 函数。例如,我们需要在网络请求 axios 库的前后拦截器中触发一个动作。我们可以使用 bindActionCreators 创建这样的 dispatch action 分发函数。具体可以参考:Redux-bindActionCreators-使用