ReactNative
React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用。在 JavaScript 中用 React 抽象操作系统原生的 UI 组件,代替 DOM 元素来渲染等[2]。
React Native 使你能够使用基于 JavaScript 和 React 一致的开发体验在本地平台上构建世界一流的应用程序体验。React Native 把重点放在所有开发人员关心的平台的开发效率上——开发者只需学习一种语言就能轻易为任何平台高效地编写代码。Facebook 在多个应用程序产品中使用了 React Native,并将继续为 React Native 投资[2]。性能方面,FB团队也作出了许多努力,设计了诸如JSI、Fabric、TurboModules、Lean Core等多种优化方案[6]。
Redux
随着移动端的发展,使用JS开发原生APP变得日益复杂,主要体现在两方面:一、JS需要管理的状态越来越多越来越难以维护,包括服务端返回的数据、缓存、用户操作产生的数据和UI状态等,例如元素是否被选中,是否显示动效,分页参数等;二、管理不断变化的state是非常困难的,状态之间可能存在各种依赖,View的变化也可能导致状态的变化等,在复杂的业务环境下,状态因为什么发生了变化,什么阶段发生了怎样的变化等都变得难以追踪。三、React主要负责管理视图的渲染,本身并未提供一套帮助我们管理state的机制,无论是state、props还是context都需要自己管理。
例如:
this.state = {
products: [
{name:"lucy", age:18},
{name:"jerry", age: 20}
]
}
// 后文中可能使用这种方式。特点是:简单,但不能记录state的变化
this.setState({
products: [...this.state, {name: "Mike", age: 20}]
})
我们可能直接调用this.setState()的方式来更新数据,这种方式不能记录我们改变的状态。特别是对于一些共有state,在多个组件中可能导致其state的变化,使得该state变得难以维护。为此,Redux通过派发Action来更新state,每个Action描述了本次操作的type和对应的content,这个content交给一个名为Reducer的纯函数[4]来处理,进行对应的业务处理,并返回一个新的state。这样每个更新操作都将记录在Action中,便于以后的维护。·
Redux的三大原则:
1、单一数据源:整个应用的 state 被储存在 一棵 object tree 中,并且这个 object tree 只存在于 唯一一个 store 中。
2、State 是只读的:唯一改变 state 的方法就是触发 action。这样确保了 视图 和 网络请求 都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 竞态条件的出现。
3、使用纯函数来修改state:为了描述 action 如何改变 state tree ,你需要编写reducers。
Redux的简单使用
下图所示,Redux需要先创建一个store,通过派发Action来改变store中的state,如下图所示,Component先通过派发一个action,被store接收之后,先交给中间件,中间件增加逻辑增强或者过滤之后,决定交给Reducer处理,Reducer收到action根据action type进行对应的处理,并返回新的state给store,之后Component即可获取到store中的state。
具体流程
Redux可以独立React单独使用,使用步骤:
1、环境准备,安装node和yarn,执行yarn init,并且在package.json中增加如下配置:
{
"name": "redux",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"redux": "^4.1.2"
},
"type": "module",
"scripts": {
"start": "node index.js"
}
}
2、创建index.js以及store文件目录
3、创建store
// index.js
import redux from 'redux'
import reducer from './reducer.js';
const store = redux.createStore(reducer)
export default store;
4、定义actions
// constants.js
export const ADD_ACTION = "ADD_ACTION"
export const SUB_ACTION = "SUB_ACTION"
export const INC_ACTION = "INC_ACTION"
export const DEC_ACTION = "DEC_ACTION"
// actionCreators.js
import { ADD_ACTION, INC_ACTION, SUB_ACTION, DEC_ACTION } from "./constants.js"
export const addAction = (num) => {
return {
type: ADD_ACTION,
num: num
}
}
export const subAction = (num) => {
return {
type: SUB_ACTION,
num: num
}
}
export const incAction = () => {
return {
type: INC_ACTION
}
}
export const decAction = () => {
return {
type: DEC_ACTION
}
}
5、reducer,用于处理对应Action的的纯函数
// reducer.js
import { ADD_ACTION, DEC_ACTION, INC_ACTION, SUB_ACTION } from "./constants.js";
const defaultState = {
count: 0
}
function reducer(state= defaultState, action) {
switch(action.type) {
case ADD_ACTION:
return {...state, count: state.count + action.num}
case SUB_ACTION:
return {...state, count: state.count - action.num}
case INC_ACTION:
return {...state, count: state.count + 1}
case DEC_ACTION:
return {...state, count: state.count - 1}
default:
return state
}
}
export default reducer;
6、派发action,之后执行yarn start即可看到对应的输出
// index.js
import store from './store/index.js'
import { addAction, decAction, incAction, subAction } from './store/actionCreators.js'
store.subscribe(() => {
console.log(store.getState())
})
store.dispatch(addAction(5))
store.dispatch(subAction(5))
store.dispatch(incAction())
store.dispatch(decAction())
和React一起应用
上文描述了如何独立于React单独使用Redux,接下来将描述它如何跟React一起使用。React的页面渲染从render函数开始,并通过this.setState()函数触发render(),因此我们只需要在React组件的componentDidMount()中订阅store,在componentWillUnmount()中取消订阅,并在订阅中拿到store的state,调用setState()来完成页面渲染。
class App extends PureComponent{
// 其他代码省略
constructor() {
this.state = {
count: store.getState().getCount()
}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.setState({
count: store.getState().count
})
});
}
componentWillUnmount() {
this.unsubscribe();
}
}
以上就是Redux和React结合的简单使用。
在实际开发过程中,我们可能会结合高阶组件进一步抽象,以减少相同的包依赖和重复的代码。
React-Redux
上文描述了Redux的简单使用,这种使用Redux的方式已经可以帮助我们在React中较好的管理state。但它需要多处直接依赖Redux,并且存在大量类似于componentDidMount和componentWillUnmount中的重复代码,对此,我们将公共逻辑进一步封装。封装思路:
1、使用高阶组件代替继承
2、将订阅逻辑和取消订阅逻辑从业务组件分离,封装在高阶组件中
3、为了不直接依赖redux,但又因为需要redux的dispatch action的能力,这里有两种处理方式1)直接将dispatch函数通过props传递给业务组件,在业务组件内部调用dispatch来完成action派发;2)将dispatch函数传递给我们自定义函数,该函数接收dispatch参数,进而在我们的自定义函数中完成action的派发。这里我们使用方式2,使用这种方式好处在于可以使得我们的业务组件直接变成函数式组件,而方式1只能使用类组件。
4、state中属性的映射方式暴露给业务组件
这里的高阶组件,相当于创建了一个装饰器,在装饰器内进行了逻辑增强(之前的componentDidMount和componentWillUnmount中的逻辑),以后只需要把原组件给装饰器装饰一下,即可实现将dispatch action和store的state保存到原组件的props中,以后就可以直接拿到原组件props完成action dispatch,也能获取到state中的数据。
实例:
其中Add是一个业务组件,connect用于桥接store(state和dispatch)和业务组件(state和业务方法)。
import React, { PureComponent } from "react"
import { StoreContext } from "./context"
export default function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceHOC(WrapeComponent) {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super(props, context)
this.state = {
storeState: mapStateToProps(context.getState())
}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
storeState: mapStateToProps(this.context.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrapeComponent {...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}/>
)
}
}
EnhanceComponent.contextType = StoreContext;
return EnhanceComponent;
}
}
import React, { PureComponent } from 'react'
import connect from '../react-redux/connect';
import { addAction } from '../store/actionCreators';
function Add(props) {
return (
<div>
<h2>Add</h2>
<p>{props.counter}</p>
<button onClick={() => props.addCounter(1)}>+1</button>
<button onClick={() => props.addCounter(10)}>+10</button>
</div>
)
}
// 不直接使用store内部的state,在这里再做一次映射
const mapStateToProps = state => {
return {
counter: state.counter
}
}
// 拿到dispatch能力,在这里派发对应的action
const mapDispatchToProps = dispatch => {
return {
addCounter(num) {
dispatch(addAction(num))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Add);
import React, { PureComponent } from 'react'
import Add from './pages/Add'
import Sub from './pages/Sub'
export default class App extends PureComponent {
render() {
return (
<div>
<Add/>
</div>
)
}
}
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store'
import {StoreContext} from './react-redux/context'
ReactDOM.render(
// 导入store到context,使得connect库是一个独立的库
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root')
);
事实上,redux的作者帮助我们封装了一个更可靠、功能完整的库,名为react-redux[5],它的实现思路和上述大致相同,使用它也很简单,我们只需要通过yarn add react-redux即可完成导入。
Redux中间件
从上文可知,Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。
Redux thunk
在实际开发中,我们常会遇到需要发起一个异步请求的情况,通常在componentDidMount中完成该请求,并在网络请求的回调中dispatch action来完成页面更新。例如:
class App extends PureComponent {
componentDidMount() {
axios({
url: "http://localhost.com/home/list"
}).then(res => {
const data = res.data.data;
this.props.update(data)
})
}
// 其他代码省略
}
const mapDispatchToProps = dispatch => {
return {
update(data){
dispatch(updateAction(data))
}
}
}
// 其他代码省略
此事React的请求和页面渲染流程就变成了:
这种方式使得我们必须将网络请求放在组件的生命周期中,其实,我们可以将这部分交给redux管理,在派发action时直接派发一个函数,reducer发现是函数就会直接执行。这一方式需要使用Redux中间件来完成:
1、安装redux-thunk,它是Redux中间件的一个实现
yarn add redux-thunk
2、应用redux-thunk
import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer'
import thunkMiddleware from 'redux-thunk'
// 中间件
const storeEnhancer = applyMiddleware(thunkMiddleware)
const store = createStore(reducer, storeEnhancer)
export default store
3、定义好发起网络请求的Action和更新列表信息的Action
export const requestAction = (dispatch, getState) => {
axios({
url: "https://reqres.in/api/users?page=2"
}).then(res => {
const data = res.data;
if (data != null) {
dispatch(userInfoListAction(data.data))
}
})
}
export const userInfoListAction = (responseUsersInfo) => {
const usersInfo = responseUsersInfo.map(item => {
return {
username: item.last_name,
avatar: item.avatar
}
})
return {
type: UPDATE_USER_INFO_ACTION,
usersInfo: usersInfo
}
}
4、设置reducer
export default function reducer(state = defualtState, action) {
switch(action.type) {
case UPDATE_USER_INFO_ACTION:
return {usersInfo: action.usersInfo}
default:
return state
}
}
5、在componentDidMount()中调用映射方法,dispatch网络请求的action,并渲染页面
import React, { PureComponent } from 'react'
import connect from '../reactRedux/connect'
import { requestAction } from '../store/actionCreators'
class Home extends PureComponent {
componentDidMount() {
this.props.requestUsersInfo()
}
render() {
return (
<div>
{
this.props.usersInfo.map(item => {
return (
<div>
<img src={item.avatar}/>
username : {item.username}
</div>
)
})
}
</div>
)
}
}
const mapStateToProps = state => {
return {
usersInfo: [...state.usersInfo]
}
}
const mapDispatchToProps = dispatch => {
return {
requestUsersInfo() {
dispatch(requestAction)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)
此时请求网络和渲染流程就变成了:
Redux DevTools
Redux DevTools是一款调试工具,可以观察到Redux各个Action的行为,及Store状态的变化。安装成功之后再Chrome的调试区会发现多了如下Tab。
使用该能力还需要在代码中增加一项设置,在构建store时,增加如下参数(详情可见官网):
import {createStore, applyMiddleware, compose } from 'redux'
import reducer from './reducer'
import thunkMiddleware from 'redux-thunk'
// 中间件
const storeEnhancer = applyMiddleware(thunkMiddleware)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(storeEnhancer))
export default store
Redux DevTools功能区展示:
Redux Saga
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。它使用了ES6的generator语法,它定义了一个迭代器,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。
Saga API的简要介绍
简单使用:
依然以发起一个网络请求,到返回结果,显示内容到页面为例
1、应用中间价
import {createStore, applyMiddleware, compose } from 'redux'
import reducer from './reducer'
import thunkMiddleware from 'redux-thunk'
import createSagaMiddleware from 'redux-saga'
import mySaga from './saga'
// 中间件
const sagaMiddleware = createSagaMiddleware();
const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(storeEnhancer))
sagaMiddleware.run(mySaga)
export default store
2、定义Action
export const fetchUserInfoListAction = () => {
return {
type: FETCH_USER_INFO_ACTION
}
}
3、在saga.js中发起网络请求
import { put, takeEvery } from 'redux-saga/effects'
import axios from 'axios';
import { FETCH_USER_INFO_ACTION } from './constants'
import { userInfoListAction } from './actionCreators';
function* fetchUserInfo(action) {
const res = yield axios({
url: "https://reqres.in/api/users?page=2"
})
const responseUserInfo = res.data.data;
if (responseUserInfo != null) {
yield put(userInfoListAction(responseUserInfo))
}
}
function* mySaga() {
yield takeEvery(FETCH_USER_INFO_ACTION, fetchUserInfo);
}
export default mySaga;
4、在页面组件中派发Action
const mapDispatchToProps = dispatch => {
return {
requestUsersInfo() {
dispatch(fetchUserInfoListAction())
}
}
}
中间件原理
Redux中间件实现原理非常简单,其实就是将store的dispatch能力交给了中间件代理,然后在中间件中在dispatch的前后插入自己的逻辑,将代理的函数返回给redux,redux再改写dispatch函数为dispatch返回的函数。
注:以下代码只是对中间件实现原理的简单模拟,并非官方源码。
function thunkMiddleware(middlewareApi) {
const dispatch = middlewareApi.dispatch;
const getState = middlewareApi.getState;
function thunkDispatch(action) {
if (typeof action == "function") {
action(dispatch, getState);
} else {
dispatch(action);
}
}
return thunkDispatch;
}
function applyMidlewares(...middlewares) {
middlewares.forEach(middleware => {
store.dispatch = middleware({store.dispatch, store.getState})
})
}
// 在我们的代码中使用中间件
applyMiddlewares(thunkMiddleware)
Reducer拆分
Redux官方建议我们只使用一个store,但随着业务迭代,一点会有越来越多的Action出现,只使用一个Reducer函数就会使代码变得非常复杂且难以维护,此时就需要对Reducer进行拆分,拆分方法:
先来看一下拆分后的目录结构:
在最外层的reducer.js中:
import { combineReducers } from "redux";
import { counterReducer } from "./counter";
import { homeReducer } from "./home";
const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
})
export default reducer
/*
// 原理跟这个类似,这里没做state是否重复校验,页面可能重复渲染
export default function reducer(state = {}, action) {
return {
counterInfo: counterReducer(state.counterInfo, action),
homeInfo: homeReducer(state.homeInfo, action)
}
}
*/
最外层的saga:
import { takeEvery } from 'redux-saga/effects'
import { FETCH_USER_INFO_SAGA, fetchUserInfo } from './home'
function* mySaga() {
yield takeEvery(FETCH_USER_INFO_SAGA, fetchUserInfo);
}
export default mySaga;
其他方法都放到各自模块的index.js中export
import { requestAction, userInfoListAction, fetchUserInfoListAction } from "./actionCreators"
import homeReducer from "./reducer"
import { FETCH_USER_INFO_SAGA } from "./constants"
import fetchUserInfo from "./saga"
export {
requestAction,
userInfoListAction,
fetchUserInfoListAction,
homeReducer,
FETCH_USER_INFO_SAGA,
fetchUserInfo
}
home/reducer:
import { UPDATE_USER_INFO_ACTION } from "./constants"
const defualtState = {
usersInfo: []
}
export default function homeReducer(state = defualtState, action) {
switch(action.type) {
case UPDATE_USER_INFO_ACTION:
return {usersInfo: action.usersInfo}
default:
return state
}
}
home/saga:
import { put } from 'redux-saga/effects'
import axios from 'axios';
import { userInfoListAction } from './actionCreators';
function* fetchUserInfo(action) {
const res = yield axios({
url: "https://reqres.in/api/users?page=2"
})
const responseUserInfo = res.data.data;
if (responseUserInfo != null) {
yield put(userInfoListAction(responseUserInfo))
}
}
export default fetchUserInfo
这样就能将Reducer拆分到各个模块中,如果有新增的业务只需在最外面的reducer.js和saga加上入口即可。
总结
面对日趋复杂的RN应用,为了更好的管理RN中的各种状态,本文主要介绍了Redux解决方案,描述了Redux与React如何简单结合使用。特别地,对于异步状态管理,介绍了Redux-thunk和Redux-saga中间件,并简要描述了中间件的实现原理。最后对Reducer进行了模块拆分。
参考文献
[1] React Native 简介与入门
[2] React Native 官方文档中文版
[3] 庖丁解牛!深入剖析React Native下一代架构重构
[4] 纯函数和非纯函数
[5] React Redux
[6] React Native 新架构分析
[7] Redux Saga官方文档
[8] 利用saga管理异步任务