React前端框架

126 阅读24分钟

CSS in JS

github.com/MicheleBert…

CSSinJS是使用javascript编写css的统称,用来解决css样式冲突、覆盖等问题

CSSinJS的具体实现有50多种,比如:CSS Modules、style-components等

推荐使用:CSS Modules(React脚手架集成了,可直接使用)

CSS Modules

概述

  • CSS Modules通过对css类名重命名,保证每个类名唯一性,从而避免样式冲突的问题

  • 实现方式:webpack的css-loader插件

  • 命名采用:BEM(Block块,Element元素,Modifier三部分组成)命名规范,比如:.list_item_active

  • 在React脚手架中演化成:文件名、类名、hash三部分,只要指定类名即可

    [filename]_[classname]__[hash]
    
    //类名
    .error
    //生成的类名为
    .Button_error_ax7yz
    
    

基本使用

1.创建名为[name].module.css 的样式文件(React脚手架中的约定,与普通css作区分)

//在组建中创建文件名称
index.module.css

//样式内容
.test{
   padding:0;
}

2.组件中导入该样式文件(注意语法)

//在组件中导入该样式
import styles from './index.module.css'

3.通过styles对象访问对象中的样式名来设置样式

<div className={styles.test}></div>

注意事项

  1. 在样式文件中修改当前组件的样式(使用单个类名设置样式,不实用嵌套样式)。

  2. 命名推荐使用驼峰命名发

  3. 对于组件库中已经有的全局样式(比如:.am-navbar-title),需要使用 :global() 来指定。

    :global(.am-navbar-title){}
    .root :global(.am-navbar-title){}
    

React脚手架工具

脚手架工具

//全局安装
yarn global add create-react-app

//查看版本
create-react-app --version

环境变量

开发环境

#在根目录创建 .env.development 文件

REACT_APP_URL = http://192.168.0.107:8080

生成环境

#在根目录创建 .env.production 文件

REACT_APP_URL = http://192.168.0.107:8080

三种方式初始化项目

npx

npx create-react-app my-app

npm

npm init react-app my-app

yarn

yarn create react-app my-app

常用包

yarn add react-router-dom  //react路由

yarn add antd-mobile  //antd移动UI组件

yarn add node-sass //sass

yarn add prop-types //props校验

yarn add @zeit/next-css@1.0.1 @zeit/next-less@1.0.1 less@3.8.1 -S  //安装less

react-virtualized(长列表)

//展示大型列表和表格数据(城市列表,通讯录,微博等),会导致页面卡顿、滚动不流畅等性能问题
//产生性能问题的原因:大量DOM节点的重绘和重排
//优化方案:1.懒渲染(懒加载),2.可视区域渲染
//懒渲染:每次渲染一部分数据,速度快,缺点:数据量大时,页面依然存在大量DOM,占用内存多

ArrowKeyStepper
AutoSizer
CellMeasurer
Collection
ColumnSizer
Grid
InfiniteLoader
List
Masonry
MultiGrid
ScrollSync
Table
WindowScroller

#层级
InfiniteLoader ==>  WindowScroller ==>  AutoSizer ==> List



yarn add react-virtualized  //长列表组件(可视区域渲染)

github:https://github.com/bvaughn/react-virtualized

基本使用
//引入样式
import 'react-virtualized/styles.css'
//页面引入组件
import { List,AutoSizer } from 'react-virtualized'


const list = [
  'Brian Vaughn'
  // And so on...
];

function rowRenderer({
  key, // Unique key within array of rows
  index, //索引号
  isScrolling, // 当前项是否正在滚动中
  isVisible, // 当前项在list中是可见的
  style // 注意:重点属性,一定要给每一个行数据添加样式,作用:指定每一行的位置
}) {
  return (
    <div key={key} style={style}>
      {list[index]}
    </div>
  );
}


ReactDOM.render(
 <AutoSizer>
          {({ height, width }) => (
            <List
              height={height}
              rowCount={list.length}
              rowHeight={50}
              rowRenderer={rowRenderer}
              width={width}
            />
          )}
 </AutoSizer>,
  document.getElementById('example'),
);

rowRenderer// fun 渲染行函数
onRowsRendered //fun 获取滚动信息
rowRenderer //fun 获取指定到滚动的行
measureAllRows // 提前计算所有行

react-spring(动画库)

//官网
https://www.react-spring.io/docs/props/spring
//github
https://github.com/react-spring/react-spring#readme

//安装
yarn add react-spring

//引用
import {Spring} from 'react-spring/renderprops'


formik(表单验证)

//官网
https://jaredpalmer.com/formik/docs/overview
//github
https://github.com/jaredpalmer/formik

//安装
yarn add formik


实例

import React from 'react'
// 导入withFormik
import { withFormik } from 'formik'
class Login extents React.Component{
    
}

Login = withFormik()(Login)

export default Login

yup(表单校验)

//github
https://github.com/jquense/yup

//安装
yarn add yup

//引用
import * as yup from 'yup'; // for everything
// or
import { string, object } from 'yup'; // for only what you need

React核心概念

jsx

是javaScript + xml 的简写 ,不是html也不是字符串,就是react对象。

作用:创建react对象

底层就是:React.createElement

表达式

变量

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

ReactDOM.render(
  element,
  document.getElementById('root')
);

函数

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

ReactDOM.render(
  element,
  document.getElementById('root')
);

class的使用

//不能写成class要写成className
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

Diff算法

Virtual DOM

  • 操作DOM 是很慢的。如果我们把一个简单的 div 元素的属性都打印出来,你会看到很多属性
// 1. 获取 div 元素
let div = document.querySelector('div')
// 2. 获取属性
let str = ''
let count = 0
for (let key in div) {
    count++
    str += key + ' '
}
console.log(str,count)
  • 而这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候你要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。
  • 相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:
var element = {
    tagName: 'ul', // 节点标签名
    props: { // DOM 的属性,用一个对象存储键值对
        id: 'list'
    },
    children: [ // 该节点的子节点
        {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
    ]
}
  • 上面对应的 HTML 写法是:
<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>
  • 这就是所谓的 Virtual DOM 算法。包括几个步骤:
    • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
    • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
    • 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了
  • Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

Diff算法

  • React 中有两种假定:
    • 1 两个不同类型的元素会产生不同的树
    • 2 开发者可以通过 key 属性指定不同树中没有发生改变的子元素

Diff 算法的说明 - 1

  • 如果两棵树的根元素类型不同,React 会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程:destory Counter -> insert Counter

Diff 算法的说明 - 2

  • 对于类型相同的 React DOM 元素,React 会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个 DOM 节点,React 就会递归处理子节点。
// 旧
<div className="before" title="stuff"></div>
// 新
<div className="after" title="stuff"></div>
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}}></div>
// 新
<div style={{color: 'green', fontWeight: 'bold'}}></div>
只更新:color属性

Diff 算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>1</li>
  <li>2</li>
</ul>

// 新
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

执行过程:
React将改变每一个子节点,而非保持 <li>Duke</li> 和 <li>Villanova</li> 不变

key 属性

为了解决以上问题,React 提供了一个 key 属性。当子节点带有 key 属性,React 会通过 key 来匹配原始树和后来的树。

// 旧
<ul>
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

// 新
<ul>
  <li key="3">3</li>
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015''2016' 仅仅移动位置即可
  • 说明:key 属性在 React 内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key 只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组 index 作为 key,数组中插入元素的等操作时,会使得效率底下

函数组件

#函数组件没有状态
import React from 'react'
import ReactDOM from 'react-dom'

function Child(props){
  return (
  <div>
    <div id="app">hello {props.name}</div>
    <p>word {props.age}</p>
  </div>)
}

ReactDOM.render(<Child name='张三' age={30}/>,document.getElementById('root'))

Class组件

#类组件是有状态,有自己的数据
import React from 'react'
import ReactDOM from 'react-dom'
//1.首字母大写
//2.类要继承React.Component
//3.state就相当于vue里的data
class Child extends React.Component{
    state = {
        
    }
	render(){
    	return (
  			<div>
    			<div id="app">hello {props.name}</div>
    			<p>word {props.age}</p>
  			</div>
		)
	}
}
ReactDOM.render(<Child name='张三' age={30}/>,document.getElementById('root'))

生命周期

一、挂载阶段

挂载之前
1.constructor() 构造器
2.得到外界传过来的属性
3.初始化状态
4.render() 渲染
挂载之后
//可以发送ajax,操作DOM
componentDidMount()

二、更新阶段

shouldComponentUpdate(nextProps,nextState) //是否组件更新渲染,返回boolean
	nextProps:最新属性
    nextState:最新状态

return false ; 不渲染 , 不会再走 render()
return true ; 渲染 , 走 render()
路由切换
componentDidUpdate(prevProps) //路由切换的时候会执行此方法
	prevProps:路由信息
    
    
    
    
#判断路由是否相等
componentDidUpdate(prevProps){
    if (prevProps.location.pathname !== this.props.location.pathname) {
      this.setState({
        selectedTab: this.props.location.pathname
      })
    }
}

三、销毁阶段

componentWillUnmount() 

函数组件与类组件区别

函数组件:没有状态 (没有自己的私有数据) 木偶组件 组件一旦写好,就不会改变

传参: function Child( props) { } 只读

**类组件:**有状态 (有自己的私有数据 state) 智能组件 状态发生改变,就会更新视图

​ **传参:**1- this.props 2-constructor(props) { supoer(props) }

优点:

类组件 : 有状态, 有生命周期钩子函数 ,功能比较强大

函数组件 : 渲染更快

区分使用 ?

​ 就看要不要状态, 要状态(类组件) 不要状态(函数组件)

props 和 state 能区分开吗?

​ state:自己的私有属性 类似vue的data

​ props: 外界传进来的

事件处理

import React from 'react'
import ReactDOM from 'react-dom'

class Child extends React.Component{
  state = {
    time:"aaa"
  }
  render(){
    return (
      <div>
        <button onClick={this.fn.bind(this,this.state.time)}>按钮</button>
        <button onClick={()=>{this.fn(this.state.time)}}>按钮1</button>
      </div>
    )
  }
  fn(num){
    console.log("get:" + num);
  }
}

ReactDOM.render(<Child />,document.getElementById('root'))


//向事件处理程序传递参数 this指向
onClick={this.fn.bind(this)}
onClick={(e)=>{this.fn()}}

事件对象

import React from 'react'
import ReactDOM from 'react-dom'

#有参
class Child extends React.Component{
  state = {
    time:"aaa"
  }
  render(){
    return (
      <div>
        <button onClick={this.fn.bind(this,123)}>按钮</button>
      </div>
    )
  }
  fn(num,e){
    console.log(num,e);
  }
}
#无参
class Child extends React.Component{
  state = {
    time:"aaa"
  }
  render(){
    return (
      <div>
        <button onClick={this.fn}>按钮</button>
      </div>
    )
  }
  fn(e){
    console.log(e.target);
  }
}

ReactDOM.render(<Child />,document.getElementById('root'))

条件渲染

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

ReactDOM.render(
  <Greeting isLoggedIn={false} />,
  document.getElementById('root')
);

列表渲染

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

表单

input(受控组件)

input、textarea、select

class Child extends React.Component {
  state = {
    username: '张三',
    detailt:'我也不知道'
  }
  render () {
    return (
      <div>
        <input type="text" name="username" value={this.state.username} onChange={this.handleInput.bind(this)} style={{color:'red'}}/>
        <textarea type="text" name="detailt" value={this.state.detailt} onChange={this.handleInput.bind(this)} style={{color:'red'}}/>
      </div>
    )
  }
  handleInput (e) {
    this.setState({[e.target.name]:e.target.value})
  }
}

ref(非受控)

class Child extends React.Component {
  constructor(props){
    super(props)
    this.usernameRef = React.createRef()
  }
  state = {
    username: '张三',
    detailt:'我也不知道'
  }
  render () {
    return (
      <div>
        <input type="text" name="username" value={this.state.username} ref={this.usernameRef} onChange={this.handleInput.bind(this)}/>
        <button onClick={this.fun.bind(this)}>按钮</button>
      </div>
    )
  }
  fun (e) {
    console.log(this.usernameRef.current.value);
  }
  handleInput(e){
    this.setState({[e.target.name]:e.target.value})
  }
}

非受控

#defaultValue
<input type="text" defaultValue={this.state.username}/>
    
#defaultChecked

实例

import React,{ Component, createRef } from 'react'

class Sticky extends Component {
  // 创建ref对象
  placeholder = createRef()
  content = createRef()

  // 获取DOM对象
  const placeholderEl = this.placeholder.current
  const contentEl = this.content.current

  
  render() {
    return (
      <div>
        {/* 占位元素 */}
        <div ref={this.placeholder} />
        {/* 内容元素 */}
        <div ref={this.content}>{this.props.children}</div>
      </div>
    )
  }
}

export default Sticky

特殊属性

# for ==> htmlFor
<label htmlFor=""></label>

# class ==> className
<label className=""></label>

# style ==> {{}}
<label style={{color:'red'}}></label>

组件通信

children

调用者

<Sticky>
    <Filter></Filter>
</Sticky>

Sticky组件

render() {
    return (
      <div>
        {/* 内容元素 */}
        <div>{this.props.children}</div>
      </div>
    )
}
//把<Filter>组件放到children指定的位置渲染

父传子

父组件

class Father extends React.Component {
  state = {
    msgf:'父亲'
  }
  render(){
    return <div><h2>父组件:</h2><Child msg={this.state.msgf}></Child></div>
  }
}

子组件

class Child extends React.Component{
  render(){
    return <h4>子组件:{this.props.msg}是儿子的爸爸</h4>
  }
}

子传父

父组件

class Father extends React.Component {
  state = {
    msgf:'父亲',
    childMsg:''
  }
  render(){
    return <div><h2>父组件:{this.state.childMsg}</h2><Child msg={this.state.msgf} fun={this.onFun}></Child></div>
  }
  onFun=(res)=>{
    this.setState({
      childMsg:res
    })    
  }
}

子组件

class Child extends React.Component{
  state = {
    msgc:'儿子'
  }
  render(){
    return <div><h4>子组件:{this.props.msg}是儿子的爸爸</h4><button onClick={this.onChild.bind(this)}>子按钮</button></div>
  }
  onChild(){
    this.props.fun(this.state.msgc)
  }
}

Context(全局)

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

//创建context
const context = React.createContext()

context.Provider   //提供者
context.Consumer   //消费者

代码

import React from 'react'
import ReactDOM from 'react-dom'

const { Provider, Consumer } = React.createContext()

//父组件
class Father extends React.Component {
  state = {
    color: 'red'
  }
  render () {
    return (
      <Provider value={this.state.color}>
        <div className="fa">
          父 : <Son />
        </div>
      </Provider>
    )
  }
}

//子组件
class Son extends React.Component {
  state = {}
  render () {
    return (
      <div className="son">
        子 : <Sun />
      </div>
    )
  }
}

//孙组件
class Sun extends React.Component {
  state = {}
  render () {
    return (
      <Consumer>
        {value => {
          return (
            <div style={{ color: value }} className="sun">
              孙 :
            </div>
          )
        }}
      </Consumer>
    )
  }
}

ReactDOM.render(<Father/>, document.getElementById('root'))

PropTypes(props校验)

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

HOOK

Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。

State Hook

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Effect Hook

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

React路由

安装

yarn add react-router-dom

路由使用

基本使用

import { BrowserRouter as Router,Route,Link } from 'react-router-dom'

function App() {
  return (
    <Router>
      <div className="App">
        {/* 配置路由 */}
        <Route path="/home" component={Home}></Route>
        <Route path="/citylist" component={CityList}></Route>
      </div>
    </Router>
  )
}

编程式导航

this.props.history.push('/home/index')

获取路由url

this.props.location.pathname

重定向

function App() {
  return (
    <Router>
      <div className="App">
        {/* 默认路由匹配,跳转到/home 实现路由重定向到首页 */}
        <Route exact path="/" render={()=> <Redirect to="/home" />} ></Route>
        {/* 配置路由 */}
        <Route path="/home" component={Home}></Route>
          
          
        <Route path="/citylist" component={CityList}></Route>
      </div>
    </Router>
  )
}


 render() {
    return (
      <div className="home">
            
            
        <Route exact path="/home" component={Index}></Route>
            
            
        <Route path="/home/houseList" component={HouseList}></Route>
        <Route path="/home/news" component={News}></Route>
        <Route path="/home/profile" component={Profile}></Route>
        {/* TabBar */}

        <TabBar tintColor="#21b97a" noRenderContent={true} barTintColor="white">
          {this.renderTabBarItem()}
        </TabBar>
      </div>
    )
  }

withRouter的使用

这样使用可以获取history

import React from 'react';

import { NavBar} from 'antd-mobile';

import {withRouter} from 'react-router-dom'

import './index.scss'


function NavHeader({children,history,onListClick}) {
  //默认行为
  const defaultHandle = () => history.go(-1)
  return (
    <NavBar
      className="navbar"
      mode="light"
      icon={<i className="iconfont icon-back"></i>}
      onLeftClick={onListClick || defaultHandle}
    >
     {children}
    </NavBar>
  );
}

export default withRouter(NavHeader)

路由参数

传参

<Link to="/detail/1">跳转</Link>

<Route path="/detail/:id" component=... />

获取参数

this.props.match.params.id

props

路由通过props传递给组件

history
location
match

AuthRoute(鉴权路由)

Redux(状态管理器)

转存失败,建议直接上传图片文件

首先,用户发出 Action。

store.dispatch(action);

然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

let nextState = todoApp(previousState, action);

State 一旦有变化,Store 就会调用监听函数。

// 设置监听函数
store.subscribe(listener);

listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。

function listerner() {
let newState = store.getState();
component.setState(newState);   
}

安装

npm install --save redux

yarn add redux@4.0.0 react-redux@5.0.7 next-redux-wrapper@2.0.0 -S

- redux : 数据流框架
- react-redux:数据流在react中的实现包装

redux使用

import { createStore } from 'redux'

1.定义reducer
2.基于reduce创建Store
3.获取store 的状态
4.更新store的状态
5.监测store中state的变化,驱动视图更新

Store

Store 对象包含所有数据

import { createStore } from 'redux';
const store = createStore(fn);

const state = store.getState();

Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

Action

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

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

Action Creator

View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。

const ADD_TODO = '添加 TODO';

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo('Learn Redux');

上面代码中,addTodo函数就是一个 Action Creator。

store.dispatch

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});


store.dispatch(addTodo('Learn Redux'));

reducer

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

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

const reducer = function (state, action) {
  // ...
  return new_state;
};

整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const state = reducer(1, {
  type: 'ADD',
  payload: 2
});

上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。

实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。

import { createStore } from 'redux';
const store = createStore(reducer);

上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。

为什么这个函数叫做 Reducer 呢?因为它可以作为数组的reduce方法的参数。请看下面的例子,一系列 Action 对象按照顺序作为一个数组。

const actions = [
  { type: 'ADD', payload: 0 },
  { type: 'ADD', payload: 1 },
  { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

上面代码中,数组actions表示依次有三个 Action,分别是加0、加1和加2。数组的reduce方法接受 Reducer 函数作为参数,就可以直接得到最终的状态3

纯函数

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。

纯函数是函数式编程的概念,必须遵守以下一些约束。

  • 不得改写参数
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。

// State 是一个对象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state, action) {
  return [...state, newItem];
}

最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。

store.subscribe()

Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。

import { createStore } from 'redux';
const store = createStore(reducer);

store.subscribe(listener);

显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。

store.subscribe方法返回一个函数,调用这个函数就可以解除监听。

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

Reducer 的拆分

Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大。

请看下面的例子。

const chatReducer = (state = defaultState, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case ADD_CHAT:
      return Object.assign({}, state, {
        chatLog: state.chatLog.concat(payload)
      });
    case CHANGE_STATUS:
      return Object.assign({}, state, {
        statusMessage: payload
      });
    case CHANGE_USERNAME:
      return Object.assign({}, state, {
        userName: payload
      });
    default: return state;
  }
};

上面代码中,三种 Action 分别改变 State 的三个属性。

  • ADD_CHAT:chatLog属性
  • CHANGE_STATUS:statusMessage属性
  • CHANGE_USERNAME:userName属性

这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可。

const chatReducer = (state = defaultState, action = {}) => {
  return {
    chatLog: chatLog(state.chatLog, action),
    statusMessage: statusMessage(state.statusMessage, action),
    userName: userName(state.userName, action)
  }
};

上面代码中,Reducer 函数被拆成了三个小函数,每一个负责生成对应的属性。

这样一拆,Reducer 就易读易写多了。而且,这种拆分与 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 Reducer 完全可以对应。

Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。

import { combineReducers } from 'redux';

const chatReducer = combineReducers({
  chatLog,
  statusMessage,
  userName
})

export default todoApp;

上面的代码通过combineReducers方法将三个子 Reducer 合并成一个大的函数。

这种写法有一个前提,就是 State 的属性名必须与子 Reducer 同名。如果不同名,就要采用下面的写法。

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})

// 等同于
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

总之,combineReducers()做的就是产生一个整体的 Reducer 函数。该函数根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象。

你可以把所有子 Reducer 放在一个文件里面,然后统一引入。

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const reducer = combineReducers(reducers)

中间件和异步操作

上一小节,我们学习了 Redux 的基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。

Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。

怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。

中间件的概念

举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。

中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。

中间件的用法

日志中间件, redux-logger 模块。

import { applyMiddleware, createStore } from 'redux';
import { createLogger } from 'redux-logger';
const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

上面代码中,redux-logger提供一个生成器createLogger,可以生成日志中间件logger。然后,将它放在applyMiddleware方法之中,传入createStore方法,就完成了store.dispatch()的功能增强。

这里有两点需要注意:

(1)createStore方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware就是第三个参数了。

const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);

(2)中间件的次序有讲究。

const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);

上面代码中,applyMiddleware方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。

Tip:

  • applyMiddleware 这个方法是 Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。

异步操作的基本思路

理解了中间件以后,就可以处理异步操作了。

同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。

  • 操作发起时的 Action
  • 操作成功时的 Action
  • 操作失败时的 Action

以向服务器取出数据为例,三种 Action 可以有两种不同的写法。

// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。

let state = {
  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: 'xxxxxxx'
};

上面代码中,State 的属性isFetching表示是否在抓取数据。didInvalidate表示数据是否过时,lastUpdated表示上一次更新时间。

现在,整个异步操作的思路就很清楚了。

  • 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
  • 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染

redux-thunk 中间件

github.com/reduxjs/red…

redux-promise 中间件

github.com/redux-utili…

Redux 结合 React 使用

强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

尽管如此,Redux 还是和 ReactDeku 这类库搭配起来用最好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

Redux 默认并不包含 React 绑定库,需要单独安装。

npm install --save react-redux

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。

展示组件容器组件
作用描述如何展现(骨架、样式)描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源props监听 Redux state
数据修改从 props 调用回调函数向 Redux 派发 actions
调用方式手动通常由 React Redux 生成

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。

UI 组件有以下几个特征。

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API

下面就是一个 UI 组件的例子。

const Title =
value => <h1>{value}</h1>;

因为不含有状态,UI 组件又称为"纯组件",即它纯函数一样,纯粹由参数决定它的值。

容器组件的特征恰恰相反。

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API

总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

你可能会问,如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

conncect

React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。

import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

上面代码中,TodoList是 UI 组件,VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件。

但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。

(1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数

(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

因此,connect方法的完整 API 如下。

import { connect } from 'react-redux'

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

mapStateToProps()

mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。

作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。

const mapStateToProps = (state) => {
return {
 todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数,后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。

下面就是getVisibleTodos的一个例子,用来算出todos

const getVisibleTodos = (todos, filter) => {
switch (filter) {
 case 'SHOW_ALL':
   return todos
 case 'SHOW_COMPLETED':
   return todos.filter(t => t.completed)
 case 'SHOW_ACTIVE':
   return todos.filter(t => !t.completed)
 default:
   throw new Error('Unknown filter: ' + filter)
}
}

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。

// 容器组件的代码
//    <FilterLink filter="SHOW_ALL">
//      All
//    </FilterLink>

const mapStateToProps = (state, ownProps) => {
return {
 active: ownProps.filter === state.visibilityFilter
}
}

使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。

connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

mapDispatchToProps()

mapDispatchToPropsconnect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

如果mapDispatchToProps是一个函数,会得到dispatchownProps(容器组件的props对象)两个参数。

const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
 onClick: () => {
   dispatch({
     type: 'SET_VISIBILITY_FILTER',
     filter: ownProps.filter
   });
 }
};
}

从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。

const mapDispatchToProps = {
onClick: (filter) => {
 type: 'SET_VISIBILITY_FILTER',
 filter: filter
};
}

Provider 组件

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

React-Redux 提供Provider组件,可以让容器组件拿到state

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
<Provider store={store}>
 <App />
</Provider>,
document.getElementById('root')
)

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。