跳到主要内容

状态管理

状态管理

1.对 Redux 的理解,主要解决了什么问题

React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的 JavaScript 应用工具。随着 JavaScript 单页应用(SPA)开发日趋复杂, JavaScript 需要管理比任何时候都要多的 state(状态), Redux 就是降低管理难度的。(Redux 支持 React、Angular、jQuery 甚至纯 JavaScript)。

在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。

当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。

Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将 state 直接传入 store,不用通过其他的组件。并且组件通过 subscribe 从 store 获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从 store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。

为什么要使用单向数据流的方式,因为前端项目有大多数是 mvc 或者是 mvvm 架构。这种架构有什么缺点,有一个很大的缺点就是 当业务复杂度变得越来越高的时候,因为允许 view 层和 model 层 直接传递。因为 model 可能不止对应一个 view 层,就会出现下图这样的情况:

image-20220920221818343

从图上看数据流这是混乱的,如果项目中出现 bug 就很难定位到到底是哪一步出现问题,所以 Redux 的核心是单向数据流, 因为视图更新就是从 store 通知视图更新

主要解决的问题: 单纯的 Redux 只是一个状态机,是没有 UI 呈现的,react- redux 作用是将 Redux 的状态机和 React 的 UI 呈现绑定在一起,当你 dispatch action 改变 state 的时候,会自动更新页面。

2.Redux 与 React-Redux

redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理。

React-redux 是为方便 react 使用,在 redux 上封装的库。在实际的项目中我们可以使用 redux 也可以直接使用 react-redux。

1.redux

1.redux 的概念

redux 和 react 之间是没有关系的,Redux 支持 React、angular、jQuery 甚至 Javascript

redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理,主要有三个核心方法,action,store,reducer

(1)单一数据源

(2)state 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已经发生事件的普通对象

store.dispatch({ type: "COMPLETE_TODO", index: 1 });

(3)使用纯函数来执行修改

为了描述 action 如何修改 state tree,你需要去编写 reducers

Reducers 只是一些纯函数,它接收先前的 state 和 action,并且返回新的 state.可以复用、可以控制顺序、传入附加参数

2.redux 的组成

state:就是我们传递的数据,可以分为三类:DomainData、UI State、App State

(1)Action:将我们的数据从应用传递到 store 的载体,它是 store 数据的唯一来源。我们可通过 store.dispatch()将 action 传递给 store

{
type:'USE_LIST',//必须字段
list:{...}
}
// action只是描述有事件发生,并不更新state

//action的创建函数
function addAction(params){
return{
type:'add',
...params
}
}

(2)Reducer:本质是一个函数,用来响应发送过来的 actions,经处理将 state 发生给 store

接收两个参数:一是初始化 state,二是 action

const initState = {...}
rootReducer = (state = initState,action)=>{...return{...}}

(3)Store作为 action 和 reducer 的桥梁

import { createStore } from "redux";
const store = createStore(传递reducer);

拥有以下属性和方法:

State应用的数据状态
getState获取数据状态
Dispatch发送 action
Subscribe注册监听,Subscribe 的返回值注销监听

redux 的使用例子:

1.创建 store/index.js

store 就是保存数据的地方,可以看作是一个容器。

import { createStore } from "redux";
//导入已经创建的reducer
import { reducer } from "../reducer";
export default createStore(reducer);

2.创建 action/index.js

Action 就是 View 发出的通知,表示 State 应该要发生变化了。

Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置.

const sendAction = () => {
return {
type: "send_action",
value: "发送了某个数据——--",
};
};
module.exports = {
sendAction,
};

3.创建 reducer/index.js

store 收到 action 以后,会给一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

//第一个参数为state,我们可以定义默认值,然后进行赋值
//在函数中判断第二个参数action的type值是否是我们发送的,是则通过return返回新的state,
//将reducer导出
/**
* 专门处理发送过来的action
*/
const reducer = (state = { value: "默认值" }, action) => {
console.log("reducer", state, action);
switch (action.type) {
case "send_action":
return Object.assign({}, state, action);
default:
return state;
}
};

module.exports = {
reducer,
};

4.在组件中的使用

import { Button } from "antd";
import React, { Component } from "react";
import store from "../../store";
import { sendAction } from "../../action";
class UrlList extends Component {
constructor(props) {
super(props);
//也可以使用箭头函数哈
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const action = sendAction();
store.dispatch(action);
}
//当组件加载完毕来监听
componentDidMount() {
console.log(this.context.router);
store.subscribe(() => {
//通过store.getState().value来获取state中的值
console.log("subscribe", store.getState().value);
this.setState({});
});
}

render() {
return (
<>
<Button onClick={this.handleClick}>按钮发送</Button>
<h1>{store.getState().value}</h1>
</>
);
}
}
export default Com;

2.react-redux

React-redux 中的两个核心成员:Provider、Connect

Provider:这个组件使得整个 app 都能获取到 store 中的数据

Connect:使得组件能够跟 store 关联

provider
  • Provider 包装在根组件的最外层,使得所有组件都可以拿到 state
  • Provider 接收 state 作为 props,通过 context 往下传递,这样 react 中任何组件都可以通过 context 获取 store
connect
  • Provider 内部组件如果想要使用到 state 中的数据,就必须要 connect 进行层包裹封装,换-句话来说就是必须要被 connect 进行加强
  • connect 就是方便我们组件能够获取到 store 中的 state
使用
(1)react-redux 的安装:

yarn add redux

yarn add react-redux

(2)构建 store 和 reducer

1.创建 reducer/index.js 文件,构建 reducer 来响应 actions

//render/index.js
//一、state 二、action
let initState={
count:0
}
exports.reducer = (state = initState, action) => {
switch (action.type) {
case 'send_action':
return {
count:state.count+1
}
default:
return state;
}
}

2.创建 store/index.js 文件,通过 createStore 方法,把我们的 reducer 传入进来

import { createStore } from "redux";
//导入已经创建的reducer
import {reducer} from '../reducer'
export default createStore(reducer)
(3)Provider 的实现

1.导入 Provider 组件,在 react-redux 中进行导入

2.需要利用 Provider 组件,对我们整个结构进行包裹

3.给我们 Provider 组件设置 store 的属性,而这个属性值是通过 createStore 构建出的 store 实例对象

import './App.css';
import ComA from './pages/ComA'
import ComB from './pages/ComB'
import store from "./store";
import { Provider } from 'react-redux';
function App() {
return (
<Provider store={store}>
<ComA/>
<ComB/>
</Provider>
);
}

export default App;
(4)connect

对 connect 简单分析一下

mapStateToProps 方法:connect 的第一个参数传入的,而且会在每次store中的 state 改变时调用。用户获取 state 中的数据,必须有返回值(普通对象)。

参数备注
state必传
ownProps非必传,自己的 props

mapDispatchToProps:connect 的第二个参数传入的,用于将操作分配给 store.

dispatch 是 Redux store 的一个函数。你调用 store.dispatch 来调度一个动作。这是触发状态更改的唯一方法。

第三个是要加强的组件

Connect(mapStateToProps, mapDispatchToProps)(要加强的组件);
案例:

我们实现从在 ComB 中点击按钮+1,在 ComA 中获取+1 后的值。

所以说 ComB 是发送方,ComA 是接收方。

实现步骤:

1.导入 connect

2.利用 connect 对组件进行加强

import React, {Component}  from "react";
import {connect} from 'react-redux'
class ComB extends Component{
handleClick=()=>{
console.log('ComB',this.props)
this.props.sendAction()
}

render(){
return (
<>
<button onClick={this.handleClick}>按钮+1</button>
</>
)
}

}
const mapDispatchToProps = (dispatch)=>{
return {
//将sendAction注册到ComB的props中,然后就直接在ComB中直接使用this.props.sendAction()
sendAction:()=>{
dispatch({
type:'send_action'
})
}
}
}

export default connect(null,mapDispatchToProps)(ComB)
//connect(null,mapDispatchToProps)(ComB)

第一个参数:要接收的函数
第二个参数:要发送action的函数
第三个参数:后面的括号里面是要加强的组件

3.在 ComA 的组件方法中可以通过 this.props 拿到 sendAction

import React, {Component}  from "react";
import { connect } from "react-redux";

class ComA extends Component{
render(){
console.log('Home render',this.props)
return (
<>
<div>
{this.props.count}
</div>
</>
)
}

}
const mapstateToProps = (state)=>{
console.log('Home',state)
return state;
}
export default connect(mapstateToProps)(ComA)
//简写:
//export default connect((state)=>state)(ComA)

大概总结一下整个流程: 在这里插入图片描述

3.Redux 原理及工作流程

redux原理图

(1)原理

Redux 源码主要分为以下几个模块文件

  • compose.js 提供从右到左进行函数式编程
  • createStore.js 提供作为生成唯一 store 的函数
  • combineReducers.js 提供合并多个 reducer 的函数,保证 store 的唯一性
  • bindActionCreators.js 可以让开发者在不直接接触 dispacth 的前提下进行更改 state 的操作
  • applyMiddleware.js 这个方法通过中间件来增强 dispatch 的功能
const actionTypes = {
ADD: 'ADD',
CHANGEINFO: 'CHANGEINFO',
}

const initState = {
info: '初始化',
}

export default function initReducer(state=initState, action) {
switch(action.type) {
case actionTypes.CHANGEINFO:
return {
...state,
info: action.preload.info || '',
}
default:
return { ...state };
}
}

export default function createStore(reducer, initialState, middleFunc) {

if (initialState && typeof initialState === 'function') {
middleFunc = initialState;
initialState = undefined;
}

let currentState = initialState;

const listeners = [];

if (middleFunc && typeof middleFunc === 'function') {
// 封装dispatch
return middleFunc(createStore)(reducer, initialState);
}

const getState = () => {
return currentState;
}

const dispatch = (action) => {
currentState = reducer(currentState, action);

listeners.forEach(listener => {
listener();
})
}

const subscribe = (listener) => {
listeners.push(listener);
}

return {
getState,
dispatch,
subscribe
}
}

(2)工作流程

  • const store= createStore(fn)生成数据;
  • action: {type: Symble('action01), payload:'payload' }定义行为;
  • dispatch 发起 action:store.dispatch(doSomething('action001'));
  • reducer:处理 action,返回新的 state;

通俗点解释:

  • 首先,用户(通过 View)发出 Action,发出方式就用到了 dispatch 方法
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action,Reducer 会返回新的 State
  • State—旦有变化,Store 就会调用监听函数,来更新 View

以 store 为核心,可以把它看成数据存储中心,但是他要更改数据的时候不能直接修改,数据修改更新的角色由 Reducers 来担任,store 只做存储,中间人,当 Reducers 的更新完成以后会通过 store 的订阅来通知 react component,组件把新的状态重新获取渲染,组件中也能主动发送 action,创建 action 后这个动作是不会执行的,所以要 dispatch 这个 action,让 store 通过 reducers 去做更新 React Component 就是 react 的每个组件。

单向数据流

Redux数据流向图

图中容易看出所有的东西都是以 store 为核心,我们把它看成数据存储中心,但是他要更改数据的时候不能直接修改,数据修改更新的角色由 Reducers 来担任, store 只做存储,中间人,当Reducers 的更新完成以后会通过store 的订阅来通知 react component ,组件获取新的状态,进行重新渲染,组件中我们也能主动发送 action,创建 action 后这个动作是不会执行的,所以要 dispatch 这个 action,让store 通过 reducers 去做更新 React Component 就是 react 的每个组件

4.Redux 中异步的请求怎么处理

可以在 componentDidmount 中直接进⾏请求⽆须借助 redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助redux 的异步中间件进⾏异步处理。redux 异步流中间件其实有很多,当下主流的异步中间件有两种 redux-thunk、redux-saga。

使用 react-thunk 中间件

redux-thunk优点:

  • 体积⼩: redux-thunk 的实现⽅式很简单,只有不到 20 ⾏代码
  • 使⽤简单: redux-thunk 没有引⼊像 redux-saga 或者 redux-observable 额外的范式,上⼿简单

redux-thunk缺陷:

  • 样板代码过多: 与 redux 本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的
  • 耦合严重: 异步操作与 redux 的 action 偶合在⼀起,不⽅便管理
  • 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装

使用步骤:

  • 配置中间件,在 store 的创建中配置
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";

// 设置调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
// 设置中间件
const enhancer = composeEnhancers(applyMiddleware(thunk));

const store = createStore(reducer, enhancer);

export default store;
  • 添加一个返回函数的 actionCreator,将异步请求逻辑放在里面
/**
发送get请求,并生成相应action,更新store的函数
@param url {string} 请求地址
@param func {function} 真正需要生成的action对应的actionCreator
@return {function}
*/
// dispatch为自动接收的store.dispatch函数
export const getHttpAction = (url, func) => (dispatch) => {
axios.get(url).then(function (res) {
const action = func(res.data);
dispatch(action);
});
};
  • 生成 action,并发送 action
componentDidMount(){
var action = getHttpAction('/getData', getInitTodoItemAction)
// 发送函数类型的action时,该action的函数体会自动执行
store.dispatch(action)
}

使用 redux-saga 中间件

redux-saga优点:

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action 摆脱 thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法” thunk function
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强⼤: redux-saga 提供了⼤量的 Saga 辅助函数和 Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤
  • 灵活: redux-saga 可以将多个 Saga 可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步 flow
  • 易测试,提供了各种 case 的测试⽅案,包括 mock task,分⽀覆盖等等

redux-saga缺陷:

  • 额外的学习成本: redux-saga 不仅在使⽤难以理解的 generator function,⽽且有数⼗个 API,学习成本远超 redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与 redux-observable 不同,redux-observable 虽然也有额外学习成本但是背后是 rxjs 和⼀整套思想
  • 体积庞⼤: 体积略⼤,代码近 2000 ⾏,min 版 25KB 左右
  • 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码
  • ts ⽀持不友好: yield ⽆法返回 TS 类型

redux-saga 可以捕获 action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:

  • 配置中间件
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import createSagaMiddleware from "redux-saga";
import TodoListSaga from "./sagas";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const sagaMiddleware = createSagaMiddleware();

const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));

const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoListSaga);

export default store;
  • 将异步请求放在 sagas.js 中
import { takeEvery, put } from "redux-saga/effects";
import { initTodoList } from "./actionCreator";
import { GET_INIT_ITEM } from "./actionTypes";
import axios from "axios";

function* func() {
try {
// 可以获取异步返回数据
const res = yield axios.get("/getData");
const action = initTodoList(res.data);
// 将action发送到reducer
yield put(action);
} catch (e) {
console.log("网络请求失败");
}
}

function* mySaga() {
// 自动捕获GET_INIT_ITEM类型的action,并执行func
yield takeEvery(GET_INIT_ITEM, func);
}

export default mySaga;
  • 发送 action
componentDidMount(){
const action = getInitTodoItemAction()
store.dispatch(action)
}

总结

使用 redux-thunk,当我们返回的是函数时,store 会帮我们调用这个返回的函数,并且把 dispatch 暴露出来供我们使用。对于 redux-thunk 的整个流程来说,它是等异步任务执行完成之后,我们再去调用 dispatch,然后去 store 去调用 reduces

image-20220927214653024

使用了 redux-saga,当我们 dispatch 的 action 类型不在 reducer 中时,redux-saga 的监听函数takeEvery就会监听到,等异步任务有结果就执行put方法,相当于dispatch,再一次触发 dispatch。对于 redux-saga 的整个流程来说,它是等执行完 action 和 reducer 之后,判断 reducer 中有没有这个 action

image-20220927214704531

总结来看,redux-thunk 和 redux-saga 处理异步任务的时机不一样。对于 redux-saga,相对于在 redux 的 action 基础上,重新开辟了一个 async action 的分支,单独处理异步任务

saga 自己基本上完全弄了一套 asyc 的事件监听机制,代码量大大增加,从我自己的使用体验来看 redux-thunk 更简单,和 redux 本身联系地更紧密。尤其是整个生态都向函数式编程靠拢的今天,redux-thunk 的高阶函数看上去更加契合这个闭环

5.Redux 怎么实现属性传递,原理是什么

react-redux 数据传输 ∶ view-->action-->reducer-->store-->view。看下点击事件的数据是如何通过 redux 传到 view 上:

  • view 上的 AddClick 事件通过 mapDispatchToProps 把数据传到 action ---> click:()=>dispatch(ADD)
  • action 的 ADD 传到 reducer 上
  • reducer 传到 store 上 const store = createStore(reducer);
  • store 再通过 mapStateToProps 映射穿到 view 上 text:State.text

代码示例 ∶

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";
class App extends React.Component {
render() {
let { text, click, clickR } = this.props;
return (
<div>
<div>数据:已有人{text}</div>
<div onClick={click}>加人</div>
<div onClick={clickR}>减人</div>
</div>
);
}
}
const initialState = {
text: 5,
};
const reducer = function (state, action) {
switch (action.type) {
case "ADD":
return { text: state.text + 1 };
case "REMOVE":
return { text: state.text - 1 };
default:
return initialState;
}
};

let ADD = {
type: "ADD",
};
let Remove = {
type: "REMOVE",
};

const store = createStore(reducer);

let mapStateToProps = function (state) {
return {
text: state.text,
};
};

let mapDispatchToProps = function (dispatch) {
return {
click: () => dispatch(ADD),
clickR: () => dispatch(Remove),
};
};

const App1 = connect(mapStateToProps, mapDispatchToProps)(App);

ReactDOM.render(
<Provider store={store}>
<App1></App1>
</Provider>,
document.getElementById("root")
);

6.Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?

Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。

applyMiddleware 源码:

export default function applyMiddleware(...middlewares) {
return (createStore) =>
(...args) => {
// 利用传入的createStore和reducer和创建一个store
const store = createStore(...args);
let dispatch = () => {
throw new Error();
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
};
// 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
// 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
}

从 applyMiddleware 中可以看出 ∶

  • redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数
  • 柯里化函数两端一个是 middewares,一个是 store.dispatch

7.Redux 请求中间件如何处理并发

使用 redux-Saga redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga 如何处理并发:

  • takeEvery

可以让多个 saga 任务并行被 fork 执行。

import { fork, take } from "redux-saga/effects";

const takeEvery = (pattern, saga, ...args) =>
fork(function* () {
while (true) {
const action = yield take(pattern);
yield fork(saga, ...args.concat(action));
}
});
  • takeLatest

takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。 在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。

import { cancel, fork, take } from "redux-saga/effects";

const takeLatest = (pattern, saga, ...args) =>
fork(function* () {
let lastTask;
while (true) {
const action = yield take(pattern);
if (lastTask) {
yield cancel(lastTask); // 如果任务已经结束,则 cancel 为空操作
}
lastTask = yield fork(saga, ...args.concat(action));
}
});

8.Redux 状态管理器和变量挂载到 window 中有什么区别

两者都是存储数据以供后期使用。但是 Redux 状态更改可回溯——Time travel,数据多了的时候可以很清晰的知道改动在哪里发生,完整的提供了一套状态管理模式。

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。

这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 可以称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 视图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux 就是为了帮你解决这个问题。

9.Redux 中间件是怎么拿到 store 和 action? 然后怎么处理?

1.redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个 middleware 接受 2 个参数, Store 的getState 函数和 dispatch 函数,分别获得store 和 action,最终返回一个函数

2.该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。

3.调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action。

10.Redux 中的 connect 有什么作用

connect 负责连接 React 和 Redux

(1)获取 state

connect 通过 context 获取 Provider 中的 store,通过 store.getState() 获取整个 store tree 上所有 state

(2)包装原组件

将 state 和 action 通过 props 的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps 与组件上原有的 props 合并后,通过属性的方式传给 WrappedComponent

(3)监听 store tree 变化

connect 缓存了 store tree 中 state 的状态,通过当前 state 状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发 Connect 及其子组件的重新渲染

11.Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别

  • Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可
  • Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可
  • Vuex 数据流的顺序是 ∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)

通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;

(2)共同思想

  • 单—的数据源
  • 变化可以预测

本质上 ∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。

12.mobx 的使用

响应式对象

MobX 通过 makeObservable 方法来构造响应式对象,传入的对象属性会通过 Proxy 代理,与 Vue 类似,在 6.0 版本之前使用的是 Object.defineProperty API,当然 6.0 也提供了降级方案。

import { configure, makeObservable, observable, action, computed } from "mobx";

// 使用该配置,可以将 Proxy 降级为 Object.defineProperty
configure({ useProxies: "never" });

// 构造响应对象
const store = makeObservable(
// 需要代理的响应对象
{
count: 0,
get double() {
return this.count * 2;
},
increment() {
this.count += 1;
},
decrement() {
this.count -= 1;
},
},
// 对各个属性进行包装,用于标记该属性的作用
{
count: observable, // 需要跟踪的响应属性
double: computed, // 计算属性
increment: action, // action 调用后,会修改响应对象
decrement: action, // action 调用后,会修改响应对象
}
);

我们在看看之前版本的 MobX,使用装饰器的写法:

class Store {
@observable count = 0;
constructor() {
makeObservable(this);
}
@action increment() {
this.count++;
}
@action decrement() {
this.count--;
}
@computed get double() {
return this.count * 2;
}
}

const store = new Store();

这么看起来,好像写法并没有得到什么简化,好像比写装饰器还要复杂点。下面我们看看 6.0 版本一个更强大的 API:makeAutoObservable

makeAutoObservable 是一个更强大的 makeObservable,可以自动为属性加上对象的包装函数,上手成本直线下降。

import { makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
get double() {
return this.count * 2;
},
increment() {
this.count += 1;
},
decrement() {
this.count -= 1;
},
});

计算属性

MobX 的属性与 Vue 的 computed 一样,在 makeAutoObservable 中就是一个 gettergetter 依赖的值一旦发生变化,getter 本身的返回值也会跟随变化。

import { makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
get double() {
return this.count * 2;
},
});

store.count 为 1 时,调用 store.double 会返回 2。

修改行为

当我们需要修改 store 上的响应属性时,我们可以通过直接重新赋值的方式修改,但是这样会得到 MobX 的警告 ⚠️。

const store = makeAutoObservable({
count: 0,
});

document.getElementById("increment").onclick = function () {
store.count += 1;
};

warn

MobX 会提示,在修改响应式对象的属性时,需要通过 action 的方式修改。虽然直接修改也能生效,但是这样会让 MobX 状态的管理比较混乱,而且将状态修改放到 action 中,能够让 MobX 在内部的事务流程中进行修改,以免拿到的某个属性还处于中间态,最后计算的结果不够准确。

makeAutoObservable 中的所有方法都会被处理成 action

import { makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
get double() {
return this.count * 2;
},
increment() {
// action
this.count += 1;
},
decrement() {
// action
this.count -= 1;
},
});

不同于 Vuex,将状态的修改划分为 mutation 和 action,同步修改放到 mutation 中,异步的操作放到 action 中。在 MobX 中,不管是同步还是异步操作,都可以放到 action 中,只是异步操作在修改属性时,需要将赋值操作放到 runInAction 中。

import { runInAction, makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
async initCount() {
// 模拟获取远程的数据
const count = await new Promise((resolve) => {
setTimeout(() => {
resolve(10);
}, 500);
});
// 获取数据后,将赋值操作放到 runInAction 中
runInAction(() => {
this.count = count;
});
},
});

store.initCount();

如果不调用 runInAction ,则可以直接调用本身已经存在的 action。

import { runInAction, makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
setCount(count) {
this.count = count;
},
async initCount() {
// 模拟获取远程的数据
const count = await new Promise((resolve) => {
setTimeout(() => {
resolve(10);
}, 500);
});
// 获取数据后,调用已有的 action
this.setCount(count);
},
});

store.initCount();

监听对象变更

无论是在 React 还是在小程序中想要引入 MobX,都需要在对象变更的时候,通知调用原生的 setState/setData 方法,将状态同步到视图上。

通过 autorun 方法可以实现这个能力,我们可以把 autorun 理解为 React Hooks 中的 useEffect。每当 store 的响应属性发生修改时,传入 autorun 的方法(effect)就会被调用一次。

import { autorun, makeAutoObservable } from "mobx";

const store = makeAutoObservable({
count: 0,
setCount(count) {
this.count = count;
},
increment() {
this.count++;
},
decrement() {
this.count--;
},
});

document.getElementById("increment").onclick = function () {
store.count++;
};

const $count = document.getElementById("count");
$count.innerText = `${store.count}`;
autorun(() => {
$count.innerText = `${store.count}`;
});

每当 button#increment 按钮被点击的时候,span#count 内的值就会自动进行同步。👉查看完整代码

效果演示

除了 autorun ,MobX 还提供了更精细化的监听方法:reactionwhen

const store = makeAutoObservable({
count: 0,
setCount(count) {
this.count = count;
},
increment() {
this.count++;
},
decrement() {
this.count--;
},
});

// store 发生修改立即调用 effect
autorun(() => {
$count.innerText = `${store.count}`;
});

// 第一个方法的返回值修改后才会调用后面的 effect
reaction(
// 表示 store.count 修改后才会调用
() => store.count,
// 第一个参数为当前值,第二个参数为修改前的值
// 有点类似与 Vue 中的 watch
(value, prevValue) => {
console.log("diff", value - prevValue);
}
);

// 第一个方法的返回值为真,立即调用后面的 effect
when(
() => store.count > 10,
() => {
console.log(store.count);
}
)(
// when 方法还能返回一个 promise
async function () {
await when(() => store.count > 10);
console.log("store.count > 10");
}
)();

mobox 和 redux 有什么区别?

(1)共同点

  • 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态;
  • 某一状态只有一个可信数据来源(通常命名为 store,指状态容器);
  • 操作更新状态方式统一,并且可控(通常以 action 方式提供更新状态的途径);
  • 支持将 store 与 React 组件连接,如 react-redux,mobx- react;

(2)区别 Redux 更多的是遵循 Flux 模式的一种实现,是一个 JavaScript 库,它关注点主要是以下几方面 ∶

  • Action∶ 一个 JavaScript 对象,描述动作相关信息,主要包含 type 属性和 payload 属性 ∶

    o type∶ action 类型; o payload∶ 负载数据;
  • Reducer∶ 定义应用状态如何响应不同动作(action),如何更新状态;

  • Store∶ 管理 action 和 reducer 及其关系的对象,主要提供以下功能 ∶

    o 维护应用状态并支持访问状态(getState());
    o 支持监听action的分发,更新状态(dispatch(action));
    o 支持订阅store的变更(subscribe(listener));
  • 异步流 ∶ 由于 Redux 所有对 store 状态的变更,都应该通过 action 触发,异步任务(通常都是业务或获取数据任务)也不例外,而为了不将业务或数据相关的任务混入 React 组件中,就需要使用其他框架配合管理异步任务流程,如 redux-thunk,redux-saga 等;

Mobx 是一个透明函数响应式编程的状态管理库,它使得状态管理简单可伸 ∶

  • Action∶ 定义改变状态的动作函数,包括如何变更状态;
  • Store∶ 集中管理模块状态(State)和动作(action)
  • Derivation(衍生)∶ 从应用状态中派生而出,且没有任何其他影响的数据

对比总结:

  • redux 将数据保存在单一的 store 中,mobx 将数据保存在分散的多个 store 中
  • redux 使用 plain object 保存数据,需要手动处理变化后的操作;mobx 适用 observable 保存数据,数据变化后自动处理响应的操作
  • redux 使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx 中的状态是可变的,可以直接对其进行修改
  • mobx 相对来说比较简单,在其中有很多的抽象,mobx 更多的使用面向对象的编程思维;redux 会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
  • mobx 中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而 redux 提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易

13.dva 的使用

一文彻底搞懂 DvaJS 原理

Dva 是什么

dva 首先是一个基于reduxredux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了react-routerfetch,所以也可以理解为一个轻量级的应用框架。

Dva 解决的问题

经过一段时间的自学或培训,大家应该都能理解 redux 的概念,并认可这种数据流的控制可以让应用更可控,以及让逻辑更清晰。但随之而来通常会有这样的疑问:概念太多,并且 reducer, saga, action 都是分离的(分文件)。

  • 文件切换问题。redux 的项目通常要分 reducer, action, saga, component 等等,他们的分目录存放造成的文件切换成本较大。
  • 不便于组织业务模型 (或者叫 domain model) 。比如我们写了一个 userlist 之后,要写一个 productlist,需要复制很多文件。
  • saga 创建麻烦,每监听一个 action 都需要走 fork -> watcher -> worker 的流程
  • entry 创建麻烦。可以看下这个redux entry的例子,除了 redux store 的创建,中间件的配置,路由的初始化,Provider 的 store 的绑定,saga 的初始化,还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子,看起来比较复杂。

Dva 的优势

  • 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API
  • elm 概念,通过 reducers, effects 和 subscriptions 组织 model
  • 插件机制,比如dva-loading可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
  • 支持 HMR,基于babel-plugin-dva-hmr实现 components、routes 和 models 的 HMR

Dva 的劣势

Dva 的适用场景

  • 业务场景:组件间通信多,业务复杂,需要引入状态管理的项目
  • 技术场景:使用 React Class Component 写的项目

Dva 核心概念

  • 基于 Redux 理念的数据流向。 用户的交互或浏览器行为通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State,如果是异步行为(可以称为副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。

img

  • 基于 Redux 的基本概念。包括:
    • State 数据,通常为一个 JavaScript 对象,操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
    • Action 行为,一个普通 JavaScript 对象,它是改变 State 的唯一途径。
    • dispatch,一个用于触发 action 改变 State 的函数。
    • Reducer 描述如何改变数据的纯函数,接受两个参数:已有结果和 action 传入的数据,通过运算得到新的 state。
    • Effects(Side Effects) 副作用,常见的表现为异步操作。dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator 的相关概念,所以将异步转成同步写法,从而将 effects 转为纯函数。
    • Connect 一个函数,绑定 State 到 View
  • 其他概念
    • Subscription,订阅,从源头获取数据,然后根据条件 dispatch 需要的 action,概念来源于elm。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
    • Router,前端路由,dva 实例提供了 router 方法来控制路由,使用的是react-router
    • Route Components,跟数据逻辑无关的组件。通常需要 connect Model 的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components,详见组件设计方法

Dva 应用最简结构

不带 Model
import dva from "dva";
const App = () => <div>Hello dva</div>;
// 创建应用
const app = dva();
// 注册视图
app.router(() => <App />);
// 启动应用
app.start("#root");
带 Model
// 创建应用
const app = dva();
app.use(createLoading()); // 使用插件
// 注册 Model
app.model({
namespace: "count",
state: 0,
reducers: {
add(state) {
return state + 1;
},
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: "add" });
},
},
});
// 注册视图
app.router(() => <ConnectedApp />);
// 启动应用
app.start("#root");

Dva 底层原理和部分关键实现

背景介绍
  1. 整个 dva 项目使用 lerna 管理的,在每个 package 的 package.json 中找到模块对应的入口文件,然后查看对应源码。
  2. dva 是个函数,返回一了个 app 的对象。
  3. 目前 dva 的源码核心部分包含两部分,dva 和 dva-core。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。
dva

dva 做了三件比较重要的事情:

  1. 代理 router 和 start 方法,实例化 app 对象
  2. 调用 dva-core 的 start 方法,同时渲染视图
  3. 使用 react-redux 完成了 react 到 redux 的连接。
// dva/src/index.js
export default function (opts = {}) {
// 1. 使用 connect-react-router 和 history 初始化 router 和 history
// 通过添加 redux 的中间件 react-redux-router,强化了 history 对象的功能
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
// 2. 调用 dva-core 里的 create 方法 ,函数内实例化一个 app 对象。
const app = create(opts, createOpts);
const oldAppStart = app.start;
// 3. 用自定义的 router 和 start 方法代理
app.router = router;
app.start = start;
return app;
// 3.1 绑定用户传递的 router 到 app._router
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`
);
app._router = router;
}
// 3.2 调用 dva-core 的 start 方法,并渲染视图
function start(container) {
// 对 container 做一系列检查,并根据 container 找到对应的DOM节点
if (!app._store) {
oldAppStart.call(app);
}
const store = app._store;
// 为HMR暴露_getProvider接口
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// 渲染视图
if (container) {
render(container, store, app, app._router);
app._plugin.apply("onHmr")(render.bind(null, container, store, app));
} else {
return getProvider(store, this, this._router);
}
}
}
function getProvider(store, app, router) {
const DvaRoot = (extraProps) => (
<Provider store={store}>
{router({ app, history: app._history, ...extraProps })}
</Provider>
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require("react-dom"); // eslint-disable-line
ReactDOM.render(
React.createElement(getProvider(store, app, router)),
container
);
}

我们同时可以发现 app 是通过 create(opts, createOpts)进行初始化的,其中 opts 是暴露给使用者的配置,createOpts 是暴露给开发者的配置,真实的 create 方法在 dva-core 中实现

dva-core

dva-core 则完成了核心功能:

  1. 通过 create 方法完成 app 实例的构造,并暴露 use、model 和 start 三个接口
  2. 通过 start 方法完成
  • store 的初始化
  • models 和 effects 的封装,收集并运行 sagas
  • 运行所有的 model.subscriptions
  • 暴露 app.model、app.unmodel、app.replaceModel 三个接口

dva-core create

作用: 完成 app 实例的构造,并暴露 use、model 和 start 三个接口

// dva-core/src/index.js
const dvaModel = {
namespace: "@@dva",
state: 0,
reducers: {
UPDATE(state) {
return state + 1;
},
},
};
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中构造了createOpts对象
const plugin = new Plugin(); // dva-core中的插件机制,每个实例化的dva对象都包含一个plugin对象
plugin.use(filterHooks(hooksAndOpts)); // 将dva(opts)构造参数opts上与hooks相关的属性转换成一个插件
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin), // 暴露的use方法,方便编写自定义插件
model, // 暴露的model方法,用于注册model
start, // 原本的start方法,在应用渲染到DOM节点时通过oldStart调用
};
return app;
}

dva-core start

作用:

  1. 封装 models 和 effects ,收集并运行 sagas
  2. 完成 store 的初始化
  3. 运行所有的 model.subscriptions
  4. 暴露 app.model、app.unmodel、app.replaceModel 三个接口
function start() {
const sagaMiddleware = createSagaMiddleware();
const promiseMiddleware = createPromiseMiddleware(app);
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
// 把每个 model 合并为一个reducer,key 是 namespace 的值,value 是 reducer 函数
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
// 收集每个 effects 到 sagas 数组
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// 初始化 Store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// Run sagas, 调用 Redux-Saga 的 createSagaMiddleware 创建 saga中间件,调用中间件的 run 方法所有收集起来的异步方法
// run方法监听每一个副作用action,当action发生的时候,执行对应的 saga
sagas.forEach(sagaMiddleware.run);
// Setup app
setupApp(app);
// 运行 subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// 暴露三个 Model 相关的接口,Setup app.model and app.unmodel
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
/**
* Create global reducer for redux.
*
* @returns {Object}
*/
function createReducer() {
return reducerEnhancer(
combineReducers({
...reducers,
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
}
}

路由

在前面的 dva.start 方法中我们看到了 createOpts,并了解到在 dva-core 的 start 中的不同时机调用了对应方法。

import * as routerRedux from "connected-react-router";
const { connectRouter, routerMiddleware } = routerRedux;
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};

其中 initialReducer 和 setupMiddlewares 在初始化 store 时调用,然后才调用 setupApp

可以看见针对 router 相关的 reducer 和中间件配置,其中 connectRouter 和 routerMiddleware 均使用了 connected-react-router 这个库,其主要思路是:把路由跳转也当做了一种特殊的 action。

Dva 与 React、React-Redux、Redux-Saga 之间的差异

原生 React

img按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是

以及 本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component

React-Redux

img与上图相比, 几个明显的改进点:

  1. 状态及页面逻辑从 里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer
  2. 及都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态变化, 被 connect 的组件也随之刷新
  3. 使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging

这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好。

Redux-Saga

img因为我们可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:

  1. 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action
  2. saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可
Dva

img有了前面三步的铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践, 对于提升编码体验有三点贡献:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面
  2. 增加了一个 Subscriptions, 用于收集其他来源的 action, 比如键盘操作等
  3. model 写法很简约, 类似于 DSL(领域特定语言),可以提升编程的沉浸感,进而提升效率

约定大于配置

app.model({
namespace: "count",
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return {
...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1 };
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: "minus" });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key("⌘+up, ctrl+up", () => {
dispatch({ type: "add" });
});
},
},
});

14.redux-thunk 分析

redux-thunk可以利用redux中间件让redux支持异步的action

// 如果 action 是个函数,就调用这个函数
// 如果 action 不是函数,就传给下一个中间件
// 发现 action 是函数就调用
const thunk =
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === "function") {
return action(dispatch, getState);
}

return next(action);
};
export default thunk;

简单使用

不用 redux-thunk 之前

// store.js
import { createStore } from "redux";

export const reducer = (
state = {
count: 0,
},
action
) => {
switch (action.type) {
case "CHANGE_DATA": {
return {
...state,
count: action.data,
};
}
default:
return state;
}
};
export const store = createStore(reducer);
// App.jsx
class ReduxTest extends React.Component {
constructor(props) {
super(props);
store.subscribe(() => {
console.log("subscribe");
this.setState({
count: store.getState().count,
});
});
}
changeData = () => {
const { count } = store.getState();
const action = {
type: "CHANGE_DATA",
data: count + 1,
};
store.dispatch(action);
};
render() {
return (
<div>
<span>{this.state?.count}</span>
<button onClick={this.changeData}>按钮+1</button>
</div>
);
}
}
export default ReduxTest;

对于上述代码,我们 dispatch 一个 action,其中 action 必须为一个对象。

但是实际开发中,action 里的数据往往是一个异步接口获取的数据,这个时候,我们可以

class ReduxTest extends React.Component {
constructor(props) {
super(props);
store.subscribe(() => {
console.log("subscribe", store.getState());
this.setState({
count: store.getState().count,
});
});
}
changeData = () => {
const { count } = store.getState();
let res;
const p = new Promise((resolve) => {
setTimeout(() => {
res = 111;
resolve(res);
}, 1000);
});
p.then((r) => {
const action = {
type: "CHANGE_DATA",
data: r,
};
store.dispatch(action);
});
};
render() {
return (
<div>
<span>{this.state?.count}</span>
<button onClick={this.changeData}>按钮+1</button>
</div>
);
}
}
export default ReduxTest;

但是,上述会把处理的异步的逻辑写在组件里,使代码变得混乱, 因此,假如我 dispatch 一个函数,在这个函数里去处理异步的逻辑,岂不是使代码变得更简洁?!

const getData = () => {
let res;
const p = new Promise((resolve) => {
setTimeout(() => {
res = 111;
resolve(res);
}, 1000);
});
p.then((r) => {
const action = {
type: "CHANGE_DATA",
data: r,
};
store.dispatch(action);
});
};
class ReduxTest extends React.Component {
constructor(props) {
super(props);
store.subscribe(() => {
console.log("subscribe", store.getState());
this.setState({
count: store.getState().count,
});
});
}
changeData = () => {
const { count } = store.getState();
store.dispatch(getData);
};
render() {
return (
<div>
<span>{this.state?.count}</span>
<button onClick={this.changeData}>按钮+1</button>
</div>
);
}
}
export default ReduxTest;

这样,就将组件和异步处理逻辑进行了解耦。

其实没有 redux-thunk,也可以完成同样的功能,只是将处理异步逻辑的代码写在组件里,为了让代码更简洁、解耦,所以通过 redux-thunk 可以 dispatch 一个函数,然后在这个函数里处理异步操作。

源码分析

在使用 Redux 过程,通过 dispatch 方法派发一个 action 对象。当我们使用 redux-thunk 后,可以 dispatch 一个 function。redux-thunk 会自动调用这个 function,并且传递 dispatch, getState 方法作为参数。这样一来,我们就能在这个 function 里面处理异步逻辑,处理复杂逻辑,这是原来 Redux 做不到的,因为原来就只能 dispatch 一个简单对象

redux-thunk 的源码比较简洁,实际就 11 行。前几篇我们说到 redux 的中间件形式, 本质上是对 store.dispatch 方法进行了增强改造,基本是类似这种形式:

const middleware = (store) => (next) => (action) => {};

先给个缩水版的实现:

const thunk =
({ getState, dispatch }) =>
(next) =>
(action) => {
if (typeof action === "function") {
return action(dispatch, getState);
}
return next(action);
};
export default thunk;
  • 原理:即当 action 为 function 的时候,就调用这个 function (传入 dispatch, getState)并返回;如果不是,就直接传给下一个中间件。

完整源码如下:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) =>
(next) =>
(action) => {
// 如果action是一个function,就返回action(dispatch, getState, extraArgument),否则返回next(action)。
if (typeof action === "function") {
return action(dispatch, getState, extraArgument);
}
// next为之前传入的store.dispatch,即改写前的dispatch
return next(action);
};
}

const thunk = createThunkMiddleware();
// 给thunk设置一个变量withExtraArgument,并且将createThunkMiddleware整个函数赋给它
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

我们发现其实还多了 extraArgument 传入,这个是自定义参数,如下用法:

const api = "https://jsonplaceholder.typicode.com/todos/1";
const whatever = 10;

const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({ api, whatever }))
);

// later
function fetchData() {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here
};
}

15.redux-saga 分析

深入浅出 Redux Saga——原理浅析

Side Effects 副作用

我们经常会提到副作用,就是为了处理副作用我们才会使用 Thunk, Saga 这些工具,那什么是副作用?

什么是副作用?

鲁迅说:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

简单地说,只要是跟函数外部环境发生的交互就都属于副作用

是不是还是有点困惑?我们举些例子吧,副作用包括但不限于以下情况:

  • 发送一个 http 请求
  • 更改文件系统
  • 往数据库插入记录
  • 使用 LocalStorage 进行本地存储
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

大概知道什么是副作用之后,我们继续了解两个词:纯函数 & 非纯函数

什么是纯函数?

纯函数:

函数与外界交互唯一渠道就是——参数返回值。也就是说: 函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。

什么是非纯函数?

非纯函数:

函数通过参数和返回值以外的渠道,和外界进行数据交换。 比如,读取/修改全局变量;比如,从 local storage 读取数据,还将其打印到屏幕;再比如在函数里发起一个 http 请求获取数据。。

那为什么我们要追求纯函数?那它当然有它的好处了。

  • 引用透明性:纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果。
  • 可移植性/自文档化:纯函数内部与环境无关,可以自给自足,更易于观察和理解,一切依赖都从参数中传递进来,所以仅从函数签名我们就得知足够的信息。
  • 可缓存性:由于以上特性,纯函数总能够根据输入做缓存,例如 memoize 函数,用一个对象来缓存计算结果。
  • 可测试性:不需要伪造环境,只需简单地给函数一个输入,然后断言输出就好了。
让人讨厌的副作用?

说了这么多纯函数的好处,我们费劲心思将让人讨厌的副作用处理掉,那为什么还要写有副作用的代码啊?

但是回过头想想,副作用都是我们程序里的关键,假如没有副作用,一切都变得毫无意义了。

假如一个前端项目不能发起 http 请求从后端获取信息,

假如一个文件系统或者数据库不能让我们读写数据,

假如不能根据用户的输入从屏幕上输出他们想要的信息

这一切都没有意义了。

所以我们的任务不是消除副作用,而是要把副作用统一管理,避免一些不该出现的/我们并不希望出现的问题,让程序看起来更可控更纯洁~

所以我们才会在项目的状态管理中使用 thunk,saga 等手段处理副作用:)

Why Not Redux Thunk

Redux Thunk 也是处理副作用的一个中间件,那为什么不推荐使用 Redux Thunk 呢?

鲁迅说: 因为丑!它不好看!

redux 的作者提供了 Redux Thunk 中间件给我们集中地处理副作用,所以它的优点就是:可以处理副作用。

但是也就仅提供处理副作用这个功能,处理方式相当粗暴简陋(你看看 thunk 一共就 10 行不到的代码你就懂了),我说说缺点:

  • 内部代码重复且无意义,逻辑复杂(丑)
  • Action 本应是一个纯碎的 JS 对象,但是使用 Thunk 之后 Action 的形式千奇百态(丑)
  • 代码难以测试(因为丑)

举个丑例子

const GET_DATA = "GET_DATA";
const GET_DATA_SUCCESS = "GET_DATA_SUCCESS";
const GET_DATA_FAILED = "GET_DATA_FAILED";

const getDataAction = (id) => (dispatch, getState) => {
dispatch({
type: GET_DATA,
payload: id,
});
api
.getData(id)
.then((res) => {
dispatch({
type: GET_DATA_SUCCESS,
payload: res,
});
})
.catch((err) => {
dispatch({
type: GET_DATA_FAILED,
payload: err,
});
});
};

综上,这不是我们高级而优雅的前端工程师想要的结果!!

Why Redux Saga

那为什么就推荐 Saga 了呢?

鲁迅说: 优雅!高级!一眼看不懂!

我们看一下官方介绍吧。

redux-saga is a library that aims to make application side effects easier to manage, more efficient to execute, easy to test, and better at handling failures.

从介绍中可以看到 Saga 有这么几个特点:

  • 更容易管理副作用
  • 程序更高效执行
  • 易于测试
  • 易于处理错误

那鲁迅为什么说人家优雅高级啊,高在哪儿啊?

Redux Saga 之所以更受我们欢迎,因为它的核心就是巧妙地使用了 ES6 的特性——Generator,基于 Generator 实现异步流程的控制管理。

ES6 Generator

为了更好地理解 Saga 的原理,了解 Generator 的基础知识是必经之路。

function* generator() {
yield "hello";
yield "world";
}

let gen = generator();

gen.next(); // { value: 'hello', done: false}
gen.next(); // { value: 'world', done: false}
gen.next(); // { value: undefined, done: true}

generator 是生成器函数,*:是它的专有标志。

yield 是暂停标志,每次程序运行到 yield 时都会暂停,等待下一次指令的执行;它只能在 generator 函数里,后面跟着一个表达式。

return 是终止标志。

gen 是由 generator 生成器函数生成的一个遍历器对象。

gen 对象拥有 next()方法,调用 next 方法会得到结构为一个内含 value 和 done 属性的对象,value 是 yield 后面表达式的值,done 是遍历是否结束的标志位。

只有执行了 next 才会开始调用 generator 函数。next 传入的参数会当作上一个 yield 表达式的返回值,所以第一次调用 next 传入的参数是无效的。

我们通过一个复杂一点点的例子来了解 Generator 函数

function* generator(x, y) {
// yield
// 暂停标志
// 只能在generator里
// 后面接着一个表达式
let a = yield x + y;
// ⚠️a拿到的是next传来的参数,而不是yield后面的表达式!
// ⚠️因此我们可以通过next函数在外部改变generator内部的行为
let b = yield x * y;
// return
// 终止标志
return a + b;
}

// gen
// 遍历器对象
let gen = generator(1, 2);

gen.next();
// {value: 3, done: false}
gen.next(9);
// {value: 2, done: false}
gen.next(8);
// {value: 17, done: false}
// 只有执行next才会调用generator
// next传入的参数会当作上一个yield表达式的返回值
// 所以第一次调用next传入的参数是无效的

看懂这段代码最关键的点就是

yield 前面的变量拿到的是 next 传来的参数,而不是 yield 后面的表达式!

img

Generator 通过 yield 和 next 来传递数据来控制函数的内部流程

Redux Saga

前面铺垫了这么多,终于要开始讲一下 Saga 了。

Saga 是一个中间件,所以我们首先当然要去注册一下它

import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducers";
import mySaga from "./sagas";

// create the saga middleware
const sagaMiddleware = createSagaMiddleware();
// mount it on the Store
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

// then run the saga
sagaMiddleware.run(mySaga);

export default store;

注册的步骤很简单,调用 createSagaMiddleware,用于创建 saga 中间件;讲 saga 中间件注入 store 中,并执行 run 操作,运行我们写的 saga 函数。

再来一个简单的使用例子:

import { createActions } from "redux-actions";
import { call, put, takeLatest } from "redux-saga/effects";
import { fetchDataApi } from "@api/index";

export const {
data: { fetchDataReq, fetchDataSucc, fetchDataFailed },
} = createActions({
DATA: {
FETCH_DATA_REQ: null,
FETCH_DATA_SUCC: (rsp) => ({ data: rsp }),
FETCH_DATA_FAILED: null,
},
});

function* fetchDataReqSaga() {
try {
const rsp = yield call(fetchDataApi);
yield put(fetchDataSucc(rsp));
} catch (e) {
yield put(fetchDataFailed(e));
}
}

function* watchFetchSaga() {
yield takeLatest(fetchDataReq, fetchDataReqSaga);
}

export default watchFetchSaga;

Redux Thunk 同样的小例子,发起一个 action 触发一个 http 请求,请求成功时发起 success action,请求失败时发起 failed action。

在 Saga 中会使用一种叫做Effect 的指令来完成这些操作。 call,put,takeLatest 等等,都是 Effect 指令。

通俗地讲,call 作用是调用其参数中的函数,pull 作用是发起一个 action,takelatest 作用是监听某个 action 的触发并执行回调函数。

Effects

Effects 就是简单的 JavaScript 对象,我们可以把它视作是发送给 saga middleware 的一些指令,它仅仅是负责向 middleware描述调用行为的信息,而接下来的操作是由 middleware 来执行middleware 执行完毕后将指令的结果回馈给 Generator

也就是说我们只需要通过声明 Effects 的形式,将副作用的部分都留给 middleware 来执行。

这样做的好处: 1: 集中处理异步操作,更流利熟悉地表达复杂的控制流。 2: 保证 action 是个纯粹的 JavaScript 对象,风格保持统一。 3: 声明式指令,无需在 generator 中立即执行,只需通知 middleware 让其执行;借助 generator 的 next 方法,向外部暴露每一个步骤。

接下来看一下一些常用的 effect 指令

  • put 用来命令 middleware 向 Store 发起一个 action。
  • take 用来命令 middleware 在 Store 上等待指定的 action。在发起与 pattern 匹配的 action 之前,Generator 将暂停。
  • call 用来命令 middleware 以参数 args 调用函数 fn。
  • fork 用来命令 middleware 以 非阻塞调用 的形式执行 fn。
  • race 用来命令 middleware 在多个 Effect 间运行,相当于 Promise.race。
  • all 用来命令 middleware 并行地运行多个 Effect,并等待它* 们全部完成,相当于 Promise.all。

Saga 辅助函数

Saga 除了提供 Effects,还会提供一些高阶的辅助函数给我们使用,而这些辅助函数实际上也是基于各个 Effects 实现的。

  • takeEvery(take+fork)
  • takeLatest (take+fork+cancel)
  • takeLeading(take+call)
  • throttle(take+fork+delay)
  • debounce(take+fork+delay)
  • retry (call+delay)

举个简单的例子,当用户点击按钮触发事件时:

  • takeEvery 会并发执行(take+fork);
  • takeLastest 只执行最后一次(take+fork+cancel);
  • takeLeading 只执行第一次(take+call);
  • throttle 节流,触发后一段时间不会再次触发(take+fork+delay);
  • debounce 防抖,等到一段时间后再触发;
  • retry,多次重试触发;

Saga 原理之 Channel(发布订阅模式)

了解完 Saga 的基本使用,我们开始进一步了解它的原理。先说说 Saga 中间件里头有一个 Channel 的东西,它可以理解为一个 action 监听的池子,每次调用 take 指令的时候就会将对应 action 的监听函数放进池子里,当每次调用 put 指令或者外部发起一个 action 的时候,Saga 就会在池子里匹配对应的监听函数并执行,然后将其销毁。

img

channel

以下是简化过的 Saga 源码 channel 部分:

function channel () {
let taker
function take (cb) {
taker = cb
}
function put (input) {
if (taker) {
const tempTaker = taker
taker = null
tempTaker(input)
}
}
return {
put,
take
}
}

const chan = channel()

Saga 原理之自驱动模式

可能你会有疑惑,为什么 Saga 中间件会自动按照程序设定的一般地接受指定的 Effect 指令去执行对应的同步/异步操作呢?

其实这就是 Saga 基于 Generator 的一种自驱动模式(这是我自己起的名字)

回过头想想上文提及 Generator 时归结出的一个结论:

Generator 通过 yield 和 next 来传递数据来控制函数的内部流程

Saga 就是利用了这一点,而且不止这样,Saga 中间件内部有一个驱动函数(effectRunner),它里面生成一个遍历器对象来不断地消费生成器函数(effectProducer)中的 effect 指令,完成指定的任务并递归循环下去。(这时候你可能会想到 TJ 大神的 CO 库)

这个驱动函数大概长这样

function task (saga) {
// 初始化遍历器对象
const sa = saga()
function next (args) {
// 获取到yield后面的表达式——effect指令
const result = sa.next(args)
const effect = result.value
// 执行effect对应的操作——call/put/take...
runEffect(result.value, next)
}
// 执行next函数
next()
}

这样就实现了一个自我驱动的方案,回想一下 Saga 中间件注册的步骤

调用 createSagaMiddleware,用于创建 saga 中间件;讲 saga 中间件注入 store 中,并执行 run 操作,运行我们写的 saga 函数。

当执行 run(saga)的时候其实就是启动了驱动函数(effectRunner),它开始控制我们自己编写的业务流程 Saga 函数(effectProducer),effectRunner 通过next 来控制流程和传递数据(执行结果),effectProducer 通过yield 来发布 effect 指令,这样就完美演绎了 saga 整个生命周期!!

img

Saga 源码

实际上 Saga 源码中也就这么一回事,主要挑了 channel 和自驱动函数两块东西来看,在 Saga 源码中有一个 proc 函数,其实就是上文提到的自驱动函数,它接收到 effect 指令之后进行 effect 类型分发,不同 effect 对应不同的操作。

img

Saga 测试

Saga 还有一个优点记得吗?易于测试,由于业务代码都是声明式地调用,而不是真实地进行一些副作用操作,所以在写单测的时候可以通过断言的方式来测试,而不用 mock 一些繁琐复杂的操作。

16.手写 redux 中间件

简单实现

function createStore(reducer) {
let currentState;
let listeners = [];

function getState() {
return currentState;
}

function dispatch(action) {
currentState = reducer(currentState, action);
listeners.map((listener) => {
listener();
});
return action;
}

function subscribe(cb) {
listeners.push(cb);
return () => {};
}

dispatch({ type: "ZZZZZZZZZZ" });

return {
getState,
dispatch,
subscribe,
};
}

// 应用实例如下:
function reducer(state = 0, action) {
switch (action.type) {
case "ADD":
return state + 1;
case "MINUS":
return state - 1;
default:
return state;
}
}

const store = createStore(reducer);

console.log(store);
store.subscribe(() => {
console.log("change");
});
console.log(store.getState());
console.log(store.dispatch({ type: "ADD" }));
console.log(store.getState());

迷你版

export const createStore = (reducer,enhancer)=>{
if(enhancer) {
return enhancer(createStore)(reducer)
}
let currentState = {}
let currentListeners = []

const getState = ()=>currentState
const subscribe = (listener)=>{
currentListeners.push(listener)
}
const dispatch = action=>{
currentState = reducer(currentState, action)
currentListeners.forEach(v=>v())
return action
}
dispatch({type:'@@INIT'})
return {getState,subscribe,dispatch}
}

//中间件实现
export applyMiddleWare(...middlewares){
return createStore=>...args=>{
const store = createStore(...args)
let dispatch = store.dispatch

const midApi = {
getState:store.getState,
dispatch:...args=>dispatch(...args)
}
const middlewaresChain = middlewares.map(middleware=>middleware(midApi))
dispatch = compose(...middlewaresChain)(store.dispatch)
return {
...store,
dispatch
}
}

// fn1(fn2(fn3())) 把函数嵌套依次调用
export function compose(...funcs){
if(funcs.length===0){
return arg=>arg
}
if(funs.length===1){
return funs[0]
}
return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}


//bindActionCreator实现

function bindActionCreator(creator,dispatch){
return ...args=>dispatch(creator(...args))
}
function bindActionCreators(creators,didpatch){
//let bound = {}
//Object.keys(creators).forEach(v=>{
// let creator = creator[v]
// bound[v] = bindActionCreator(creator,dispatch)
//})
//return bound

return Object.keys(creators).reduce((ret,item)=>{
ret[item] = bindActionCreator(creators[item],dispatch)
return ret
},{})
}

16.Redux 源码分析

实现一个 Redux(完善版)

带你从头到尾系统地撸一遍 Redux 源码

image-20220920221954896

types 主要存放的是 ts 定义的一些类型, utils 主要是一些通用方法,没什么涉及关键流程的,所以主要分析 applyMiddleware,combineReducers,compose, createStrore 这 4 个 ts 文件

CreateStore

// 引入 redux
import { createStore } from 'redux'

// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)

);

从图中 createStore 接受 3 个参数

  • 第一个参数就是一个 reducer 一个纯函数 ,由我们自己定义
  • 第二个参数 初始化的数状态
  • 第三个参数 其实就是制定中间件 在源码中就是 enhancer 增强 store

从拿到入参到返回出 store 的过程中,到底都发生了什么呢?这里是 createStore 中主体逻辑的源码:

a79b5992ec074da1bf65a3c30e60d844 (1)

这段代码主要是做一些类型判断, 和一些写法兼容没什么。继续往下看, 下面是一些初始状态的赋值:

24198c5fdf4343ccb7aa22e5af0965b2

接下来就进入我们经常用的 getState 函数了。

getState

96a2787b556e4becb108b4fb0810ec8f

继续往下看

subscribe

dcc54d529c904727ad982a2a185d56d1

这里订阅的时候浅拷贝了一下,卸载的时候也浅拷贝,用的都是nextListeners, 还记得有个currentListeners,接着往下看。

dispatch

64dbac4766484026a7cf805910c63ab8

dispatch 的时候:又将 next 重新复制给 current,然后执行每个 listenr,看到这里应该明白 reducer 中 dispatch 或者做一些 subscribe 做一些脏操作,redux 源码中为了防止这种 就是设置 isDispatching 这个 变量来控制。

所以 dispacth 一个 action? Redux 帮我们做了啥事,就很简单 2 件事

  1. oldState 经过 reducer 产生了 newState, 更新了 store 数据中心
  2. 触发订阅

整个 Redux 的工作流,到这里其实已经结束了, 但是还有一个疑问就是 subscribe 为啥都是 nextListeners 然后在 dispatch 中的又把值重新赋给 currentListeners?

答案就是:为了保证触发订阅的稳定性

这句话怎么理解呢我举一个例子:

// 定义监听函数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)

从上文我可以得知当前的 currentListeners:

[listenera, listenerb, listenerc];

但是比较特殊的是 listenb 其实卸载 listena 的,如果我们不浅拷贝一下, 那么触发订阅的时候数组遍历到 i = 2 的时候其实数组是 undefined ,这样引发报错,因为我们在 订阅前和卸载订阅都浅拷贝一下,nextListeners 数据随便怎么变,只要保证currentListener 稳定就好了。

本次 dispacth 完之后,下一次 dispacth 假设没有新增订阅,数据关系又重新赋值。

listeners =( currentListeners = nextListeners)

这也是为什么 Redux 订阅稳定的原因了。 接下来就是分析 Redux 的中间件模型。

Redux 中的中间件思想

要想理解 redux 的中间件思想, 我们先看下 compose 这个文件做了什么, 这个文件其实做的事情十分简单。主要是用到函数式编程中的组合概念, 将多个函数调用组合成一个函数调用。

其实主要是 reduce 的这个 api,简单手写下数组reduce 的实现:

img

这东西其实就是个累加器按照某种方式。我们接下来就直接进入 compose 函数话不多说直接看代码:

// 参数是函数数组
export default function compose(...funcs: Function[]) {
// 处理边界情况
if (funcs.length === 0) {
return <T>(arg: T) => arg;
}
// 数量为1 就没有组合的必要了
if (funcs.length === 1) {
return funcs[0];
}
// 主要是下面这一行代码
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
);
}

分析下这行代码,举例子说明: 假设我们有这个 3 个函数:

funcs = [fa, fb, fc];

由于没有出初始值累加器的就是 fa 经过一次遍历后, accumulateur 变为下面的样子:

let  m=...args) => fa(fb(...args))

在经过一次遍历后,此时 b = fc 此时 accumulateur 变为下面的样子:

...args) => m(fc(...args))

我们将 fc(...args) 看做一个整体带入上面 m 的函数 所以就变成了下面的样子

...args)=> fa(fb(fc(...args)))

到这里就大工告成了,fa, fb, fc 我们可以想象成 redux 的 3 个中间件, 按照顺序传进去,

当这个函数被调用的时候也会按照 fa, fb, fc 顺序调用。 接下来看 compose 是如何和 Redux 做结合的。

applyMiddleWare

先把整体结构分析下,函数参数分下:

// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组

export default function applyMiddleware(...middlewares) {

// 它返回的是一个接收 createStore 为入参的函数

return createStore => (...args) => {

......

}

}

createStore 就是 上面我们分析过的 创建数据中心 Store ,而 args 主要是有两个, 还是 createStore 两个约定入参 一个是 reducer, 一个是 initState

enhance-dispatch

接下来就是比较核心的, 改写 dispacth, 为什么要改写 dispatch, 还是举个例子说明。 dispacth 接受的 action 只能是对象,如果不是对象的话会直接报类型错误。如图:

image-20220920224351438

社区比较有名的 redux-thunk 中间件, dispatch 可以接受一个函数,

应用了中间件的dispatch 和 没有用中间的dispatch 肯定是不等的,
dispatch = enhancer(dispacth) 肯定是增强的至于怎么个增强法 继续往下看。
const store = createStore(...args)
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI)) // 绑定 {dispatch和getState}
dispatch = compose(...chain)(store.dispatch)

从代码中可以看到的第一步 其实就是将 getState 和 dispatch 作为中间件中的闭包使用, 有人会这里提问,这里为什么是匿名函数 ? 而不是 store.dispatch ?

举个例子说明,上面的 fa , fb, fc 接下来把他们展开。

function fa(store) {
return function (next) {
return function (action) {
console.log("A middleware1 开始");
next(action);
console.log("B middleware1 结束");
};
};
}

function fb(store) {
return function (next) {
return function (action) {
console.log("C middleware2 开始");
next(action);
console.log("D middleware2 结束");
};
};
}

function fc(store) {
return function (next) {
return function (action) {
console.log("E middleware3 开始");
next(action);
console.log("F middleware3 结束");
};
};
}

ok 我们一步一步分析, 看到底做了什么。

chain = middlewares.map((middleware) => middleware(middlewareAPI));

很显然 我们 的 middlewares = [fa, fb, fc],map 之后返回一个新的 chain 这时候的 chain 应该是下面 这样子的:

chain = [ (next)=>(action)=>{...}, (next) => (action) => {...}, (next) => (action) => {...} ]

只不过 chain 中的每一个元素 都有 getSate,dispatch 的闭包而已

继续往下走就到了 compose 函数 还记得上面 compose(fa, fb, fc ) 返回值是什么?

(...args)=> fa(fb(fc(...args)))

就是这个东西,这里的 chain 也是一样的, 所以这里的 dispatch 就变成了增强的 dispatch,一起看下

dispatch = fa(fb(fc(store.dispacth)));

看到这里有人就问? 每一个中间件的 next 和 原先的 store.dispacth 有什么不同 ? 这和洋葱模型有什么关系?

那我就带你一步一步分析, 这里的 dispatch 就是指的是当前的 fa(fb(fc(store.dispatch))) , 我们可以直接函数调用来分析,

fa 的参数 是 fb(fc(store.dispatch)) , 由于依赖 fb, 所以调用 fb, 然后发现 fb 依赖的参数是 fc(store.dispacth), 紧接着又开始调用 fc, ok 到这里终于结束了, 终于没有依赖了。 所以从上面的过程我们可以得到 next 其实是他上个中间件的副作用, 最后一个中间件的 next 就是 store.dispatch。

副作用: 每个中间件的中的 (action )=> {...}

我用流程图表示整个洋葱模型流程:

image-20220920224733879

当我调用 dispatch 的时候, 先打印 E, 然后发现 next 是副作用 fb, 然后调用副作用 fb, 打印 c,发现 next 竟然是 副作用 fc ,再去调用 fc, 打印 A, next 这时候就是 store.dispacth, 调用结束, 打印 b, 然后打印 D, 最后打印 F 。 这样的一系列操作是不是有点像洋葱模型,对于之前提出的问题?

1.为什么 dispacth 是匿名函数?

2.为什么 dispacth 一个 action 后, 还是返回 action

问题 1: 为什么 dispatch 是是一个匿名函数,因为有的中间件原理的实现,并不会 next(action), 这时候需要肯定是增强的 dispacth, redux-thunk 的执行原理, 就是当你传递一个函数, 直接调用这个函数,并把 dispatch 的 权限 交给你自己处理。

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === "function") {
// 这个的dispatch 其实是增强的dispatch,
// 如果用store.dispatch如果还有其他中间件就丢失了 return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

这里看下源码:

let dispatch: Dispatch = () => {
throw new Error('Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.')
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose 完之后将闭包更新成增强的
dispatchdispatch = compose < typeof dispatch > (...chain) (store.dispatch)

所以 Redux 严格 意义上 并不算是洋葱模型, 他的洋葱模型是建立在你的每个中间件,都要 next(action); 如果不 next(action) 其实就破坏了洋葱模型。

问题 2: dispatch (action) 返回 action, 就是为了方便下一个中间件的处理。

17.React-Redux 源码

「源码解析」一文吃透 react-redux 源码(useMemo 经典源码级案例)