CSS in JS
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>
注意事项
-
在样式文件中修改当前组件的样式(使用单个类名设置样式,不实用嵌套样式)。
-
命名推荐使用驼峰命名发
-
对于组件库中已经有的全局样式(比如:.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 中间件
redux-promise 中间件
Redux 结合 React 使用
强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,Redux 还是和 React 和 Deku 这类库搭配起来用最好,因为这类库允许你以 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方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 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()
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的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了。