loading...

1

深入浅出react和redux个人阅读记录

react 阅读书籍读完大概需要74分钟

  • 发布时间:2018-07-17 20:10 星期二
  • 刘伟波
  • 337
  • 更新于2018-09-11 22:24 星期二

关于阅读书籍部分,是个人在本书籍中收集的精华部分和实战部分,为了后续再次阅读节省时间
和方便在工作中的应用,后续会分享本书籍的电子版pdf在线下载。不过我还是建议读着去阅读原著。

--

http://images.liuweibo.cn/image/common/react_redux_1536675813594_227729_1536675825489.png

阅读深入浅出react和redux本书值得记录的地方

github源码:https://github.com/mocheng/react-and-redux

第二章 设计高质量的 React 组件

1 React prop

propTypes 检查

import PropTypes from 'prop-types';

Counter.propTypes = {
caption: PropTypes.string.isRequired,
initValue: PropTypes.number 
}

prop Types 虽然能够在开发阶段发现代码中的问题,但是放在产品环境中就不大合
适,现有的 babel-react-optimize 就具有这个功能,可以通过 npm 安装,但
是应该确保只在发布产品代码时使用它。

props默认值 React的 defaultProps 功能,让代码更加容易读懂
Counter 组件添加 defaultProps 的代码如下:

Counter .defaultProps = {
initValue: 0
}

2 组件的生命周期

装载过程

constructor

1 初始化 state ,因为组件生命周期中任何函数都可能要访问 state ,那么整个生命
周期中第一个被调用的构造函数自然是初始化 state 最理想的地方;
2 绑定成员函数的 this 环境

getlnitialState 和getDefaultProps

ES5的 React. createClass 方法创造的组件类才会发生作用,已经被 Facebook 官方逐渐废弃

render

render 函数应该是一个纯函数,完全根据 this.state this.props 来决定返
回的结果,而且不要产生任何副作用。在 render 函数中去调用 this.setState 毫无疑问是错
误的,因为一个纯函数不应该引起状态的改变

componentWillMount 和componentDidMount

1 在装载过程中, componentWil!Mount 会在调用 render 函数之前被调用, componentDidMount
会在调用 render 函数之后被调用,这两个函数就像是 render 函数的前哨和后
卫,一前一后,把 render 函数夹住,正好分别做 render 前后必要的工作

2 componentWillMount 都是紧贴着自己组件的 render 函数之
前被调用, componentDidMount 可不是紧跟着 render 函数被调用,当所有三个组件的
render 函数都被调用之后, 个组件的 componentDidMount 才连在一起被调用
之所以会有上面的现象,是因为 render 函数本身并不往 DOM 树上渲染或者装载内
容,它只是返回 JSX 表示的对象,然后由 React 库来根据返回对象决定如何渲染
React 库肯定是要把所有组件返回的结果综合起来,才能知道该如何产生对应的 DOM
修改 所以,只有 React 库调用 Counter 组件的 render 函数之后,才有可能完成装
载,这时候才会依次调用各个组件的 componentDidMount 函数作为装载过程的收尾

3 componentWilIMount componentDidMount 这对兄弟函数还有一个区别,就是 componentWillMount
可以在服务器端被调用,也可以在浏览器端被调用;而 component-DidMount
只能在浏览器端被调用,在服务器端使用 React 的时候不会被调用

更新过程

componentWillReceiveProps

1.只要是父组件的 render 函数被调用,在 render 函数里面被谊染的子组件就会经历更新过
程,不管父组件传给子组件的 props 有没有改变,都会触发子组件的 componentWillReceiveProps
函数
2、注意,通过 this.setState 方法触发的更新过程不会调用这个函数,这是因为这个函数
适合根据新的 props 值(也就是参数 nextProps )来计算出是不是要更新内部状态 state
更新组件内部状态的方法就是 this.setState ,如果 this.setState 的调用导致 componentWillReceiveProps再一次被调用,那就是一个死循环了
3、this.setState 不会引发这个函数 componentWillReceiveProps
被调用
4、在 React 的组件组合中,完全可以只渲染 个子组件,
而其他组件完全不需要渲染,这是提高 React 性能的重要方式

sholdComponentUpdate(nextProps, nextState)

1、render 函数重要,是因为 render 函数决定了该渲染什么,而说 shouldComponentUpdate
函数重要,是因为它决定了一个组件什么时候不需要渲染
2、说shouldComponentUpdate 重要,就是因为只要使用恰当,他就能够大大提高 React
组件的性能,虽然 React 的渲染性能已经很不错了,但是,不管渲染有多快,如果发现
没必要重新渲染,那就干脆不用渲染好了,速度会更快

 shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) ||
           (nextState.count !== this.state.count);
  }

现在,只有当 caption 改变,或者 state 中的 count 值改变, shouldComponent 才会返回
true
3.通过 this setState 函数引发更新过程,并不是立刻更新组件的 state
值,在执行到到函数 shouldComponentUpdate 的时候, this state 依然是 this.setState 函数
执行之前的值,所以我们要做的实际上就是在 nextProps nextState this.props this.state 中互相比对

componentWillUpdate和componentDidUpdate

1.如果组件的 shouldComponentUpdate 函数返回 true
2、当在服务器端使用 React 渲染时,这一对函数中的 Did 函数,
也就是 componentDidUpdate 函数,并不是只在浏览器端才执行的,无论更新过程发生在
服务器端还是浏览器端,该函数都会被调用
3.React 组件被更新时,原有的内容被重新绘
制,这时候就需要在 componentDidUpdate 函数再次调用 jQuery 代码
4.读者可能会问, componentDidUpdate 函数不是可能会在服务器端也被执行吗?在
服务器端怎么能够使用 jQuery 呢?实际上,使用 React 做服务器端渲染时,基本不会经
历更新过程,因为服务器端只需要产出 HTML 字符串,一个装载过程就足够产出 HTML
了,所以正常情况下服务器端不会调用 componentDidUpdate 函数,如果调用了,说明我
们的程序有错误,需要改进

卸载过程

componentWillU nmount

1、React 组件要从
DOM 树上删除掉之前,对应的 componentWillUnmount 函数会被调用,所以这个函数适
合做一些清理性的工作
2、不过, componentWillUnmount 中的工作往往和 componentDidMount 有关,比如,在
componentDidMount 中用非 React 的方法创造了一些 DOM 元素,如果撒手不管可能会造
成内存泄露,那就需要在 componentWillUnmount 中把这些创造的 DOM 元素清理掉

3 组件向外传递数据

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  initValue: PropTypes.number,
  onUpdate: PropTypes.func
};

Counter.defaultProps = {
  initValue: 0,
  onUpdate: f => f //默认这个函数什么也不做
};

新增加的 prop 叫做 onUpdate ,类型是一个函数,当 Counter 的状态改变的时候,就
会调用这个给定的函数,从而达到通知父组件的作用
这样, Counter的 onUpdate 就成了作为子组件的 Counter 向父组件 ControlPanel
递数据的渠道,我们先约定这个函数的第一个参数是 Counter 更新之后的新值,第二个
参数是更新之前的值,至于如何使用这两个参数的值,是父组件 ControlPanel 的逻辑,
Counter 不用操心,而且根据两个参数的值足够可以推导出数值是增加还是减少

Flux的缺陷

Store 之间依赖关系

1.Flux 的体系中,如果两个 Store 之间有逻辑依赖关系,就必须用上 Dispatcher的
waitFor 函数 在上面的例子中我们已经使用过 waitFor 函数, SummaryStore对 action 类型的
处理,依赖于 CounterStore 已经处理过了 所以,必须要通过 waitFor 函数告诉 Dispatcher,
先让 CounterStore 处理这些 action 对象,只有 CounterStore 搞定之后 SummaryStore才
继续
2.那么, SummaryStore 如何标识 CounterStore 呢?靠的是 register 函数的返回值 dispatchToken
,而 dispatchToken 的产生,当然是 CounterStore 控制的,换句话说,要这样设计:
1)CounterStore 必须要把注册回调函数时产生的 dispatchToken 公之于众;
2)SummaryStore 必须要在代码里建立对 CounterStore的 dispatchToken 的依赖
虽然 Flux 这个设计的确解决了 Store 之间的依赖关系,但是,这样明显的模块之间
的依赖,看着还是让人感觉不大舒服,毕竟,最好的依赖管理是根本不让依赖产生

难以进行服务器端渲染

3.2 Redux

3.2.1 Redux 的基本原则

1. 唯一数据源

1).如果状态数据分散在多个 Store 中,容易造成数据冗余,这样数据一致性方面就会出
问题。 虽然利用 Dispatcher的 waitFor 方法可以保证多个 Store 之间的更新顺序,但是这
又产生了不同 Store 之间的显示依赖关系,这种依赖关系的存在增加了应用的复杂度,容
易带来新的问题
Redux 对这个问题的解决方法就是,整个应用只保持一个 Store ,所有组件的数据源
就是这个 Store 上的状态
2)Redux阻并没有阻止一个应用拥有多个Store,只是,在Redux的框架下,让一个应
用拥有多个 Store 不会带来任何好处,最后还不如使用一个 Store 更容易组织代码

2. 保持状态只读( State is read-only);

修改 Store 的状态,必须要通过派发一个
action 对象完成,这一点 ,和 Flux 的要求并没有什么区别

3. 数据改变只能通过纯函数完成(Changes are made with pure functions)

这里所说的纯函数就是 Reducer
在 Redux 中,一个实现同样功能的 reducer 代码
如下:

function reducer(state , action) => {
    const {counterCaption} = action;
    switch (act on.type) {
        case ActionTypes.INCREMENT :
            return { ... state , [ counterCaption] : state [ counterCaption ] + 1};
        case ActionTypes . DECREMENT:
            return { ... state, [counterCaption] : state [ counterCaption) - 1};
        default :
            return state 
    }
}

可以看到 reducer 函数不光接受 action 为参数,还接受 state 为参数 也就是说, Redux的
reducer 只负责计算状态,却并不负责存储状态

“如果你愿意限制做事方式的灵活度,你几乎总会发现可以做得更好。”
一一-John earmark

作为制作出《 Doom >< Quake 》这样游戏的杰出开发者, John earmark 这句话道出
了软件开发中的一个真谛
在计算机编程的世界里,完成任何一件任务,可能都有一百种以上的方法,但是无节制的灵活度反而让软件难以维护增加限制是提高软件质量的法门。

3.2.2 Redux 实例

创造一个 src/Store 文件,这个文件输出全局唯一的那个 Store

import {createStore} from 'redux';
import reducer from './Reducer.js';
const initValues = {
  'First': 0,
  'Second': 10,
  'Third': 20
};

const store = createStore(reducer, initValues);

在这里,我们接触到了 Redux 库提供的 create Store 函数,这个函数第一个参数代表更
新状态的 reducer ,第二个参数是状态的初始值,第三个参数可选,代表 Store Enhancer,
在这个简单例子中用不上,在后面的章节中会详细介绍

reducer文件

import * as ActionTypes from './ActionTypes';

export default (state,action)=>{
  const {counterCaption} = action;

  switch (action.type){
    case ActionTypes.INCREMENT:
      return {
        ...state,
        [counterCaption]:state[counterCaption]+1
      }
    case ActionTypes.DECREMENT:
      return {
        ...state,
        [counterCaption]:state[counterCaption]-1
      }
    default:
      return state
  }
};

扩展操作符 (spread operator) 并不是因桦的一部分,甚至都不是 ES Next 刁飞 法的一部分,但是
因为其语法简单,已经被广泛使用,因为 babel 的存在,也不 会有兼容性问题,所以我们完全可以放心使用

3.2.3 窑器组件和傻瓜组件

1.傻瓜组件 Counter 代码的逻辑前所未有的简单,只有一个 render 函数
1.CounterContainer ,这是容器组件,组件承担了所有的和 Store 关联的工作,它的 render 函数所做的就是渲染傻瓜组件 Counter 而已,只负责传递必要的 prop

3.2.4 组件 Context

1.Provider 也是一个 React 组件,不过它的 render 函数就是简单地把子组件渲染出来,
在渲染上, Provider 不做任何附加的事情

import {PropTypes, Component} from 'react';

class Provider extends Component {

  getChildContext() {
    return {
      store: this.props.store
    };
  }

  render() {
    return this.props.children;
  }

}

Provider.propTypes = {
  store: PropTypes.object.isRequired
}

Provider.childContextTypes = {
  store: PropTypes.object
};

export default Provider;
 <Provider store={store}>
    <ControlPanel />
  </Provider>,

3.2.5 React-Redux

1. connect :连接容器组件和傻瓜组件;

以Counter 组件为例,react-redux 的例子中没
有定义 CounterContainer 这样命名的容器组件,而是直接导出了一个这样的语句

export default connect(mapStateToProps, mapDispatchToProps), Counter);

1)第一眼看去,会让人觉得这不是正常的 JavaScript 语法 其实, connect是 react-redux
提供的一个方法,这个方法接收两个参数 mapStateToProps和 mapDispatch-ToProps ,执行
结果依然是一个函数,所以才可以在后面又加一个圆括号,把 connect 函数执行的结果立
刻执行,这一次参数是 Counter 这个傻瓜组件。
2)这里有两次函数执行,第一次是 connect 函数的执行,第二次是把 connect 函数返回
的函数再次执行,最后产生的就是容器组件,功能相当于前面 redux_smart_dumb 中的
CounterContainer;
3)这个 connect 函数具体做了什么工作呢?

  • 把 Store 上的状态转化为内层傻瓜组件的 prop;

  • 把内层傻瓜组件中的用户动作转化为派送给 Store 的动作
    4)mapStateToProps函数

    function mapStateToProps(state ,ownProps) {

     return {
         value: state[ownProps.caption] 
         }

    }
    5)mapDispatchToProps函数

    function mapDispatchToProps(dispatch, ownProps) {
     return {
             nincrement () => {
                 dispatch(Actions.increment(ownProps.caption));
             }
             onDecrement : () => {
                 dispatch(Actions.decrement(ownProps.caption)); 
             }
           }
    }

    6)mapStateToProps和 mapDispatchToProps 都可以包含第二个参数,代表 ownProps,
    也就是直接传递给外层容器组件的 props ,在 ControlPanel 的例子中没有用到,我们在后
    续章节中会有详细介绍

2.Provider :提供包含 store context

react-redux 和我们例子中的 Provider 几乎一样,但是更加严谨,比如我们只要求 store 属性是一个 object ,而react-redux 要求 store 不光是 object ,而且是必须包含三个函数的 object ,这三个函数
分别是

  • subscribe
  • dispatch
  • getState

拥有上述 3个函数的对象,才能称之为一个 Redux 的store;
另外, react-redux 定义了 Provider的 componentWillReceiveProps 函数,在 React组
件的生命周期中, componentWillReceiveProps 函数在每次重新渲染时都会调用到, react-redux在
componentWillReceiveProps 函数中会检查这一次渲染时代表 store的 prop 和上
一次的是否一样。 如果不一样,就会给出警告,这样做是为了避免多次渲染用了不同的
Redux Store。 每个 Redux 应用只能有一个 Redux Store ,在整个 Redux 的生命周期中都
应该保持 Store 的唯一性

模块化 React 末日 Redux 应用

4.1 模块化应用要点

reducers/
    todoReducer. js
    filterReducer.js
actions/
    todoActions.js
    filterActions.js
components/
    doList js
    todoitern . js
    filter.js
containers/
    todoListContainer . js
    todoiternCont ainer . js
    filterContaine r. js 

4.2.2 接功能组织

  • actionTypes.js 定义 action 类型;
  • actions. js定义 action 构造函数,决定了这个功能模块可以接受的动作;
  • reducer扣定义这个功能模块如何相应 actions. 中定义的动作;
  • views 目录,包含这个功能模块中所有的 React 组件,包括傻瓜组件和容器组件;
  • index.js 这个文件把所有的角色导人,然后统一导出

4.3 模块接口

“在最理想的情况下,我们应该通过增加代码就能增加系统的功能,而不是 通过对现有代码的修改来增加功能"

一一-Robert C. Martin

4.4 状态树的设计

4.4.1 一个状态节点只属于一个模块

比如,如果 模块的 reducer 负责修改状态树上 字段下的数据,那么另 个模块
reducer 就不可能有机会修改 字段下的数据

4.4.2 避免冗余数据

4.4.3 树形结构扁平

4.5 Todo 应用实例

4.5.1 Todo 状态设计

4.6 开发辅助工具

工欲善其事,必先利其器 一一《论语·卫灵公》

第五章 React 组件的性能优化

5.2 多个 React 组件的性能优化

5.2.1 React 的调和(Reconciliation )过程

1.节点类型不同的情况

举个例子 在更新之前,组件的结构是这样:

    <div>
        <Todos />
    </div>

我们想要更新成这样:

    <span>
        <Todos />
    </span>

这时候, componentWillUnmount 方法会被调用,取而代之的组件则会经历装载过程
的生命周期,组件的 componentWillMount render componentDidMount 方法依次被
调用,一看根节点原来是 div ,新的根节点是 span ,类型就不一样,切推倒重
来。
虽然是浪费,但是为了避免 O(N3)的时间复杂度, React 必须要选择 个更简单更快
捷的算法,也就只能采用这种方式
作为开发者,很显然一定要避免上面这样浪费的情景出现 所以, 一定要避免作为
包裹功能的节点类型被随意改变

2.节点类型相同的情况

比如原本的节点用 JSX 表示是这样:

    <div style={{color :”red”, fontSize: 15}} className=” welcome ” >
        Hello World
    </div>

改变之后的 JSX 表示是这样:

    <div style={{color :” gree ”, fontSize: 15}} className=” farewell ” >
        Good Bye
    </div>

React 能做的只是根据新节点的 props 去更新原来根节点的组件实例,
引发这个组件实例的更新过程,也就是按照顺序引发下列函数:

  • shouldComponentUpdate
  • componentWillReceiveProps
  • componentWillUpdate
  • render
  • componentDidUpdate

如果 shouldComponentUpdate 函数返回 false 的话,那么更新过程
就此打住,不再继续 所以为了保持最大的性能,每个 React 组件类必须要重视 shouldComponentUpdate
,如果发现根本没有必要重新渲染,那就可以直接返回 false

3 .多个子组件的情况

    <ul>
        <Todoitem text= Firstcompleted={ false}>
        <To do tern text Second completed={false}>
    </ul>

在更新之后,用 JSX 表示是这样:

     <ul>
        <Todo item text=” First ” completed={ false}>
        <Todoitem text=” Second" completed={false}>
        <Todo tem text=”Third” completed={false}>
    </ul> 

那么 React 会发现多出了一个 Todoltem ,会创建一个新的 Todoltem 组件实例,这个
Todoltem 组件实例需要经历装载过程,对于前两个 Todoltem 实例, React 会引发它们的
更新过程,但是只要 To do Item shouldComponentUpdate 函数实现恰当,检查 props
后就返回 false 的话,就可以避免实质的更新操作

 <ul>
    <Todoltem text=” Zero” completed={ false)>
    <Todoltem text=”First” completed={ false)>
    <Todoltem text=”Second” completed={ false)>
 </ul> 

从直观上看,内容是“ Zero ,,的新加待办事项被插在了第一位,只需要创造一个新
的组件 Todoltem 实例放在第一位,剩下两个内容为“ First ”和“ Second ,,的 Todoltem
实例经历更新过程,但是因为 props 没有改变,所以 shouldComponentUpdate 可以帮助
这两个组件不做实质的更新动作。
可是实际情况并不是这样 如果要让 React 按照上面我们构想的方式来做,就必须
要找出两个子组件序列的不同之处,现有的计算出两个序列差异的算法时间是 O(N2),虽
然没有树形结构比较的 O(N3)时间复杂度那么夸张,但是也不适合一个对性能要求很高
的场景,所以 React 选择看起来很傻的一个办法,不是寻找两个序列的精确差别,而是
直接挨个比较每个子组件。
, React 并不是没有意识到这个问题,所以 React 提供了方法来克服这种浪费,
不过需要开发人员在写代码的时候提供一点小小的帮助,这就是 key 的作用

5.2.2 Key 的用法

用数组下标作为 key ,看起来 key 值是唯一的,但是却不是稳定不变的,随着 todos
数组值的不同,同样一个 Todoltem 实例在不同的更新过程中在数组中的下标完全可能不
同,把下标当做 key 就让 React 彻底乱套了。
需要注意,虽然 key 是一个 prop ,但是接受 key 的组件并不能读取到 key 的值,因
key 和ref是 React 保留的两个特殊 prop ,并没有预期让组件直接访问。

5.3 用reselect 提高数据获取性能

5.3.1 两阶段选择过程

const selectVisibleTodos = (todos, filter) => {
  switch (filter) {
    case FilterTypes.ALL:
      return todos;
    case FilterTypes.COMPLETED:
      return todos.filter(item => item.completed);
    case FilterTypes.UNCOMPLETED:
      return todos.filter(item => !item.completed);
    default:
      throw new Error('unsupported filter');
  }
}

const mapStateToProps = (state) => {
  return {
    todos: selectVisibleTodos(state.todos, state.filter)
  };
}

既然这个 selectVisibleTodos 函数的计算必不可少,那如何优化呢?
如果上 次的计算结果被缓存起来的话,那就可以重用缓存的
数据。
这就是 reselect 库的工作原理:只要相关状态没有改变,那就直接使用上一次的缓存
结果

npm install --save reselect 


 import {createSelector} from ’ reselect ’;
 import {FilterTypes} from ’.. /constants. j s ’;
    export const selectVisibleTodos = createSelector(
        [getFilter, getTodos],
        (filter, todos) => {
            switch (filter) {
                case FilterTypes.ALL:
                    return todos;
                case FilterTypes.COMPLETED:
                    return todos.filter(item =>item.completed};
                case FilterTypes.UNCOMPPLETED:
                    return todos.filter(item => !item.completed);
                default:
                    throw new Error (’ unsupported filter ’);  
                            }
                           }
     )

reselect 提供了创造选择器的 createSelector 函数 ,这是一个高阶函数,也就是接受
函数为参数来产生一个新函数的函数
第一个参数是一个函数数组,每个元素代表了选择器步骤一需要做的映射计算,这
里我们提供了两个函数 getFilte和 getTodos ,对应代码如下:

const getFilter = (state) => state.filter;
const getTodos = (state) => state.todos; 

5.3.2 范式化状态树

所谓范式化,就是遵照关系型数据库的设计原则,减少冗余数据.
如果使用反范式化的设计,那么状态树上的数据最好是能够不用计算拿来就能用,
在Redux Store 状态树的 todos 字段保存的是所有待办事项数据的数组,对于每个数组元
素,反范式化的设计会是类似下面的对象:

{
    id: 1, //待办事项id
    text :”待办事项 ”,//待办事项文字内容
    completed : false,//是否已完成
    type: { //种类
        name :”紧急”,//种类的名称
        color:”red” //种类的显示颜色
    }
}

但这也有缺点,当需要改变某种类型的名称和颜色时,
不得不遍历所有 Todoltem 数据来完成改变.
反范式化数据结构的特点就是读取容易,修改比较麻烦

如果使用范式化的数据结构设计,那么 Redux Store 上代表 Todoltem 的一条数据是
类似下面的对象:

{
    id: 1 ,
    text :”待办事项 l ”,
    completed : false ,
    typeid: 1 //待办事项所属的种类id
}

用一个typeId 代表类型,然后在 Redux Store 上和 to dos 平级的根节点位置创建一个
types 字段,内容是一 个数组,每个数组元素代表 一个类型,一个种类的数据是类似下面
的对象:

{
    id: 1 , //种类
    name :” 紧急 ”, //种类的名称
    color :”red” //种类的显示颜
}

当Todoltem 组件要渲染内容时从 Redux Store 状态树的 to dos 宇段下获取的数据
是不够的,因为只有 typeId。
这个过程当然要花费一 些时间,但是当要改变某个种类的名称或者颜色时,就异常
地简单,只需要修改 types 中的一处数据就可以了

5.4 本章小结

1.利用 react-redux 提供的 shouldComponentUpdate 实现来提高
组件渲染功能的方法, 一个要诀就是避免传递给其他组件的 prop 值是 一个不同的对象,
不然会造成元谓的重复渲染
2.不能随意修改一个作为容器的 HTML 节点的类型 其次,对于动态数
量的同类型子组件,一 定要使用 key 这个 prop
3.利用 reselect 库来实现高效的数据获取。 因为 reselect 的缓存功
能,开发者不用顾忌范式化的状态树会存在性能问题, Redux Store 的状态树应该按照范
式化原则来设计,减少数据冗余,这样利于保持数据一致。

第六章 React 高级组件

“重复是优秀系统设计的大敌。 ”一-Robert C.Martin

6.1 高阶组件

1.高阶组件( Higher Order Component, HOC )并不是 React 提供的某种 API ,而是使用
React 的一种模式,用于增强现有组件的功能。
2.简单来说,一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的组件作为结果,而且,返回的新组件拥有了输入组件所不具有的功能
3.这里提到的组件指的并不是组件实例,而是一个组件类,也可以是一个无状态组件
的函数。

4.我们先看一个非常简单的高阶组件的例子,感受一下高阶组件是如何工作的,代码
如下:

import React from ’ react ’;
function removeUserProp(WrappedComponent) {
    return class WrappingComponent extends React.Component {
         render() {
            const {user, ... otherProps} = this.props;
            return <WrappedComponent { ... otherProps) />
                }
    }
}
export default removeUserProp; 

只是忽略名为 user的 prop 也就是说,如果 Wrapped Component 能够处理名为 user的
prop ,这个高阶组件返回的组件则完全无视这个 prop。

5.假如我们现在不希望某个组件接收到 user prop ,那么我们就不要直接使用这个组
件,而是把这个组件作为参数传递给 removeU serProp 函数,然后我们把这个函数的返回
结果当做组件来使用:

const NewComponent = removeUserProp(SampleComponent) ; 

在上面的代码中, NewComponent 拥有和 SampleComponent 完全一样的行为,唯一
的区别就是即使传递 user 属性给它,它也会当没有 user 来处理。
6.定义高阶组件的意义何在呢?

  • 首先,重用代码
  • 其次,修改现有 React 组件的行为

6.1.1 代理方式的高阶组件

1.上面的 removeUserProp 例子就是一个代理方式的高阶组件,特点是返回的新组件类
直接继承自 React. Component 新组件扮演的角色是传入参数组件的一个“代理”,在
新组建的 render 函数中,把被包裹组件渲染出来,除了高阶组件自己要做的工作,其余
功能全都转手给了被包裹的组件.
2.如果高阶组件要做的功能不涉及除了 render 之外的生命周期函数,也不需要维护自
己的状态,那也可以干脆返回一个纯函数,像上面的 removeUserProp ,代码可以简写成
下面这样:

function removeUserProp(WrappedComponent) {
    return function newRender(props) {
        con st {user, ... otherProps) = props;
        return <WrappedComponent { ... otherProps} /> 
    }
}

3.代理方式的高阶组件,可以应用在下列场景中:

  • 操纵 prop;
  • 访问 ref;
  • 抽取状态;
  • 包装组件

6.1.2 继承方式的高阶组件

1. 操纵 Props

2. 操纵生命周期函数

例如,我们可以定义一个高阶组件,让参数组件只有在用户登录时才显示,代码
如下:

const onlyForLoggedinHOC = (WrappedComponent) => {
    return class NewComponent extends WrappedComponent {
        render () {
            if (this.props.loggedin) {
                return super.render();
            } else {
                return null; 
                    }
            }
     }
}

又例如,我们可以重新定义 shouldComponentUpdate 函数,只要 prop 中的 useCache
不为逻辑 false 就不做重新渲染的动作,代码如下:

const cacheHOC = (WrappedComponent) => {
    return class NewComponent extends WrappedComponent {
        shouldComponentUpdate(nextProps, nextState) {
        return !nextProps.useCache; 
        }
    }
}

6.1.3 高阶组件的显示名

6.1.4 曾经的 React Mixin

在 ES6的 React组件类定义方法中不能使用 Mixin, React 官方也很明确声明 Mixin 是应该被废弃的方法
所以我们只需要知道在 React 的历史上,曾经有这样一个重用代码的解决方法就足够了

6.2 以函数为子组件

6.2.1 实例 CountDown

6.2.2 性能优化问题

第七章 Redux 和服务器通信

7.2.1 redux-thunk 中间件

使用 Redux 访问服务器,同样要解决的是异步问题
Redux 的单向数据流是同步操作,驱动 Redux 流程的 ac tion 对象, 每一个 action
对象被派发到 Store 上之后,同步地被分配给所有的 reducer 函数,每个 reducer 都是纯
函数,纯函数不产生任何副作用,自然是完成数据操作之后立刻同步返回, reducer 返回
的结果又被同步地拿去更新 Store 上的状态数据,更新状态数据的操作会立刻被同步给监
Store 状态改变的函数,从而引发作为视图的 React 组件更新过程。

实际上, re dux-thunk 的实现极其简单,只有几行代码。
假如有一个 JavaScript 函数f 如下定义:

const f = (x) => {
    return x () + 5; 
}

f把输入参数x 当做一个子程序来执行,结果加上5 就是f 的执行结果,那么我们试
着调用一次 f:

const g = () => {
    return 3 + 4 ;
}
f (g); 11 结果是( 3+4 )巧= 37 

上面代码中函数f 就是一个 thunk ,这样使用看起来有点奇怪,但有个好处就是g的
执行只有在f 实际执行时才执行,可以起到延迟执行的作用,我们继续看 redux-thunk的
用法来理解其意义。

按照 redux-thunk 的想法,在 Redux 的单向数据流中,在 action 对象被 reducer 函数
处理之前,是插入异步功能的时机

在Redux 架构下,一个 action 对象在通过 store.dispatch派发,在调用 reducer 函数
之前,会先经过 个中间件的环节,这就是产生异步操作的机会,实际上 redux-thunk提
供的就是一个Redux 中间件,我们需要在创建 Store 时用上这个中间件。

7.2.2 异步 action 对象

redux-也unk 的工作是检查 action 对象是不是函数,如果不是函数就放行,完成普通
action 对象的生命周期,而如果发现 action 对象是函数,那就执行这个函数,并把 Store的
dispatch 函数和 getState 函数作为参数传递到函数中去,处理过程到此为止,不会让
这个异步 action 对象继续往前派发到 reducer 函数

举一个并不涉及网络 API 访问的异步操作例子 ,在 Co unter 组件中存在一个普通的
同步增加计数的 action 构造函数 increment ,代码如下:

const increment= () => ({
    type: ActionTypes.INCREMENT,
}); 

派发 increment 执行返回的 action 对象, Redux 会同步更新 Store 状态和视图,但是
我们现在想要创造一个功能,能够发出一个“让 Counter 组件在 秒之后计数加一”的
指令,这就需要定义一个新的异步 action 构造函数,代码如下:

const incrementAsync = () => {
    return (dispatch) => {
        set Timeout ( () => {
            dispatch (increment());
        },1000); 
    }
}

异步 action 构造函数 incrementAsync 返回的是一个新的函数,这样一 个函数被
dispatch 函数派发之后,会被 redux-thunk 中间件执行,于是 setTimeout 函数就会发生作
用,在 1秒之后利用参数 dispatch 函数派发出同步 action 构造函数 increment 的结果。

这就是异步 action 的工作机理,这个例子虽然简单,但是可以看得出来,异步
action 最终还是要产生同步 action 派发才能对 Redux 系统产生影响。

7.2.3 异步操作的模式

7.2.4 异步操作的中止

对于访问服务器这样的异步操作,从发起操作到操作结束,都会有段时间延迟,在
这段延迟时间中,用户可能希望中止异步操作。
用户也会进行一些操作引发新的请求发往服务器,而这就是我们开发者需要考虑的问题。

从用户角度出发希望是最后一次选择结果。
在jQuery 中,可以通过 abort 方法取消掉一个 AJAX 请求:

const xhr = $.ajax( ... );
xhr.abort {);//取消掉已经发出的AJAX请求

但是,很不幸,对于 fetch 没有对应 abort 函数的功能,因为 fetch 返回的是一个
Promise 对象,在 ES6 的标准中, Promise 对象是不存在“中断”这样的概念的.

既然 fetch 不能帮助我们中止一个 API 请求,那就只能在应用层实现“中断”的效
果,有一个技巧可以解决这个问题,只需要修改 action 构造函数。

let nextSeqid = 0;
export const fetchWeather = (cityCode) => {
    return (dispatch) => {
        const apiUrl =、/ data/cityinfo/${cityCode).html
        const seqid = ++ nextSeqid;
        const dispatchifValid = (action) => {
            if (seqid === nextSeqid) { //**这里一个请求对应一个请求`id`如果不相等,就抛弃**
                return dispatch(action);
            }
        }
        dispatchifValid ( fetchWeatherStarted () )
        fetch(apiUrl) .then((response) => {
            if (response.status !== 200) {
                throw new Erro r (’ Fail to get response with status ’+ response.status);
            }
            response.json() .then((responseJson) => {
                dispatchifValid(fetchWeatherSuccess(responseJson.weatherinfo));
            )).catch((error) => {
                dispatchifVal (fetchWeatherFailure(error));
            ));
        }).catch ((error) => {
            dispatchifValid(fetchWeatherFailure(error)); 
        })
   }
}

在action 构造函数文件中定义一个文件模块级的 nextSeqld 变量,这是一个递增的整
数数字,给每一个访问 API 的请求做序列编号

这里一个请求对应一个请求id如果不相等,就抛弃。

如果还不明白,另外用vue的例子来说明

<div id="app">
    <select @change="_getWeather">
        <option v-for="(value, key) in city_code" >{{key}}</option>
    </select>
    <div>{{cont}}</div>
</div>
<script>
    new Vue({
      el:'#app',
      data:{
        nextSeqid:0,
        baseUrl:'http://www.weather.com.cn',
        city_code: {
          '北京': 101010100,
          '上海': 101020100,
          '广州': 101280101,
          '深圳': 101280601
        },
        cont:'正在请求。。。'
      },
      methods:{
        _getWeather(e){
          const seqid = ++ this.nextSeqid;
          console.log(seqid,this.nextSeqid,'请求')
          let url=`/data/cityinfo/${this.city_code[e.target.value]}.html`;
            axios.get(url).then(res=>{
              let {city}=res.data.weatherinfo;

          console.log(seqid,this.nextSeqid,'请求完成')
              if (seqid === this.nextSeqid) {
                this.cont=city;
                }
            })
        }
      }
    })
</script>

控制台结果:

- 1 1 "请求"
- (index):55 2 2 "请求"
- (index):60 1 2 "请求完成"
- (index):55 3 3 "请求"
- (index):60 2 3 "请求完成"
- (index):55 4 4 "请求"
- (index):60 3 4 "请求完成"
- (index):55 5 5 "请求"
- (index):60 4 5 "请求完成"
- (index):55 6 6 "请求"
- (index):60 5 6 "请求完成"
- (index):55 7 7 "请求"
- (index):60 6 7 "请求完成"
- (index):60 7 7 "请求完成"

你会发现在重复请求,请求id不对应,所以不渲染,只有当相等菜渲染。

if (seqid === this.nextSeqid) {
   this.cont=city;
}

虽然不能真正“中止”一个 API 请求,但是我们可以用这种方法让一个 API 请求的
结果被忽略,达到了中止一个 API 请求一样的效果。

在这个例子中 Weather 模块只有一种API 请求,所以一个 API 调用编号序列就足够,
如果需要多种 API 请求,则需要更多类似nextSeqld 的变量来存储调用编号。

7.3 Redux 异步操作的其他方法

  • redux-saga
  • redux-effects
  • redux-side-effects
  • redux-loop
  • redux-observable

第八章 单元测试

第九章 扩展 Redux

9.1 中间件

  • 中间件的特点是:
  • 中间件是独立的函数;
  • 中间件可以组合使用;
  • 中间件有一个统一的接口

第十章 动画

10.1.1 css方式

运行效率要比脚本方式高,因为浏览器原生支持,省去了 Java
Script 的解释执行负担,有的浏览器(比如 Chrome 浏览器)甚至还可以充分利用 GPU加
速的优势,进一步增强了动画渲染的性能

时间和速度曲线的不合理是 CSS3 先天的属性更让开发者头疼的就是开发 CSS3
则的过程,尤其是对 tra nsition-duration 时间很短的动画调试,因为 CSS3 transition
程总是一闪而过,捕捉不到中间状态,只能一遍一遍用肉眼去检验动画效果,用 CSS3
做过复杂动画的开发者肯定都深有体会

虽然 CSS3 有这样一些缺点,但是因为其无与伦比的性能,用来处理一些简单的动
画还是不错的选择

React 提供的 ReactCSSTransitionGroup 功能,使用的就是 CSS3 的方式来实现动画,
在后面的章节会详细介绍

10.1.2 脚本方式

脚本方式最大的好处就是更强的灵活度,最原始的脚本方式就是利用 setlnterval 或者 setTimeout 来实现。

 var animatedElement = document.getElementById ('sample');
  var left = 0;
  var timer;
  var ANIMATIONINTERVAL = 16;
  timer = setInterval (function() {
    left += 10;
    animatedElement.style.left = left + 'px';
    if ( left >= 400 ) {
      clearInterval(timer);
    }
  } , ANIMATIONINTERVAL);

在上面的例子中,有一个常量 ANIMATION INTERVAL 定义为 16 , setlnterval 以这
个常盘为间隔,每 16 毫秒计算一次 sample 元素的 left 值,每次都根据时间推移按比例增加 left 的值,直到 left 大于 400.
为什么要选择 16 毫秒呢?因为每秒渲染 60 帧(也叫 60fps, 60 Frame Per Second)
会给用户带来足够流畅的视觉体验,一秒钟有 1000 毫秒, 1000/60约等于16 ,也就是说,如
果我们做到每 16 毫秒去渲染一次画面,就能够达到比较流畅的动画效果。

对于简单的动画, setlnterval 方式勉强能够及格,但是对于稍微复杂一些的动画,脚
本方式就顶不住了,比如渲染一帧要花去超过 32 毫秒的时间,那么还用 16 毫秒一个间
隔的方式肯定不行 实际上,因为一帧渲染要占用网页线程 32 毫秒,会导致 setlnterval
根本无法以 16 毫秒间隔调用渲染函数,这就产生了明显的动画滞后感,原本一秒钟完
成的动画现在要花两秒钟完成,所以这种原始的 setlnterval 方式是肯定不适合复杂的动
画的。

出现上面问题的本质原因是 setlnterval和setTimeout 并不能保证在指定时间间隔或
者延迟的情况下准时调用指定函数 所以可以换 个思路,当指定函数调用的时候,根
据逝去的时间计算当前这一帧应该显示成什么样子,这样即使因为浏览器渲染主线程忙
碌导致一帧渲染时间超过 16 毫秒,在后续帧谊染时至少内容不会因此滞后,即使达不倒
60fps 的效果,也能保证动画在指定时间内完成。

下面是一个这种方法实现动画的例子,首先我们实现一个 raf 函数, raf request
animation frame 的缩写,代码如下:

var lastTmeStamp = new Date().getTime();
  function raf(fn) {
    var currTimeStamp = new Date().getTime();
    var delay = Math.max(O, 16 - (currTimeStamp - lastTmeStamp));
    var handle = setTimeout(function(){
      fn(currTimeStamp)
    },delay);
    lastTmeStamp = currTimeStamp;
  return handle;
  }

在上面定义的 raf 中,接受的 fn 函数参数是真正的渲染过程, raf 只是协调渲染的节奏。

raf 尽量以每隔 16 毫秒的速度去调用传染的fn参数,如果发现上一次被调用时间和
这一次被调用时间相差不足 16 毫秒,就会保持 16 毫秒一次的渲染间隔继续,如果发现
两次调用时间间隔已经超出了 16 毫秒,就会在下 次时钟周期立刻调用 fn。

还是让 id 为sample 的元素向右移动的例子,我们定义渲染每一帧的函数 render ,代
码如下:

  var left = 0;
    var animatedElement = document.getElementById("sample");
    var startTimestamp = new Date().getTime();

    function render(timestamp) {
        left += (timestamp - startTimestamp) / 16;
        animatedElement.style.left = left + 'px';
        if (left < 400) {
            raf(render);
        }
    }

    raf(render);

上面的 render 函数中根据当前时间和开始动圆的时间差来计算 sample 元素的 left属
性,这样无论 render 函数何时被调用,总能够渲染出正确的结果。
最后,我们将 render 作为参数传递给 raf ,启动了动画过程:

raf (render); 

实际上, 现代浏览器提供了 一个新 的函数 requestAnimationFrame ,采用的就是
上面描述的思路,不是以固定 16 毫秒间隔的时间去调用渲染过程,而是让脚本通过
requestAnimationFrame 传一 个回调函数,表示想要渲染一帧画面,浏览器会决定在合
适的时间来调用给定的回调函数,而回调函数的工作是要根据逝去的时间来决定将界面
渲染成什么样子。

这样一来,渲染动面的方式就改成按需要来渲染,而不是每隔 16 毫秒渲染固定的帧内容。

不是所有浏览器都支持 requestAnimationFrame ,对于不支持这个函数的浏览器,可
以使用上面 raf 函数的方式模拟 requestAnimationFrame 的行为。

10.2 ReactCSSTransitionGroup

React 提供了一个叫做 ReactCSSTransitionGroup 的功能帮助实现动画,为了使用这
个功能 ,首先要通过 npm 安装 react-addons-css-transition-group 这个库,之后就可以导人
这个库的内容:

import TransitionGroup from ’ react-addons- css-transition-group ’; 

Transition Group 的工作就是帮助组件实现装载过程和卸载过程的动画,而对于更新
过程,并不是 Transition Group 要解决的问题.

10.2.1 Todo 应用动画

 <ul>
      <TransitionGroup transitionName="fade" transitionEnterTimeout={500} transitionLeaveTimeout={200}>
    {
      todos.map((item) => (
        <TodoItem
          key={item.id}
          id={item.id}
          text={item.text}
          completed={item.completed}
        />
        ))
    }
      </TransitionGroup>
    </ul>
.fade-enter{
  opacity: 0.01;
}

.fade-enter.fade-enter-active {
  opacity: 1;
  transition: opacity 500ms ease-in;
}

.fade-leave {
  opacity: 1;
}

.fade-leave.fade-leave-active {
  opacity: 0.01;
  transition: opacity 200ms ease-in;
}

10.2.2 ReactCSSTransitionGroup 规则

假设 transitionName sample ,那么定制相关 React 组件的类名就是:

  • sample-enter
  • sample-enter-active
  • sample-leave
  • sample-leave-active

装载时机
读者可能会有一个疑问,为什么用 TransitionGroup在 todoList.js 文件中包住所有
Todoltem 组件实例的数组,而不是让 TransitionGroup在 todoltem.js 文件中包住单个
Todoltem 组件呢?
看起来应该能实现同样效果,但实际上这样做不行 因为 TransitionGroup 要发挥作
用,必须自身已经完成装载了 这很好理解, Transition Group 也只是一个 React 组件,
功能只有在被装载之后才能发挥,它自己都没有被装载,怎么可能发挥效力呢?

10.3 React-Motion 动画库

react-motion 是很优秀的动画库,它采用的动
画方式和 TransitionGroup 不同,是用脚本的方式。

10.4 本章小结

在这一章中,我们了解了网页动画的两种实现方式, CSS3 方式和脚本方式,在 React
的世界,也有对应这两种方式的动画解决方案。

React 官方的 ReactCSSTransitionGroup ,能够帮助定制组件在装载过程和卸载过程
中的动画,对于更新过程的动画,则不在 ReactCSSTransitionGroup 考虑之列,可以直接用
CSS3 来实现。

React-Motion 库提供了更强大灵活的动画实现功能,利用“以函数为子组件”的模
式, React-Motion 只需要提供几个组件,这些组件通过定时向子组件提供动画参数,就
可以让开发者自由定义动画的功能。

第十一章 多页面应用

第十二章 同构

React Redux 都是完全在浏览器中运行的,其实, React作为一
个产生用户界面的 JavaScript 库, Redux 作为一个管理应用数据的框架,两者也可以
在服务器端运行。
理想情况下, 一个React 组件或者说功能组件既能够在浏览器端渲染也可以在服务
器端渲染产生 HTML ,这种方式叫做“同构”( Isomorphic ),也就是同 份代码可以在不
同环境下运行。

传统的模板库就是生硬的字符串替换操作,无论如何优化都会有它的极限,而且模
板的输出依然是字符串,将 HTML 字符串插入网页的过程,也就是 DOM 树的操作,性
能也无法优化 在前面的章节中我们介绍过 React的 Virtual DOM 工作原理,配合生命
周期函数的应用,性能不是字符串替换的模板库能够比拟的。

12.2 构建渲染动态内容服务器

虽然 Face book 声称 React 并不是给服务器端渲染设计的,但是 React 真的很适合来
做同构

你可能感兴趣的文章

    发表评论

    评论支持markdown,评论内容不能超过500字符,如果内容过多或者要及时回复,建议去 segmentfault平台, 也可以来我的直播间来提问。
    关于技术问题或者有啥不懂的都可以留言, 我会定期回复答疑, 也可以来 我的直播间 提问, 推荐最新仓库 前端知识体系, 感謝支持!