React 更新视图过程

真实的 setState 的过程:

setState( partialState ) {
  // 1. 通过组件对象获取到渲染对象
  var internalInstance = ReactInstanceMap.get(publicInstance);
  // 2. 把新的状态放在渲染对象的 _pendingStateQueue 里面 internalInstance._pendingStateQueue.push( partialState )
  // 3. 查看下是否正在批量更新
  //   3.1. 如果正在批量更新,则把当前这个组件认为是脏组件,把其渲染对象保存到 dirtyComponents 数组中
  //   3.2. 如果可以批量更新,则调用 ReactDefaultBatchingStrategyTransaction 开启更新事务,进行真正的 vdom diff。
  //    |
  //    v
  // internalInstance.updateComponent( partialState )
}

updateComponent 方法的说明:

updateComponent( partialState ) {
  // 源码中 partialState 是从 this._pendingStateQueue 中获取的,这里简化了状态队列的东西,假设直接从外部传入
  var inst = this._instance;
  var nextState = Object.assign( {}, inst.state, partialState );
  // 获得组件对象,准备更新,先调用生命周期函数
      // 调用 shouldComponentUpdate 看看是否需要更新组件(这里先忽略 props 和 context的更新)
  if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) {
    // 更新前调用 componentWillUpdate
    isnt.componentWillUpdate( inst.props, nextState, nextContext );
    inst.state = nextState;
    // 生成新的 vdom
    var nextRenderedElement = inst.render();
    // 通过上一次的渲染对象获取上一次生成的 vdom
    var prevComponentInstance = this._renderedComponent; // render 中的根节点的渲染对象
    var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根节点的 vdom
    // 通过比较新旧 vdom node 来决定是更新 dom node 还是根据最新的 vdom node 生成一份真实 dom node 替换掉原来的
    if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) {
      // 更新 dom node
      prevComponentInstance.receiveComponent( nextRenderedElement )
    } else {
      // 生成新的 dom node 替换原来的(以下是简化版,只为了说明流程)
      var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance );
      // 根据新的 vdom 生成新的渲染对象
      var child = instantiateReactComponent( nextRenderedElement );
      this._renderedComponent = child;
      // 生成新的 dom node
      var nextMarkup = child.mountComponent();
      // 替换原来的 dom node
      oldHostNode.empty();
      oldHostNode.appendChild( nextMarkup )
    }
  }
}

接下来看下 shouldUpdateReactComponent 方法:

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return (nextType === 'string' || nextType === 'number');
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

react 渲染流程

1. 从 jsx到dom 的过程

class Form extends React.Component {
  constructor() {
    super();
  }
  render() {
    return (
        <form>
          <input type="text"/>
        </form>
    );
  }
}

ReactDOM.render( (
  <div className="test">
    <span onClick={function(){}}>CLICK ME</span>
    <Form/>
  </div>
), document.getElementById('main'))

1.1  打包时babel 把 jsx 转成 浏览器可以执行的 js ,如下代码, (react17 会使用 JSX 转换器添加 react/jsx-runtime 和 react/jsx-dev-runtime)

React.createElement( 'div', {
  className: 'test'
  },
  React.createElement( 'span',
    { onClick: function(){} },
    'CLICK ME'
  ),
  React.createElement(Form, null)
)

1.2  API: React.createElement(component, props, …children) 它生成一个 js 的对象, 俗称的虚拟dom,

虚拟 dom 的意思是用 js 对象结构模拟出 html 中 dom 结构,批量的增删改查先直接操作 js 对象,最后更新到真正的 dom 树上, 要比操作真实dom要快), 生成对应出来的虚拟 dom 

{
  type: 'div',
  props: {
    className: 'xxx',
    children: [ {
      type: 'span',
      props: {
        children: [ 'CLICK ME' ]
      },
      ref:
      key:
    }, {
      type: Form,
      props: {
        children: []
      },
      ref:
      key:
    } ] | Element
  }
  ref: 'xxx',
  key: 'xxx'
}

1.3 有了虚拟 dom,接下来的工作就是把这个虚拟 dom 树真正渲染成一个 dom 树,

React 的做法是针对不同的 type 构造相应的渲染对象,渲染对象提供一个 mountComponent 方法(负责把对应的某个虚拟 dom 的节点生成成具体的 dom node),然后循环迭代整个 vdom tree 生成一个完整的 dom node tree,最终插入容器节点。查看源码你会发现如下代码

// vdom 是第2步生成出来的虚拟 dom 对象
var renderedComponent = instantiateReactComponent( vdom );
// dom node
var markup = renderedComponent.mountComponent();
// 把生成的 dom node 插入到容器 node 里面,真正在页面上显示出来
// 下面是伪代码,React 的 dom 操作封装在 DOMLazyTree 里面
containerNode.appendChild( markup );

1.3.1 instantiateReactComponent 传入的是虚拟 dom 节点,这个方法做的就是根据不同的 type 调用如下方法生成渲染对象:

// 如果节点是字符串或者数字
return ReactHostComponent.createInstanceForText( vdom(string|number) );
// 如果节点是宿主内置节点,譬如浏览器的 html 的节点
return ReactHostComponent.createInternalComponent( vdom );
// 如果是 React component 节点
return new ReactCompositeComponentWrapper( vdom );
  • div 等 html 的原生 dom 节点对应的渲染对象是 ReactDOMComponent 的实例。如何把 { type:’div’, … } 生成一个 dom node 就在这个类(的 mountComponent 方法)里面。
class ReactDOMComponent {
  constructor( vdom ) {
    this._currentElement = vdom;
  }
  mountComponent() {
    var result;
    var props = this._currentElement.props;
    if ( this._currentElement.type === 'div' ) {
      result = document.createElement( 'div' );
      
      for(var key in props ) {
        result.setAttribute( key, props[ key ] );
      }
    } else {
      // 其他类型
    }
    // 迭代子节点
    props.children.forEach( child=>{
      var childRenderedComponent =  = instantiateReactComponent( child );
      var childMarkup = childRenderedComponent.mountComponent();
      result.appendChild( childMarkup );
    } )
    return result;
  }
}
  • 我们再看下 React component 的渲染对象 ReactCompositeComponentWrapper(主要实现在 ReactCompositeComponent 里面,ReactCompositeComponentWrapper 只是一个防止循环引用的 wrapper
// 以下是伪代码
class ReactCompositeComponent {
  _currentElement: vdom,
  _rootNodeID: 0,
  _compositeType:
  _instance: 
  _hostParent:
  _hostContainerInfo: 
  // See ReactUpdateQueue
  _updateBatchNumber:
  _pendingElement:
  _pendingStateQueue:
  _pendingReplaceState:
  _pendingForceUpdate:
  _renderedNodeType:
  _renderedComponent:
  _context:
  _mountOrder:
  _topLevelWrapper:
  // See ReactUpdates and ReactUpdateQueue.
  _pendingCallbacks:
  // ComponentWillUnmount shall only be called once
  _calledComponentWillUnmount:

  // render to dom node
  mountComponent( transaction, hostParent, hostContainerInfo, context ) {
    // ---------- 初始化 React.Component --------------
    var Component = this._currentElement.type;
    var publicProps = this._currentElement.props;
    /*
      React.Component 组件有2种:
      new Component(publicProps, publicContext, updateQueue);
      new StatelessComponent(Component);
      对应的 compositeType 有三种
      this._compositeType = StatelessFunctional | PureClass | ImpureClass,
      组件种类和 compositeType 在源码中都有区分,但是这里为了简单,只示例最常用的一种组件的代码
    */
    var inst = new Component(publicProps, publicContext, updateQueue);
    
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
    
    // 渲染对象存储组件对象
    this._instance = inst;

    // 通过 map 又把组件对象和渲染对象联系起来
    ReactInstanceMap.set(inst, this);
    /*
      ReactInstanceMap: {
              -----------------------------------------------
              |                                              |
              v                                              |
        React.Component: ReactCompositeComponentWrapper {    |
          _instance:  <-------------------------------------
        }
      }
      这样双方都在需要对方的时候可以获得彼此的引用
    */

    // ---------- 生成 React.Component 的 dom  --------------
    // 组件生命周期函数 componentWillMount 被调用
    inst.componentWillMount();
    // 调用 render 方法返回组件的虚拟 dom
    var renderedElement = inst.render();
    // save nodeType  
    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    // 根据组件的虚拟 dom 生成渲染对象
    var child = instantiateReactComponent(renderedElement)
    this._renderedComponent = child;
    // 生成真正的 dom node
    // 其实源码中的真正代码应该是 var markup = ReactReconciler.mountComponent( child, ... ),
    // 这里为了简化说明,先不深究 ReactReconciler.mountComponent 还做了点什么
    var markup = child.mountComponent(); 
    // 把组件生命周期函数 componentDidMount 注册到回调函数中,当整个 dom node tree 被添加到容器节点后触发。
    transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    return markup;
  }
}
// static member
ReactCompositeComponentWrapper._instantiateReactComponent = instantiateReactComponent

最终流程

jsx –babel–> react.createElement() 打包阶段生成 —> 渲染阶段 createElement 执行 生成虚拟dom

–> vdom 生成渲染对象

ReactDOMComponent -> <span/>
ReactCompositeComponentWrapper.render() -> vdom -> instantiateReactComponent(vdom) -> <form><input/></from>

–> 挂载 (containerDomNode.appendChild( domNode );)

组件封装

form 组件高度封装

对于form 表单项一般有 基础组件(input, select 等), 业务组件 (需要获取数据,且比较复杂, 比如Tree 选择)

思路: 通用表单项样式统一,组件prop加上可渲染的组件来定义业务表单组件。

//vue ,xComponent 传入组件名, 通过异步组件实现
<SField
  label="覆盖机构" type="array" name="agencyIds" :enum="typePurposeList" xComponent="MultiAgencySelect"/>


//react 
<SField render={<MultiAgencySelect/>}/>
// 根据名称加载异步组件
<template>
  <component :is="asyncComponent" v-bind="$attrs" v-on="$listeners" />
</template>

<script>
const loadView = view => {
  // 路由懒加载
  return () => Promise.resolve(require(`@/components/business/${view}`).default);
};
// 动态异步组件
export default {
  name: 'AsyncComponent',
  props: {
    name: {
      type: String,
    },
  },
  data() {
    return {
      asyncComponent: this.render(),
    };
  },
  watch: {
    name() {
      this.asyncComponent = this.render();
    },
  },
  methods: {
    render() {
      return loadView(this.name);
    },
  },
};
</script>

react setState 异步执行问题

link

如果同步执行 的话

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

通常共享状态需要做状态提升

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

这就回打乱我们代码,props只会在父组件更新时更新

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

在同步这种情况下,会立即刷新this.state,但不会刷新this.props。我们无法在不重新渲染父级的情况下立即刷新this.props(如果同步刷新,性能大大降低)

if you’re mixing data from props (not yet flushed) and state (proposed to be flushed immediately) to create a new state

那么React今天如何解决这个问题呢?在React中,this.state和都this.props仅在协调和冲洗之后更新,因此您会看到0在重构前后都被打印。这使提升状态安全。

< a > javascript: URLs

a 标签里写 javascript: void(0) 或在其他

  1. If you’re just using <a> for the link styling, consider using a <button> and styling it as a link instead.
  2. If you still must use <a> for some reason, a workaround is to give it href=”#” onClick={e => e.preventDefault()} .This is not great, so prefer option 1 if you can.
  3. If you absolutely insist on using a javascript: URL, set it yourself in a ref: <a ref={node => node && node.setAttribute(‘href’, ‘…’)} >. This works for the bookmarklet case.
  4. If you have hundreds of links like this, as the original post says, one possible solution is to codemod every <a> to <Mylink> and implement either of the above solutions in Mylink. This blog post has an example of something similar.

react typescript

Interfaces

A better way to define our ExtendedButton element would be to extend a native HTML button element type like so:

import React, {ButtonHTMLAttributes} from 'react';

interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    /** The text inside the button */
    text: string,
    /** The type of button, pulled from the Enum ButtonTypes */
    type: ButtonTypes,
    /** The function to execute once the button is clicked */
    action: () => void
}

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {

}

Enums

//...
/** A set of groupped constants */
enum SelectableButtonTypes {
    Important = "important",
    Optional = "optional",
    Irrelevant = "irrelevant"
}

interface IButtonProps {
    text: string,
    /** The type of button, pulled from the Enum SelectableButtonTypes */
    type: SelectableButtonTypes,
    action: (selected: boolean) => void
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {
        setSelected(!selected)
        action(selected)
    }}>{text}</button>)
}

/** Exporting the component AND the Enum */
export { ExtendedSelectableButton, SelectableButtonTypes}

Importing and using Enums:

import React from 'react';
import './App.css';
import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {
          console.log(selected) 
        }} />       
        
      </header>
    </div>
  );
}

export default App;

Optional types for your props

//...
interface IProps {
  prop1: string,
  prop2: number, 
  myFunction: () => void,
  prop3?: boolean //optional prop
}

//...
function MyComponent({...props}: IProps) {
  //...
}

/** You can then use them like this */
<mycomponent prop1="text here" prop2=404 myFunction={() = {
  //...
}} />

Hooks

Thanks to TypeScript’s type validation, you can enforce the type (or interface) of the initial value of the state, like this:

const [user, setUser] = React.useState<IUser>(user);

Nullable values to hooks

const [user, setUser] = React.useState<IUser | null>(null);

// later...
setUser(newUser);

Generic Components 通用组件

interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>(props: Props<T>) {
  const { items, renderItem } = props;
  const [state, setState] = React.useState<T[]>([]); 
  
  return (
    <div>
      {items.map(renderItem)}
    </div>
  );
}
//type inference
ReactDOM.render(
  <List
    items={["a", "b"]} // type of 'string' inferred here
    renderItem={item => (
      <li key={item}>
        {item.trim()} //allowed, because we're working with 'strings' all around 
      </li>
    )}
  />,
  document.body
);
//directly specifying the data types
ReactDOM.render(
  <List<number>
    items={[1,2,3,4]} 
    renderItem={item => <li key={item}>{item.toPrecision(3)}</li>}
  />,
  document.body
);

Extending HTML Elements


export interface IBorderedBoxProps extends React.HTMLAttributes<HTMLDivElement> {
    title: string;
}

class BorderedBox extends React.Component<IBorderedBoxProps, void> {
    public render() {
        const {children, title, ...divAttributes} = this.props;

        return (
            //it is a DIV afterall, and we're trying to let the user use this component knowing that.
            <div {...divAttributes} style={{border: "1px solid red"}}>
                <h1>{title}</h1>
                {children}
            </div>
        );
    }
}

const myBorderedBox = <BorderedBox title="Hello" onClick={() => alert("Hello")}/>;

Event Types

function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {
    console.log("TEST!")
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)

//And you’ll see an error message 

//so You can, however, use unions to allow a single handler to be re-used by multiple components:

/** This will allow you to use this event handler both, on anchors and button elements */
function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {
    console.log("TEST!")
}

redux saga

reference Link

takeEvery takeLalest

takeEvery 允许多个 fetchData 实例同时启动,在某个特定时刻,尽管之前还有一个或多个 fetchData 尚未结束,我们还是可以启动一个新的 fetchData 任务,

takeEvery 不同,在任何时刻 takeLatest 只允许一个 fetchData 任务在执行。并且这个任务是最后被启动的那个。 如果已经有一个任务在执行的时候启动另一个 fetchData ,那之前的这个任务会被自动取消。

下面创建一个 Saga 来监听所有的 USER_FETCH_REQUESTED action,并触发一个 API 调用获取用户数据

// sagas.js

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

/*
  在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
  允许并发(译注:即同时处理多个相同的 action)
*/
function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
  // 监听指定action 并在dispatch('USER_FETCH_REQUESTED') 执行fetchData
}

/*
  也可以使用 takeLatest

  不允许并发,dispatch 一个 `USER_FETCH_REQUESTED` action 时,
  如果在这之前已经有一个 `USER_FETCH_REQUESTED` action 在处理中,
  那么处理中的 action 会被取消,只会执行当前的
*/
function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;

call apply cps

call apply 处理 Promise 的函数和 Generator 函数,会阻塞后面, cps 适合处理Node callback 风格的函数

yield call([obj, obj.method], arg1, arg2, ...) // 如同 obj.method(arg1, arg2 ...)

yield apply(obj, obj.method, [arg1, arg2, ...])

//下面是node 回掉形式处理异步, 在saga 写法
// readFile('/path/to/file', (error, result) => ()) 
import { cps } from 'redux-saga'
const content = yield cps(readFile, '/path/to/file')
 // effects 将会同时执行, 同Promise.all 行为
const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]

put

redux-saga 为此提供了另外一个函数 put,这个函数用于创建 dispatch Effect。声明式的解决方案。只需创建一个对象来指示 middleware 我们需要发起一些 action,然后让 middleware 执行真实的 dispatch。 好处对于测试 只需检查 yield 后的 Effect,并确保它包含正确的指令

dispatch({ type: 'PRODUCTS_RECEIVED', products })
yield put({ type: 'PRODUCTS_RECEIVED', products })

take

它创建另一个命令对象,告诉 middleware 等待一个特定的 action, 直到匹配的action 被发起

import { take, put } from 'redux-saga/effects'
// #并在用户初次创建完三条 Todo 信息时显示祝贺信息
function* watchFirstThreeTodosCreation() {
  for (let i = 0; i < 3; i++) {
    const action = yield take('TODO_CREATED')
  }
  yield put({type: 'SHOW_CONGRATULATION'})
}
//  同一个地方写控制流 
// login 后跟着 logout 监听, logout 后也始终跟着login 监听
function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}

fork

call 会阻塞后面流程, fork不用等待fork的任务结束

const {user, password} = yield take('LOGIN_REQUEST')
//1不用等到fork 任务完成,下面会接着执行,如果监听到 logout
//2 直接取消fork 任务,保持状态统一 
//3 fork 返回 Task Object, 所以任务内容全部放在 authorize 里
const task = yield fork(authorize, user, password)
 const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
 if(action.type === 'LOGOUT')
    yield cancel(task) // 不管fork任务有没有完成,直接取消
    yield call(Api.clearItem('token'))
 }
//4 取消fork 任务后,可能还残留 loginLoading 状态清除,可以在fork 
// 异步任务里 try catch finally处 或者 退出登陆时 发送 清除loading

race

在多个 Effects 之间触发一个竞赛

触发一个远程的获取请求,并且限制了 1 秒内响应,否则作超时处理
function* fetchPostsWithTimeout() {
  const {posts, timeout} = yield race({
    posts: call(fetchApi, '/posts'),
    timeout: call(delay, 1000)
  })

  if (posts)
    put({type: 'POSTS_RECEIVED', posts})
  else
    put({type: 'TIMEOUT_ERROR'})
}

race 的另一个有用的功能,自动取消那些失败的 Effects,假设有 2 个 按钮:

第一个用于在后台启动一个任务,这个任务运行在一个无限循环的 while(true) 中(例如:每 x 秒钟从服务器上同步一些数据)

function* backgroundTask() {
  while (true) { ... }
}

function* watchStartBackgroundTask() {
  while (true) {
    yield take('START_BACKGROUND_TASK')
    yield race({
      task: call(backgroundTask),
      cancel: take('CANCEL_TASK')
    })
  }
}
//在 CANCEL_TASK action 被发起的情况下,race Effect 将自动取消 backgroundTask,并在 backgroundTask 中抛出一个取消错误。

取消任务

import { take, put, call, fork, cancel, cancelled, delay } from 'redux-saga/effects'
import { someApi, actions } from 'somewhere'

function* bgSync() {
  try {
    while (true) {
      yield put(actions.requestStart())
      const result = yield call(someApi)
      yield put(actions.requestSuccess(result))
      yield delay(5000)
    }
  } finally {
    if (yield cancelled())
      yield put(actions.requestFailure('Sync cancelled!'))
  }
}

function* main() {
  while ( yield take(START_BACKGROUND_SYNC) ) {
    // 启动后台任务
    const bgSyncTask = yield fork(bgSync)

    // 等待用户的停止操作
    yield take(STOP_BACKGROUND_SYNC)
    // 用户点击了停止,取消后台任务
    // 这会导致被 fork 的 bgSync 任务跳进它的 finally 区块
    yield cancel(bgSyncTask)
  }
}

自动取消

两种情况会自动取消

  • 在race 任务中,一个成功其他任务都会被cancel
  • 在并行的 Effect (yield […., …]) 中, 其中一个报错,其他的任务都被cancel

yield all[…,…] 和 yield […,…] 不同

Sagas 被实现为 Generator functions,它会 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行

当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。 在上面的例子中,incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve。

一旦 Promise 被 resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield。 在这个例子中,下一个语句是另一个被 yield 的对象:调用 put({type: ‘INCREMENT’}) 的结果,意思是告诉 middleware 发起一个 INCREMENT 的 action。

put 就是我们称作 Effect 的一个例子。Effects 是一些简单 Javascript 对象,包含了要被 middleware 执行的指令。 当 middleware 拿到一个被 Saga yield 的 Effect,它会暂停 Saga,直到 Effect 执行完成,然后 Saga 会再次被恢复。

put({type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000)        // => { CALL: {fn: delay, args: [1000]}}

middleware 检查每个被 yield 的 Effect 的类型,然后决定如何实现哪个 Effect。如果 Effect 类型是 PUT 那 middleware 会 dispatch 一个 action 到 Store。 如果 Effect 类型是 CALL 那么它会调用给定的函数。

redux Dispatch 理解

dispatch 触发 reduce 纯函数更新, 其中为了处理 一些副作用,或者异步action ,需要对dispatch 进行包装,相当于中间件的概念middleware。 dispatch 函数

如下返回新的 dispatch 函数

function logger(store) {
  let next = store.dispatch

  // 我们之前的做法:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}
可以让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。
const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}
// 警告:这只是一种“单纯”的实现方式!
// 这 *并不是* Redux 的 API.

function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse() // 为什么反转
  // 每个middleware 都接受 state 和上一次的 返回的dispatch函数 
 // crashReporter 中间件先被遍历, 返回 dispatch函数,内层next 才是初始的store.dispatch
// logger 后遍历, 这时返回新的dispatch函数 已经包含了looger 中间件的处理内容;
// 如果 disptch 一个action, 先经过 logger (action) -> crashReporter (action) -> 最后执行 store.dispatch(action)

  let dispatch = store.dispatch
  middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
  )

  return Object.assign({}, store, { dispatch })
}

然后是将它们引用到 Redux store 中:

import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() 告诉 createStore() 如何处理中间件
  applyMiddleware(logger, crashReporter)
)

就是这样!现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:

// 将经过 logger 和 crashReporter 两个 middleware!
store.dispatch(addTodo('Use Redux'))

7个示例

/**
 * 记录所有被发起的 action 以及产生的新的 state。
 */
const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

/**
 * 在 state 更新完成和 listener 被通知之后发送崩溃报告。
 */
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

/**
 * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。
 * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。
 */
const timeoutScheduler = store => next => action => {
  if (!action.meta || !action.meta.delay) {
    return next(action)
  }

  let timeoutId = setTimeout(
    () => next(action),
    action.meta.delay
  )

  return function cancel() {
    clearTimeout(timeoutId)
  }
}

/**
 * 通过 { meta: { raf: true } } 让 action 在一个 rAF 循环帧中被发起。
 * 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。
 */
const rafScheduler = store => next => {
  let queuedActions = []
  let frame = null

  function loop() {
    frame = null
    try {
      if (queuedActions.length) {
        next(queuedActions.shift())
      }
    } finally {
      maybeRaf()
    }
  }

  function maybeRaf() {
    if (queuedActions.length && !frame) {
      frame = requestAnimationFrame(loop)
    }
  }

  return action => {
    if (!action.meta || !action.meta.raf) {
      return next(action)
    }

    queuedActions.push(action)
    maybeRaf()

    return function cancel() {
      queuedActions = queuedActions.filter(a => a !== action)
    }
  }
}

/**
 * 使你除了 action 之外还可以发起 promise。
 * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。
 * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。
 */
const vanillaPromise = store => next => action => {
  if (typeof action.then !== 'function') {
    return next(action)
  }

  return Promise.resolve(action).then(store.dispatch)
}

/**
 * 让你可以发起带有一个 { promise } 属性的特殊 action。
 *
 * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。
 *
 * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。
 */
const readyStatePromise = store => next => action => {
  if (!action.promise) {
    return next(action)
  }

  function makeAction(ready, data) {
    let newAction = Object.assign({}, action, { ready }, data)
    delete newAction.promise
    return newAction
  }

  next(makeAction(false))
  return action.promise.then(
    result => next(makeAction(true, { result })),
    error => next(makeAction(true, { error }))
  )
}

/**
 * 让你可以发起一个函数来替代 action。
 * 这个函数接收 `dispatch` 和 `getState` 作为参数。
 *
 * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
 *
 * `dispatch` 会返回被发起函数的返回值。
 */
const thunk = store => next => action =>
  typeof action === 'function' ?
    action(store.dispatch, store.getState) :
    next(action)

// 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。)
let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  applyMiddleware(
    rafScheduler,
    timeoutScheduler,
    thunk,
    vanillaPromise,
    readyStatePromise,
    logger,
    crashReporter
  )
)