一、React简介
React
的基础大体包括下面这些概念:
- 组件
JSX
Virtual DOM
Data Flow
React.js
不是一个框架,它只是一个库。它只提供UI
层面的解决方案。在实际的项目当中,它并不能解决我们所有的问题,需要结合其它的库,例如Redux
、React-router
等来协助提供完整的解决方案。
二、React浏览器开发环境
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!--凡是使用 JSX 的地方,都要加上 type="text/babel"-->
<script type="text/babel">
// 1. 创建虚拟DOM
const vDom = <h1>Hello,React</h1>
// 2. 渲染虚拟DOM到页面
ReactDOM.render(vDom, document.getElementById('root'))
</script>
</body>
</html>
搭建浏览器开发环境一共用了3个库:react
、react-dom
、babel
,它们必须首先加载。
react.js
:React
的核心库。react-dom.js
:负责Web
页面的DOM
操作。babel.js
:将JSX
语法转为JavaScript
语法。
三、Virtual DOM介绍
一个真实页面对应一个DOM
树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作DOM
来进行更新。DOM
操作非常昂贵。而且这些操作DOM
的代码变得难以维护。
React
把真实DOM
树转换成JavaScript
对象树,也就是Virtual DOM
。如下图:
每次数据更新后,重新计算Virtual DOM
,并和上一次生成的Virtual DOM
做对比,对发生变化的部分做批量更新。VirtualDOM
不仅提升了React
的性能,而且它最大的好处还可以在其他平台集成(比如react-native
是基于Virtual DOM
渲染出的原生控件)。
因此在
Virtual DOM
输出的时候,是输出Web DOM
,还是Android
控件,还是iOS
控件,由平台本身决定。
四、JSX介绍
手动编码创建虚拟DOM
是非常是非常繁琐的,如果要创建<h1 id="title"><span>Hello,React</span></h1>
的html
的结构:
<body>
<div id="root"></div>
<script type="text/babel">
// 1. 创建虚拟DOM
const element = React.createElement('h1', { id: 'title' }, React.createElement('span', null, 'Hello,React!'))
// 2. 渲染虚拟DOM到页面
ReactDOM.render(element, document.getElementById('root'))
</script>
</body>
React.createElement
会构建一个JavaScript
对象来描述HTML
结构的信息,包括标签名、属性、还有子元素等。类似如下的对象结构:
{
type: 'h1',
props: {
id: 'title',
children: {
type: 'span',
props: {
children: 'Hello,React'
}
}
}
}
使用React.createElement
创建虚拟DOM
的时候在标签结构还不怎么复杂的结构时,书写就已经很难受了。没有使用HTML
编写结构时的简洁。为了解决这个问题,所以JSX
语法就诞生了。假如我们使用JSX
语法来重新表达上述元素,只需下面这么写:
<body>
<div id="root"></div>
<script type="text/babel">
// 1. 创建虚拟DOM
const element = (
<h1 id="title">
<span>Hello,React</span>
</h1>
)
// 2. 渲染虚拟DOM到页面
ReactDOM.render(element, document.getElementById('root'))
</script>
</body>
JSX
将HTML
语法直接加入到JavaScript
代码中,会让代码更加直观并易于维护。通过编译器转换到纯JavaScript
后由浏览器执行。JSX
在产品打包阶段都已经编译成了纯JavaScript
。
JSX
是JavaScript
语言的一种语法扩展,长得像HTML
,但并不是HTML
。JSX
是第三方标准,这套标准适用于任何一套框架。使用Babel
的JSX
编译器可以实现对JSX
语法的编译。
4.1、JSX中的HTML属性
React
与HTML
之间有很多属性存在差异。比如说class
要改写成className
:
ReactDOM.render(<div className="foo">Hello</div>, document.getElementById('root'))
4.2、JSX中的JavaScript表达式
JSX
遇到HTML
标签(以<
开头),就用HTML
规则解析。遇到代码块(以 {
开头),就用 JavaScript
规则解析。也就是说在JSX
使用表达式要包裹在大括号{}
中:
<body>
<div id="root"></div>
<script type="text/babel">
const names = ['zhangsan', 'lisi', 'wangwu']
const title = '标题'
ReactDOM.render(
<div>
<h1>{title}</h1>
<div>
{names.map((name, key) => {
return <div key={key}>Hello,{name}</div>
})}
</div>
</div>,
document.getElementById('root')
)
</script>
</body>
JSX
可以直接在模板插入JavaScript
变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
const names = [<div key="1">zhangsan</div>, <div key="2">lisi</div>]
ReactDOM.render(
<div>
{names}
</div>,
document.getElementById('root')
)
JSX 的{}
内可以嵌入任何表达式,{{}}
就是在{}
内部用对象字面量返回一个对象。比如说内联样式要用style={{key:value}}
的形式去编写:
<span style={{ color: 'red' }}>Hello,React</span>
4.3、JSX中的注释
在JSX
里使用注释也很简单,就是沿用JavaScript
,唯一要注意的是在一个组件的子元素位置使用注释要用 {}
包起来。
const element = (
/* 多行
注释 */
// 单行注释
<h1 id="title">
{/* 节点注释 */}
<span>Hello,React</span>
</h1>
)
ReactDOM.render(element, document.getElementById('root'))
4.4、JSX中的HTML转义
React
会将所有要显示到DOM
的字符串转义,防止XSS
。所以任何的HTML
格式都会被转义掉:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { title: '<h1>标题</h1>' }
}
render() {
return <div>{this.state.title}</div>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
表达式插入并不会把一个<h1>
渲染到页面,而是以文本形式渲染:
angerouslySetInnerHTML
是React
为浏览器DOM
提供 innerHTML
的替换方案,可以使dangerouslySetInnerHTML
来实现,dangerouslySetInnerHTML
传入一个对象,这个对象的 __html
属性值就相当于元素的innerHTML
:
render() {
return <div dangerouslySetInnerHTML={{ __html: this.state.title }}></div>
}
4.5、JSX中的自定义HTML属性
如果在JSX
中使用的属性不存在于HTML
的规范中,这个属性会被忽略。如果要使用自定义属性,可以用data-
前缀。可访问性属性的前缀aria-
也是支持的。
ReactDOM.render(<div data-attr="abc">内容</div>, document.getElementById('root'))
4.6、Fragment 标签
React.Fragment
代表空标签。它能够在不额外创建DOM
元素的情况下,让render()
方法中返回多个元素。Fragment
只能接收key
属性:
import React from 'react'
class App extends React.Component {
render() {
return (
<React.Fragment key={1}>
<p>App</p>
</React.Fragment>
)
}
}
Fragment
简写语法为 <></>
,但是简写方式不能使用key
:
<>
<p>App</p>
</>
五、组件化
React
允许将代码封装成组件(component
)。Component
(组件)可以是类组件(class component
)、函数式组件(function component
)。
组件有三个核心概念,React
组件基本上由组件的外部属性状态(props
),内部属性状态(state
)和生命周期方法组成。如下图:
组件生成的 HTML
结构只能有一个单一的根节点。在React
中,数据是自顶向下单向流动的,从父组件到子组件。
官方在React
组件构建上提供了2
种不同的方法:ES6 class
和无状态函数(stateless function
)。
官方在
React@15.5.0
后不推荐用React.createClass
创建组件了,这里就不做介绍了。
5.1、class方式编写组件
class Hello extends React.Component {
constructor(props) {
super(props)
}
render() {
return (<h1>Hello,React</h1>)
}
}
ReactDOM.render(<Hello />, document.getElementById('root'))
5.2、无状态组件
可以用纯函数来定义无状态的组件(stateless function
),这种组件没有状态(state
),没有生命周期,只是简单的接收props
渲染生成DOM
结构。无状态组件非常简单,开销很低。比如使用函数定义:
function Person(props) {
return <h1>Hello, {props.name}</h1>
}
ReactDOM.render(<Person name="张三" />, document.getElementById('root'))
六、state
state
是组件的当前状态(数据)。一旦状态(数据)更改,组件就会自动调用render
重新渲染UI
,这个更改的动作通过this.setState
方法来触发。
不能直接修改state
,要使用setState()
方法进行修改。如果直接修改state
,组件不会重新触发render
方法:
// 错误,不会触发render()重新渲染
this.state.count = this.state.count + 1
6.1、setState()方法介绍
setState()
方法,它接受一个对象或者函数作为参数,来更新state
。
- 对象作为参数时只需要传入需要更新的部分,
React
会自动执行浅合并,而不需要传入整个对象:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0, name: '张三' }
}
handleClick() {
// 只修改count
this.setState({ count: this.state.count + 1 })
}
render() {
return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
- 函数作为参数时可以得到
prevState
和props
两个参数,state
的值通过对象返回:
prevState
:上一个state
。props
:更新被应用时的props
。
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick(e) {
this.setState((prevState, props) => {
// 返回第一个state
return { count: this.state.count + 1 }
})
this.setState((prevState, props) => {
//第一个state,也就是上一个的state
console.log(prevState) // 1
return { count: this.state.count + 1 }
})
}
render() {
return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
6.2、setState()异步更新
setState()
方法更新state
是异步的。React
并不会马上修改state
。而是把这个对象放到一个更新队列里面,最后才会从队列当中把新的状态提取出来合并到state
当中,然后再触发组件更新:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick(e) {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 0
}
render() {
return <div onClick={this.handleClick.bind(this)}>{this.state.count}</div>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
当触发handleClick
事件的时候,this.state.count
输出是0
。
一种解决方案是setState(updater, [callback])
方法还可以传入一个回调函数,一旦setState()
完成并且组件重绘之后,这个回调函数将会被调用。
handleClick(e) {
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count) // 1
})
}
6.3、setState()浅合并
React.js
出于性能原因,可能会将多次setState()
的状态修改合并成一次状态修改。所以不要依赖当前的setState()
计算下个State
。如下的一个计数器:
class AddCount extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick() {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<div>
{/* count 输出为1 */}
<h2>{this.state.count}</h2>
<button onClick={this.handleClick.bind(this)}>添加</button>
</div>
)
}
}
当触发handleClick
事件的时候,会发现页面输出的是1
。虽然setState()
方法调用了两次。是因为当调用setState()
修改组件状态时,组件state
的更新其实是一个浅合并的过程,相当于:
Object.assign(
previousState,
{count: state.count + 1},
{count: state.count + 1},
...
)
所以如果后续操作要依赖前一个setState()
的结果的情况下就要使用函数来作为setState()
参数。React
会把上一个setState()
的结果传入这个函数,就可以使用上一个正确的结果进行操作,然后返回一个对象来更新state
:
handleClick () {
this.setState({ count: this.state.count + 1 })
this.setState((prevState) => {
// 1
console.log(prevState.count)
return { count: prevState.count + 1 }
})
}}
把上次操作setState()
更新的值传入到下一个setState()
里,就可以正确的显示count
了。
6.4、state的Immutable(不可变性)
React
官方建议把state
当作是的Immutable
(不可变性)对象,state
中包含的所有状态都应该是不可变对象。当state
中的某个状态发生变化,我们应该重新创建这个状态对象,而不是直接修改原来的状态。
假如有一个数组类型的状态names
,当向name
中添加一个名字时,使用数组的concat
方法或ES6
的扩展运算符:
const names = ['张三']
// 1
this.setState(prevState => ({
names: prevState.names.concat(['李四'])
}))
// 2
this.setState(prevState => ({
names: [...prevState.names,'李四']
}))
不要使用
push
、pop
、shift
、unshift
、splice
等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat
、slice
等返回一个新的数组。
假如有一个对象类型的状态person
,为了不改变原本的对象,我们可以使用Object.assign
方法或者对象扩展属性:
const person = { age: 30 }
// 1
function updatePerson (person) {
return Object.assign({}, person, { age: 20 })
}
// 2
function updatePerson (person) {
return {...person,age:20}
}
创建新的状态对象要避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。
6.5、修改深度嵌套对象
由于setState()
只合并对象属性的第一级。如果想修改多层嵌套对象内的一个属性,就要像下面一层一层解构:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { person: { city: { cityName: '北京' } } }
}
handleClick() {
this.setState((prevState) => {
return {
person: {
...prevState.person,
city: {
...prevState.person.city,
cityName: '上海'
}
}
}
})
}
render() {
return (
<div>
<h2>{this.state.person.city.cityName}</h2>
<button onClick={this.handleClick.bind(this)}>修改城市</button>
</div>
)
}
}
上面代码可以看出当state
对象结构的层级更深的时候,改动最深层的state
子节点写起来会更麻烦。
我们可以想出一个简单的解决方案,先深拷贝出一个新的对象,然后直接更改新对象的属性,比如使用lodash
的cloneDeep
:
handleClick() {
this.setState((prevState) => {
const newState = _.cloneDeep(prevState)
newState.person.city.cityName = '上海'
return newState
})
}
但是,这种方案有明显的性能问题。不管打算更新对象的哪一个属性,每次都要深拷贝整个对象。当对象特别大的时候,深拷贝会导致性能问题。
还有另一种解决方案就是可以使用一些Immutable
的库,如immer.js
来简化开发。它避免深拷贝所有属性,而只针对目标属性进行修改。
// 安装
npm i immer
当我们调用immer
的API produce
时,immer
将内部暂时存储着我们的目标对象。并暴露一个draft
(草稿)我们就可以在draft
上作修改,然后返回。这里只是简单使用:
import { produce } from 'immer'
handleClick() {
this.setState((prevState) => {
return produce(prevState, (draftState) => {
draftState.person.city.cityName = '上海'
})
})
}
6.6、setState() 使用原则
-
如果新状态不依赖上一个状态可以使用对象方式。
-
如果新状态依赖上一个状态使用函数的方式。
-
如果需要在
setState()
获取最新的状态数据,可以在第二个callback
函数中获取。
七、props
组件是相互独立、可复用的。一个组件可能在不同地方被用到。在不同的场景下对这个组件的需求可能会根据情况有所不同,所以要针对相同的组件传入不同的配置项。
React
的props
就可以达到这个效果。每个组件都可以接受一个props
参数,它是一个对象,包含了所有对这个组件的配置:
class Person extends React.Component {
render() {
const { name, age } = this.props
return (
<ul>
<li>{name}</li>
<li>{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person name="张三" age={20} />, document.getElementById('root'))
组件内部是通过this.props
的方式获取到组件的参数的。在使用一个组件的时候,所有的属性都会作为props
对象的键值。我们还可以通过defaultProps
静态属性来指定默认值:
class Person extends React.Component {
// 指定默认值
static defaultProps = {
name: '李四',
age: 30
}
render() {
const { name, age } = this.props
return (
<ul>
<li>{name}</li>
<li>{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person age={20} />, document.getElementById('root'))
如果没有传入name
属性,就会使用自定义默认的属性值:
7.1、props 标签属性类型检查
有时候我们需要对传入props
的类型进行限制。从React16
开始,类型检查被移除拆分为另一个库中。需要单独安装:
npm install prop-types
或者直接在浏览器引入:
<script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>
下面是一个简单的小例子:
class Person extends React.Component {
// 对属性进行检查
static propTypes = {
name: PropTypes.string.isRequired, // name必传,并且为字符串
getName: PropTypes.func // getName为函数 因为`function`是关键字,所以使用`func`
}
render() {
const { name, age } = this.props
return (
<ul>
<li>{name}</li>
<li>{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person name="张三" age={20} />, document.getElementById('root'))
7.2、props 不可变
props
一旦传入进来就不可以在组件内部对它进行修改。但是可以通过父组件主动修改state
重新渲染的方式来传入新的props
,从而达到更新的效果。如下:
// 子组件
function Child(props) {
return <div>{props.name}</div>
}
// 父组件
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
// 修改name,重新渲染
changeName() {
this.setState({ name: '李四' })
}
render() {
return (
<div>
<Child name={this.state.name} />
<button onClick={this.changeName.bind(this)}>修改name</button>
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root'))
八、 Ref
React
并不能完全满足所有DOM
操作需求,有些时候我们还是需要和DOM
打交道。比如进入页面以后自动 focus
到某个输入框,需要调用input.focus()
的DOM API
。React
提供几种方式来获取挂载后元素的DOM
节点。
不要滥用
refs
。比如用它来按照传统的方式操作界面UI:找到 DOM -> 更新 DOM
。
8.1、字符串形式
通过在DOM
元素上面设置一个ref
属性指定一个名称,然后通过 this.refs.name
来访问对应的DOM
元素。
class MyComponent extends React.Component {
// 点击获取焦点
handleClick() {
this.refs.textInput.focus()
}
render() {
return (
<div>
<input ref="textInput" />
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
官方已经不建议使用它,因为
string
类型的refs
存在一些问题。它已过时并可能会在未来的版本被移除。
8.2、回调形式
通过ref
属性可以设置为一个回调函数,回调函数会接收到当前DOM
元素:
class MyComponent extends React.Component {
handleClick() {
this.textInput.focus()
}
render() {
return (
<div>
<input ref={(element) => (this.textInput = element)} />
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
如果ref
回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null
,然后第二次会传入参数DOM
元素。这是因为在每次渲染时会创建一个新的函数实例,所以React
清空旧的ref
并且设置新的。通过将ref
的回调函数定义成class
的绑定函数的方式可以避免上述问题,但是大多数情况下是无关紧要的。
8.3、createRef()形式
在React 16.3
版本中引入的React.createRef() API
也可以用来获取DOM
元素,通过调用createRef()
后返回一个容器,该容器可以存储被ref
所标识的节点,并且该容器只能存一个元素节点:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
handleClick() {
this.myRef.current.focus()
}
render() {
return (
<div>
<input ref={this.myRef} />
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
8.4、ref获取组件实例
Ref
不仅可以获取DOM
元素,还可以获取子组件的实例。下面用三种不同的方式获取子组件实例:
import React, { createRef } from 'react'
// 子组件
class Child extends React.Component {
handleChild() {
console.log('Child Component')
}
render() {
return <div>Child组件</div>
}
}
// 父组件
class Father extends React.Component {
constructor(props) {
super(props)
this.childRef3 = createRef()
}
componentDidMount() {
this.refs.childRef1.handleChild() //Child Component
this.childRef2.handleChild() //Child Component
this.childRef3.current.handleChild() //Child Component
}
render() {
return (
<div>
<Child ref="childRef1" />
<Child ref={(element) => (this.childRef2 = element)} />
<Child ref={this.childRef3} />
</div>
)
}
}
九、事件处理
React
里面绑定事件的方式和在HTML
中绑定事件类似,但是要使用驼峰式命名的方式。下面是一个数字累加的小例子:
class AddCount extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick(e) {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
}
ReactDOM.render(<AddCount />, document.getElementById('root'))
需要注意的是要显式调用
bind(this)
将事件函数上下文绑定组件实例上。
十、收集表单数据
React
处理表单可以通过受控组件和非受控组件来管理。下面分别介绍下面这两种方式。
10.1、受控组件
在React
中,表单元素通过组件的state
属性来维护。并根据用户输入调用setState()
来进行数据更新。被React
以这种方式控制取值的表单输入元素就叫做受控组件。下面是一个修改用户名的一个例子:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { userName: 'init userName' }
}
// 修改用户名
changeUsername(e) {
this.setState({ userName: e.target.value })
}
// 提交
handleSubmit(e) {
e.preventDefault()
const { userName } = this.state
console.log(`您的用户名是:${userName}`)
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<input type="text" value={this.state.userName} name="username" onChange={this.changeUsername.bind(this)} />
<button type="submit">提交</button>
</form>
)
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
在React
中,数据是单向流动的。表单的数据源于组件的state
,并通过props
传入,这也称为单向数据绑定。然后,我们又通过onChange
事件处理器将新的表单数据写回到组件的state
,完成了双向数据绑定。
React
受控组件更新state
的流程:
- 通过在初始
state
中设置表单的默认值。 - 每当表单的值发生变化时,调用
onChange
事件处理器。 - 通过事件处理器获取最新的值,并使用
setState()
更新state
。 setState()
触发视图的重新渲染,完成表单组件值的更新。
大多数情况下,我们还是使用受控组件来处理表单数据。
10.2、非受控组件
非受控组件就是输入类元素不通过state
来维护数据,使用ref
从DOM
节点中获取数据:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.userNameNode = React.createRef()
}
handleSubmit(e) {
e.preventDefault()
console.log(`您的用户名是:${this.userNameNode.current.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<input type="text" name="username" ref={this.userNameNode} />
<button type="submit">提交</button>
</form>
)
}
}
ReactDOM.render(<MyComponent />, document.getElementById('root'))
十一、组件生命周期
React中的每个组件都包含组件生命周期方法,在运行过程中特定的阶段执行这些方法。
组件的生命周期分为四类:
- 挂载(初始化)阶段。
- 更新阶段。
- 销毁阶段。
- 错误阶段。
因为React
版本问题导致一些生命周期方法被弃用或者被修改,下面会根据版本的不同来介绍组件的生命周期。
11.1、React 16.3前的生命周期
先通过一张图片总览一下旧的生命周期:
挂载阶段
当组件实例被创建并插入DOM
中时,组件生命周期调用顺序如下:
constructor()
:构造器调用。componentWillMount()
:组件即将挂载之前调用。render()
:初始化渲染。componentDidMount()
:在组件挂载后(插入DOM
树中)立即调用。
class LifeCycle extends React.Component {
constructor(props) {
super(props)
console.log('1. construct')
}
componentWillMount() {
console.log('2. componentWillMount')
}
render() {
console.log('3. render')
return <div>React 旧生命周期</div>
}
componentDidMount() {
console.log('4. componentDidMount')
}
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))
组件挂载阶段会按照下面依次输出:
1. construct
2. componentWillMount
3. render
4. componentDidMount
更新阶段
组件的更新阶段就是setState()
使React
重新渲染组件并且把组件的变化应用到DOM
元素上的过程。React
也提供了一系列的生命周期函数可以让我们在这个组件更新的过程执行一些操作。当组件的state
发生变化时会触发更新。组件更新的生命周期调用顺序如下:
shouldComponentUpdate()
: 当props
或state
发生变化时会调用。componentWillUpdate()
:组件开始重新渲染之前调用。render()
:更新后重新渲染。componentDidUpdate()
:组件重新渲染并且变更到真实的DOM
以后调用。
需要注意shouldComponentUpdate()
可以通过这个方法控制组件是否重新渲染。默认值返回true
,就是每次发生变化组件都会重新渲染。如果编写这个函数后返回false
组件就不会重新渲染。这个生命周期在React
性能优化上非常有用(后面会说)。
class LifeCycle extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
shouldComponentUpdate() {
console.log('1. shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('2. componentWillUpdate')
}
render() {
console.log('3. render')
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
componentDidUpdate() {
console.log('4. componentDidUpdate')
}
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))
当触发handleClick
点击事件时会按照下面依次输出:
1. shouldComponentUpdate
2. componentWillUpdate
3. render
4. componentDidUpdate
forceUpdate()
我们还可以使用强制更新forceUpdate()
让组件重新渲染。强制更新会跳过shouldComponentUpdate()
。一般情况下应该避免使用 forceUpdate()
。
props变化的更新
如果有一个父子组件,通过props
传递参数,子组件的生命周期更新会增加一个componentWillReceiveProps()
生命周期。它的作用是子组件从父组件接收到新的props
之前调用。
componentWillReceiveProps(nextProps)
接收一个参数,这个参数是更新后的props
对象:
// 父组件
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
changeName() {
this.setState({ name: '李四' })
}
render() {
return (
<div>
<Child name={this.state.name} />
<button onClick={this.changeName.bind(this)}>修改名字</button>
</div>
)
}
}
// 子组件
class Child extends React.Component {
componentWillReceiveProps() {
console.log('1. componentWillReceiveProps')
}
shouldComponentUpdate() {
console.log('2. shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('3. componentWillUpdate')
}
render() {
console.log('4. render')
return <div>我是子组件,接收到的名字是{this.props.name}</div>
}
componentDidUpdate() {
console.log('5. componentDidUpdate')
}
}
ReactDOM.render(<Parent />, document.getElementById('root'))
当触发点击事件时会按照下面依次输出:
1. componentWillReceiveProps
2. shouldComponentUpdate
3. componentWillUpdate
4. render
5. componentDidUpdate
初始化传递
props
的时候不会执行componentWillReceiveProps()
。如果父组件导致组件重新渲染,即使props
没有更改,也会调用此方法。
卸载阶段
当组件从DOM
中移除时会调用下面组件生命周期:
componentWillUnmount()
:在组件卸载及销毁之前直接调用。
class LifeCycle extends React.Component {
handleClick() {
// 卸载
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
render() {
return <button onClick={this.handleClick.bind(this)}>组件卸载</button>
}
componentWillUnmount() {
console.log('1. componentWillUnmount')
}
}
const App = () => <LifeCycle />
export default App
当触发点击事件handleClick
时会输出:
1. componentWillUnmount
11.2、React v16.3后的新的生命周期
先通过一张图片总览一下新的生命周期:
新版本的生命周期有三个方法改了名字:
componentWillMount()
改为了UNSAFE_componentWillMount()
componentWillReceiveProps()
改为了UNSAFE_componentWillReceiveProps()
componentWillUpdate
改为了UNSAFE_componentWillUpdate()
新版本后必须要添加
UNSAFE_
前缀,否则可能没办法使用这三个生命钩子。
新版本新增了两个静态方法生命周期:
static getDerivedStateFromProps()
static getSnapshotBeforeUpdate()
static getDerivedStateFromProps()
getDerivedStateFromProps()
的意思是从props
中获取state
,将传入的props
映射到state
上面。
getDerivedStateFromProps()
在初始挂载及后续更新时都会被调用。
在使用getDerivedStateFromProps()
的时候必须要初始化state
。它应返回一个对象来更新state
,如果返回null
则不更新任何内容。
getDerivedStateFromProps(nextProps,nextState)
接收两个参数:
nextProps
:最新的props
nextState
: 最新的state
当返回null
的时候功能不会受影响:
class LifeCycle extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
static getDerivedStateFromProps(props, state) {
return null
}
render() {
return <div>{this.state.count}</div>
}
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))
把props
当作状态对象返回的时候,组件内的状态值在任何时候都取决于props
的值:
class LifeCycle extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
//组件内的状态值取决于props,设置值无效
handleClick() {
this.setState({ count: this.state.count + 1 })
}
static getDerivedStateFromProps(props, state) {
return props // 返回props:{count: 100}
}
render() {
return <div onClick={this.handleClick.bind(this)}>count:{this.state.count}</div>
}
}
ReactDOM.render(<LifeCycle count={100} />, document.getElementById('root'))
传入props
的count={100}
,通过getDerivedStateFromProps
返回,代替了原始的this.state = { count: 0 }
:
static getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到DOM
节点)之前调用,在更新之前获取快照。
必须返回一个Snapshot value
(任意值)或返回null
。返回的值将作为参数传递给 componentDidUpdate()
钩子函数。
使用
getSnapshotBeforeUpdate()
的时候也必须要定义componentDidUpdate()
钩子函数。
componentDidUpdate(prevProps, prevState, snapShotValue)
函数接收三个参数:
prevProps
:上一个props
prevState
:上一个state
snapShotValue
:getSnapshotBeforeUpdate()
返回的快照值。
getSnapshotBeforeUpdate()
返回的值Snapshot value
通过componentDidUpdate()
方法接收:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick(e) {
this.setState({ count: this.state.count + 1 })
}
// 在更新之前获取快照
getSnapshotBeforeUpdate() {
return '张三'
}
componentDidUpdate(prevProps, prevState, snapShotValue) {
console.log(snapShotValue) // 张三
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
}
ReactDOM.render(<MyComponent count={100} />, document.getElementById('root'))
可以使用getSnapshotBeforeUpdate()
实现一个列表高度持续增加的时候始终定位当前位置的案例:
class News extends React.Component {
constructor(props) {
super(props)
this.state = { news: [] }
this.newWrapRef = React.createRef()
}
componentDidMount() {
// 每秒生成一条新闻
setInterval(() => {
const { news } = this.state
const oneNew = '新闻' + news.length
this.setState({ news: [oneNew, ...news] })
}, 1000)
}
render() {
return (
<ul ref={this.newWrapRef} style={{ height: '180px', border: '1px solid red', overflow: 'auto' }}>
{this.state.news.map((item, index) => {
return (
<li key={index} style={{ height: '30px' }}>
{item}
</li>
)
})}
</ul>
)
}
getSnapshotBeforeUpdate() {
// 获取重新渲染之前容器的的scrollHeight
return this.newWrapRef.current.scrollHeight
}
componentDidUpdate(prevProps, prevState, snapshotValue) {
// 新的scrollHeight减去上一次的scrollHeight
const newScrollHeight = this.newWrapRef.current.scrollHeight - snapshotValue
// 重新设置最新的scrollTop
this.newWrapRef.current.scrollTop += newScrollHeight
}
}
ReactDOM.render(<News count={100} />, document.getElementById('root'))
挂载阶段
当组件实例被创建并插入DOM
中时,组件生命周期调用顺序如下:
constructor()
:构造器调用。getDerivedStateFromProps()
:在初始挂载及后续更新时调用。render()
:初始化渲染。componentDidMount()
:在组件挂载后(插入DOM
树中)立即调用。
class LifeCyCle extends React.Component {
constructor(props) {
super(props)
this.state = {}
console.log('1. constructor')
}
static getDerivedStateFromProps() {
console.log('2. getDerivedStateFromProps')
return null // 必须有返回值
}
render() {
console.log('3. render')
return <div>React 新生命周期</div>
}
componentDidMount() {
console.log('4. componentDidMount')
}
}
ReactDOM.render(<LifeCyCle />, document.getElementById('root'))
组件挂载阶段会按照下面依次输出:
1. constructor
2. getDerivedStateFromProps
3. render
4. componentDidMount
更新阶段
组件更新的生命周期调用顺序如下:
getDerivedStateFromProps()
:初始挂载及后续更新时都会被调用。shouldComponentUpdate()
: 当props
或state
发生变化时会调用。render()
:状态更新后渲染。getSnapshotBeforeUpdate()
:在最近一次渲染输出(提交到 DOM 节点)之前调用。componentDidUpdate()
:组件重新渲染并且变更到真实的DOM
以后调用。
class LifeCycle extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
static getDerivedStateFromProps() {
console.log('1. getDerivedStateFromProps')
return null
}
shouldComponentUpdate() {
console.log('2. shouldComponentUpdate')
return true
}
render() {
console.log('3. render')
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>Click me</button>
</div>
)
}
getSnapshotBeforeUpdate() {
console.log('4. getSnapshotBeforeUpdate')
return null
}
componentDidUpdate() {
console.log('5. componentDidUpdate')
}
}
ReactDOM.render(<LifeCycle />, document.getElementById('root'))
当触发点击事件handleClick
时会按照下面依次输出:
1. getDerivedStateFromProps
2. shouldComponentUpdate
3. render
4. getSnapshotBeforeUpdate
5. componentDidUpdate
卸载阶段
新老版本的销毁阶段的生命周期没有发生变化。
十二、错误边界
部分UI
的JavaScript
错误不应该导致整个应用崩溃,为了解决这个问题,React 16
引入了一个新的概念——错误边界(Error Boundaries
)。错误边界是一种React
组件,它可以用来捕获后代组件错误,渲染备用页面。它只能捕获后代组件生命周期产生的错误。
如果子组件发生错误,父组件需要通过getDerivedStateFromError()
渲染备用UI
,使用 componentDidCatch()
打印错误信息。下面是一个父子组件的案例:
import React from 'react'
// 子组件
class Child extends React.Component {
constructor(props) {
super(props)
this.state = { userList: 'abc' }
}
render() {
return (
<div>
<h2>我是Child组件</h2>
{/* Child组件会报错 */}
{this.state.userList.map((item) => {
return <span key={item.id}>{item.name}</span>
})}
</div>
)
}
}
// 父组件
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
// 如果子组件出现错误,会触发getDerivedStateFromError的调用,并在参数里携带错误信息
static getDerivedStateFromError(error) {
console.log(error)
return { hasError: error } // 需要返回错误信息
}
componentDidCatch() {
// 可以将错误日志上报给服务器
}
render() {
return (
<div>
<h2>我是Parent组件</h2>
{/* 开发环境下同样会抛出错误,生产环境会友好提示 */}
{this.state.hasError ? <span>出现错误</span> : <Child />}
</div>
)
}
}
十三、PureComponent组件优化
当父组件数据修改重新渲染的时候,子组件没有用到父组件的任何数据时也会重新渲染:
import React from 'react'
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
handleChangeName(e) {
this.setState({ name: '李四' })
}
render() {
console.log('Parent render')
return (
<div>
<button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
<Child />
</div>
)
}
}
class Child extends React.Component {
render() {
console.log('Child render')
return <div>我是Child组件</div>
}
}
当点击按钮触发handleChangeName()
方法时,会输出Child render
。说明子组件没有用到父组件的任何数据会重新渲染。
还有一点需要注意的是:当调用
this.setState({})
什么值都不传的时候,组件也会重新渲染。
我们可以使用shouldComponentUpdate()
来进行优化,父组件比较state
,子组件比较props
来确定是否重新渲染:
import React from 'react'
// 父组件
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
handleChangeName(e) {
this.setState({ name: '李四' })
}
/**
* @param {*} nextProps 最新的props
* @param {*} nextState 最新的state
*/
shouldComponentUpdate(nextProps, nextState) {
// state的name属性如果没有变化则不需要重新渲染
return !(this.state.name === nextState.name)
}
render() {
console.log('Parent render')
return (
<div>
<button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
<Child name={this.state.name} />
</div>
)
}
}
// 子组件
class Child extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// props的name属性如果没有变化则不需要重新渲染
return !(this.props.name === nextProps.name)
}
render() {
console.log('Child render')
return <div>我是Child组件:{this.props.name}</div>
}
}
上面代码可以看出来,如果对象有多个属性,还需要每个属性对比。项目开发的时候这种编写方式是非常麻烦的。React
提供了React.PureComponent
。React.PureComponent
中以浅层次对比prop
和state
的方式来实现了shouldComponentUpdate
函数。
import React from 'react'
class Parent extends React.PureComponent {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
handleChangeName(e) {
this.setState({ name: '李四' })
}
render() {
console.log('Parent render')
return (
<div>
<button onClick={this.handleChangeName.bind(this)}>修改姓名</button>
<Child name={this.state.name} />
</div>
)
}
}
class Child extends React.PureComponent {
render() {
console.log('Child render')
return <div>我是Child组件:{this.props.name}</div>
}
}
React.PureComponent
中的 shouldComponentUpdate()
仅作对象的浅层比较。
如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的对比结果。
仅在props
和 state
较为简单时,才使用 React.PureComponent
,或者在深层数据结构发生变化时调用forceUpdate()
来确保组件被正确地更新。
总之不要直接修改数据,要重新生成新数据。
十四、Render Props
具有render prop
的组件接受一个返回React
元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑。
首先先看一个需求,如果组件标签里面写入一些内容,标签体内容需要通过组件的props.children
来获取。
import React from 'react'
// 父组件
class Parent extends React.Component {
render() {
return (
<div>
<p>我是Parent组件</p>
<Child>Hello!</Child>
</div>
)
}
}
// 子组件
class Child extends React.Component {
render() {
return (
<div>
<p>我是Child组件</p>
{this.props.children}
</div>
)
}
}
Child
组件就会输出Hello
:
假如说还有一个需求就是像下面一样,组件里嵌套组件形成父子关系:
class App extends React.Component {
render() {
return (
<B>
<C></C>
</B>
)
}
}
要把B
组件的state
数据要传递给C
组件,就可以使用Render Props
的方式来编写,在B
组件里使用this.props.render
来调用我们编写的render
的函数:
import React from 'react'
class App extends React.Component {
render() {
return (
<div>
<p>我是App组件</p>
{/* 编写render的回调函数,并接收数据 */}
<B render={(name) => <C name={name} />} />
</div>
)
}
}
// 子组件
class B extends React.Component {
constructor(props) {
super(props)
this.state = { name: '张三' }
}
render() {
return (
<div>
<p>我是B组件</p>
{/* 调用props的 render() 方法,并传递数据 */}
{this.props.render(this.state.name)}
</div>
)
}
}
// 孙组件
class C extends React.Component {
render() {
return (
<div>
<p>我是C组件,我从B组件获取的name是:{this.props.name}</p>
</div>
)
}
}
通过Render Props
的方式,即使是嵌套关系,C
组件里通过props
也能获取B
组件的数据:
Render Props
类似于Vue
的插槽技术。
十五、React脚手架
全局安装官方推荐脚手架工具create-react-app
,类似于Vue
的vue-cli
:
npm install -g create-react-app
// 使用create-react-app新建项目
create-react-app my-react-app
安装成功之后,npm start
或者 yarn start
就可以启动项目了。
package.json
安装的React
依赖如下:
从package.json
的dependencies
可以看出来脚手架工具默认安装了React
需要的依赖。下面就介绍这些主要核心依赖的作用:
react
:是React
的核心库。react-dom
:负责Web
页面的DOM
操作。react-scripts
:生成项目所有的依赖。例如babel
,css-loader
,webpack
等从开发到打包前端工程化所需要的react-scripts
都帮我们做好了。
十六、todoList功能
下面我们可以使用create-react-app
脚手架新建一个项目来做一个todoList
的功能:
由于还没有接触到状态管理,我们可以兄弟组件互相传递数据,通过父组件来中转实现功能。首先先定义下面四个组件:
Header
组件。Item
组件,代表列表中的每一项。List
列表组件。Footer
组件。
App.js
/* app.css */
#root {
display: flex;
justify-content: center;
}
.todo-container {
margin-top: 20px;
min-height: 240px;
min-width: 420px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 20px;
box-sizing: border-box;
}
// App.js
import React from 'react'
import Header from './components/Header'
import List from './components/List'
import './app.css'
import Footer from './components/footer'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [{ id: 1, name: 'JS', isChecked: false }]
}
}
// 添加todo
addTodo(todo) {
const newList = [todo, ...this.state.list]
this.setState({ list: newList })
console.log(todo)
}
// 修改todo
editTodo(item, isChecked) {
const newTodo = this.state.list.map((el) => {
return el.id === item.id ? { ...el, isChecked } : el
})
this.setState({ list: newTodo })
}
// 删除todo
deleteTodo(item) {
const newTodo = this.state.list.filter((el) => el.id !== item.id)
this.setState({ list: newTodo })
}
// 删除所有选中
deleteChecked() {
const newTodos = this.state.list.filter((el) => !el.isChecked)
this.setState({ list: newTodos })
}
// 删除所有元素
deleteCheckedAll() {
this.setState({ list: [] })
}
// 全选和反选
changeTodoAll(isChecked) {
const newTodos = this.state.list.map((el) => {
return { ...el, isChecked }
})
this.setState({ list: newTodos })
}
render() {
return (
<div className="todo-container">
<Header addTodo={this.addTodo.bind(this)} />
<List list={this.state.list} editTodo={this.editTodo.bind(this)} deleteTodo={this.deleteTodo.bind(this)} />
<Footer list={this.state.list} deleteChecked={this.deleteChecked.bind(this)} deleteCheckedAll={this.deleteCheckedAll.bind(this)} changeTodoAll={this.changeTodoAll.bind(this)} />
</div>
)
}
}
export default App
Header组件
/* components/Header/index.css */
.header {
margin-bottom: 20px;
}
.header input {
box-sizing: border-box;
padding: 8px;
width: 100%;
border: 1px solid #ccc;
}
.header input:focus {
outline: none;
border-color: #409eff;
}
// components/Header/index.jsx
import React from 'react'
import './index.css'
export default class Header extends React.Component {
// 回车添加元素
handleKeyUp(e) {
if (e.keyCode !== 13) return
const val = e.target.value
if (val.trim() === '') return
const todo = {
id: Math.random(), // 这里简单使用随机数,实际开发中不推荐
name: val,
isChecked: false
}
// 调用父组件props传入的函数添加数据
this.props.addTodo(todo)
e.target.value = ''
}
render() {
return (
<div className="header">
{/* 输入框 */}
<input onKeyUp={this.handleKeyUp.bind(this)} type="text" autoComplete="off" name="myInput" placeholder="请输入任务名称,按回车确认" />
</div>
)
}
}
List组件
/* components/List/index.css */
.list-container {
border: 1px solid #ccc;
padding: 20px;
}
// components/List/index.jsx
import React from 'react'
import Item from '../Item'
import './index.css'
export default class List extends React.Component {
render() {
const { list, editTodo, deleteTodo } = this.props
return (
<div className="list-container">
{list.map((item) => {
/* 使用Item组件 */
return <Item key={item.id} item={item} editTodo={editTodo} deleteTodo={deleteTodo} />
})}
</div>
)
}
}
Item组件
// components/Item/index.jsx
import React from 'react'
export default class Item extends React.Component {
constructor(props) {
super(props)
this.state = {
isShowBtn: false
}
}
// 鼠标移入显示
handleMouseEnter(e) {
this.setState({ isShowBtn: true })
}
// 鼠标移除隐藏
handleMouseLeave(e) {
this.setState({ isShowBtn: false })
}
handleChange(e, item) {
// 通知父组件修改状态
this.props.editTodo(item, e.target.checked)
}
// 删除
handleDelete(e, item) {
this.props.deleteTodo(item)
}
render() {
const { item } = this.props
const { isShowBtn } = this.state
return (
<div
onMouseEnter={this.handleMouseEnter.bind(this)}
onMouseLeave={this.handleMouseLeave.bind(this)}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0' }}
>
<div>
<input type="checkbox" id={item.id} checked={item.isChecked} onChange={(e) => this.handleChange.call(this, e, item)} />
<label style={{ paddingLeft: '10px' }} htmlFor={item.id}>
{item.name}
</label>
</div>
<button onClick={(e) => this.handleDelete.call(this, e, item)} style={{ display: isShowBtn ? 'block' : 'none' }}>
删除
</button>
</div>
)
}
}
Footer组件
// components/Footer/index.jsx
import React from 'react'
export default class Footer extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
handleChange(e) {
// 通知父元素修改
this.props.changeTodoAll(e.target.checked)
}
render() {
const { list, deleteChecked, deleteCheckedAll } = this.props
// 已经选中的列表
const checkeds = list.filter((item) => item.isChecked)
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '20px' }}>
<div>
<input type="checkbox" checked={checkeds.length === list.length && list.length !== 0} onChange={this.handleChange.bind(this)} name="all" />
<span>
已完成 {checkeds.length} / 全部 {list.length}
</span>
</div>
<div>
<button onClick={deleteChecked} style={{ marginRight: '5px' }}>
删除所有选中
</button>
<button onClick={deleteCheckedAll}>删除所有</button>
</div>
</div>
)
}
}
十七、Redux
Redux
专门用来做状态管理的库(不是React
的官方库)。它将整个应用状态存储在store
中。组件可以派发(dispatch
)行为(action
)给store
。其它组件可以通过订阅store
中的状态(state
)来刷新自己的视图:
可以把整个Redux
当做一个饭店,React Components
是一桌客人,Action Creators
是服务员,Store
是大堂经理,Reducers
是后厨负责做饭。客人通过点餐给服务员,服务员通知大堂经理,大唐经理通知后厨来做饭,做好饭之后通过大堂经理送到客人餐桌上:
action
action
表示动作对象。它包含两个属性:
type
:标识属性。值为字符串并且唯一。data
:传递的数据。
store
store
存储state
(数据集合)。并将state,action,reducer
联系在一起的对象。整个应用只有一个store
对象。
reducer
reducer
用于初始化状态和加工状态。加工状态时,根据旧的state
和action
,产生新的state
纯函数。
安装:
npm install --save redux
下面是一个加减的小案例:
首先创建redux/store.js
:
// redux/store.js
// 引入createStore,用于创建redux中最核心的store对象
import { createStore } from 'redux'
// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1
/**
* 创建action
*/
export const actions = {
increment(data) {
return { type: INCREMENT, data }
},
decrement(data) {
return { type: DECREMENT, data }
}
}
/**
* 创建reducer
* @param {*} preState 上一个状态
* @param {*} action 行为对象
* @returns
*/
function countReducer(preState = 0, action) {
/**
* 从action对象中获取type和data
* type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
*/
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
// 创建store,传入对应的countReducer
const store = createStore(countReducer)
export default store
然后在App.js
里使用:
import React from 'react'
// 引入store和Action
import store, { actions } from './redux/store'
class App extends React.Component {
/**
* 通过dispatch调用action使状态改变后redux默认不会去渲染页面,需要通过store.subscribe()监听状态变化
*/
componentDidMount() {
// 监听redux中状态的变化,只要变化,就调用setState()重新渲染页面
store.subscribe(() => {
this.setState({})
})
}
increment() {
store.dispatch(actions.increment(1))
}
decrement() {
store.dispatch(actions.decrement(1))
}
render() {
return (
<div>
<h1>{store.getState()}</h1>
<button onClick={this.increment.bind(this)}>加1</button>
<button onClick={this.decrement.bind(this)}>减1</button>
</div>
)
}
}
export default App
17.1、异步action
action
不仅可以返回对象,还可以返回函数。如果是对象就是同步action
,如果是函数就是异步action
。
如果要在Action
中使用异步方法.需要使用到redux-thunk
中间件:
// 安装
npm install -S redux-thunk
安装完成之后,要使用使用redux
的applymiddleware()
中间件作为createStore()
的第二个参数:
// 引入react-thunk,用于支持异步action
import thunk from 'redux-thunk'
// 传入对应的countReducer和redux-thunk
const store = createStore(countReducer, applyMiddleware(thunk))
下面是一个异步加减的小案例:
首先创建redux/store.js
:
// 引入createStore,用于创建redux中最核心的store对象
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1
/**
* 创建action
*/
export const actions = {
increment(data) {
return { type: INCREMENT, data }
},
decrement(data) {
return { type: DECREMENT, data }
},
incrementAsync(data) {
return (dispatch) => {
setTimeout(() => {
dispatch(this.increment(data)) // 异步action中一般都会调用同步action
}, 500)
}
},
decrementAsync(data) {
return (dispatch) => {
setTimeout(() => {
dispatch(this.decrement(data)) // 异步action中一般都会调用同步action
}, 500)
}
}
}
/**
* 创建reducer
* @param {*} preState 上一个状态
* @param {*} action 行为对象
* @returns
*/
function countReducer(preState = 0, action) {
/**
* 从action对象中获取type和data
* type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
*/
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
// 创建store,传入对应的countReducer和applyMiddleware
const store = createStore(countReducer, applyMiddleware(thunk))
export default store
然后在App.js
里使用:
import React from 'react'
// 引入store和Action
import store, { actions } from './redux/store'
class App extends React.Component {
/**
* 通过dispatch调用action使状态改变后redux默认不会去渲染页面,需要通过store.subscribe()监听状态变化
*/
componentDidMount() {
// 监听redux中状态的变化,只要变化,就调用setState()重新渲染页面
store.subscribe(() => {
this.setState({})
})
}
incrementAsync() {
store.dispatch(actions.incrementAsync(1))
}
decrementAsync() {
store.dispatch(actions.decrementAsync(1))
}
render() {
return (
<div>
<h1>{store.getState()}</h1>
<button onClick={this.incrementAsync.bind(this)}>异步加1</button>
<button onClick={this.decrementAsync.bind(this)}>异步减1</button>
</div>
)
}
}
export default App
十八、react-redux
React
官方出品的react-redux
,可以在React
更加简单和方便的使用redux
。
react-redux
内部已经自动实现监听数据变化重新渲染页面的功能,不用在使用store.subscribe()
来监听数据的变化来手动编码实现渲染。首先进行安装:
# If you use npm:
npm install react-redux
# Or if you use Yarn:
yarn add react-redux
- 所有的
UI
组件都应该包裹一个容器组件,他们是父子关系。 - 容器组件是真正和
redux
交互的,可以随意的使用redux
的api
。 - UI组件中不能使用任何
redux
的api
。 - 容器组件会传给组件
redux
中所保存的状态和用于操作状态的方法,通过props
传递。
connect()
方法用于连接UI
组件和redux
。
connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
接收4
个参数,这里只介绍前两个参数:
-
mapStateToProps
:值为Function
,表示redux
中保存的状态,返回一个对象,对象中的key
就是传递给UI
组件props
的key
,value
就是传递给UI
组件props
的value
。 -
mapDispatchToProps
:值为Function | Object
,表示redux
中操作状态的方法,返回一个对象,对象中的key
就是传递给UI
组件props
的key
,value
就是传递给UI
组件props
的value
。 -
mergeProps
:值为Function
。 -
options
:值为Object
。
下面是一个使用react-redux
加减的小案例:
首先首先创建redux/store.js
:
// 引入createStore,用于创建redux中最核心的store对象
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
// 定义type类型的常量值
export const INCREMENT = 'increment' // 加1
export const DECREMENT = 'decrement' // 减1
/**
* 创建action
*/
export const actions = {
increment(data) {
return { type: INCREMENT, data }
},
decrement(data) {
return { type: DECREMENT, data }
}
}
/**
* 创建reducer
* @param {*} preState 上一个状态
* @param {*} action 行为对象
* @returns
*/
function countReducer(preState = 0, action) {
/**
* 从action对象中获取type和data
* type初始化默认值是类似@@redux/INITc.q.3.o.k.g的一个随机值
*/
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
const store = createStore(countReducer, applyMiddleware(thunk))
export default store
新建一个Count
容器组件containers/Count.jsx
:
// 引入Count的UI组件
import CountUI from '../components/Count'
// 引入actions
import { actions } from '../redux/store'
// 引入connect连接UI组件和redux
import { connect } from 'react-redux'
/**
* @param {*} state: redux保存的状态,由react-redux自动传入
*/
const mapStateToProps = function (state) {
return { count: state }
}
/**
* @param {*} dispatch redux的dispatch方法,由react-redux自动传入
*/
const mapDispatchToProps = function (dispatch) {
return {
incr: (data) => dispatch(actions.increment(data)),
decr: (data) => dispatch(actions.decrement(data))
}
}
// 使用connect()()创建并暴露一个Count容器组件
export default connect(mapStateToProps, mapDispatchToProps)(CountUI)
需要注意的一点是mapDispatchToProps
的简写方式还可以传入一个对象,react-redux
会自动帮我们调用dispatch()
:
const mapDispatchToProps = {
incr: actions.increment,
decr: actions.decrement
}
然后新建UI
组件components/Count.jsx
:
import React from 'react'
// 引入store和Action
class Count extends React.Component {
componentDidMount() {
console.log(this.props) // {store: {…}, count: 0, incr: ƒ, decr: ƒ}
}
increment() {
console.log(this.props)
this.props.incr(1)
}
decrement() {
this.props.decr(1)
}
render() {
return (
<div>
<h1>{this.props.count}</h1>
<button onClick={this.increment.bind(this)}>加1</button>
<button onClick={this.decrement.bind(this)}>减1</button>
</div>
)
}
}
export default Count
在App.js
里使用,需要注意的是store
需要通过props
传递给容器组件:
import React from 'react'
// 引入store
import store from './redux/store'
// 引入Count容器
import Count from './containers/Count'
class App extends React.Component {
render() {
// 给容器组件传递store
return <Count store={store} />
}
}
export default App
通过使用react-redux
,我们把与redux
状态交互的代码全部写到了容器组件内,通过connect()
关联他们,最后UI
组件只需要通过props
来调用。
18.1、Provider
Provider
可以让所有的组件都能收到store
并作为props
绑定到组件上。
假如说我们要使用组件很多次,每个组件都要传递store
,就像下面一样:
import store from './redux/store'
class App extends React.Component {
render() {
return (
<div>
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
<Count store={store} />
</div>
)
}
}
可以把store
传递给Provider
,store
会自动传递给容器组件:
import { Provider } from 'react-redux'
import store from './redux/store'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
)
容器组件就不用手动传入store
:
import store from './redux/store'
class App extends React.Component {
render() {
return (
<div>
<Count />
<Count />
<Count />
<Count />
<Count />
<Count />
<Count />
<Count />
</div>
)
}
}
18.2、整合UI组件和容器组件
如果说项目里有100
个UI
组件需要用到容器组件,那么我们还要新建100
个容器组件。这样会使文件成倍增长。我们可以把UI
组件和容器组件整合在一个文件内:
// Container/Count.jsx
import { actions } from '../redux/store'
import { connect } from 'react-redux'
import React from 'react'
// UI组件
class Count extends React.Component {
componentDidMount() {
console.log(this.props) // {store: {…}, count: 0, incr: ƒ, decr: ƒ}
}
increment() {
console.log(this.props)
this.props.incr(1)
}
decrement() {
this.props.decr(1)
}
render() {
return (
<div>
<h1>{this.props.count}</h1>
<button onClick={this.increment.bind(this)}>加1</button>
<button onClick={this.decrement.bind(this)}>减1</button>
</div>
)
}
}
// 导出容器组件
export default connect(
// mapStateToProps
(state) => {
return { count: state }
},
// mapDispatchToProps
{
incr: actions.increment,
decr: actions.decrement
}
)(Count)
18.3、组件间数据共享
之前介绍的redux
功能都比较简单。redux
里的数据只是一个单一的基本数据类型。实际开发的时候会使用到很多数据,我们需要合并redux
的数据,需要用对象{}
存储。可以使用combineReducers()
方法来实现合并:
// 引入combineReducers
import { combineReducers } from 'redux'
/**
*
* 通过combineReducers()方法汇总所有的reducer,变为一个reducer
* 返回的结果合并成一个 state 对象。如:{a: aReducer,b: bReducer}
*/
const allReducer = combineReducers({
a: aReducer,
b: bReducer
})
我们还需要更清晰的功能目录划分,如下:
├── redux
├── actions // 所有的action
└── personAction.js // 自定义person action
└── bookAction.js // 自定义book action
├── reducers // 所有的reducers
└── index.js // 汇总所有的reducer
└── countReducer.js // 自定义person reducer
└── bookReducer.js // 自定义book reducer
├── constant.js // 常量
├── store.js // store
下面是一个Person
和Book
组件实现一个人可以有多本书的案例:
编写constant
/**
* type类型常量值
*/
export const ADD_PERSON = 'addPerson' // 添加一个人
export const ADD_BOOK = 'addBook' // 添加一本书
编写Action
:
// redux/actions/bookAction
import { ADD_BOOK } from '../constant'
export const addBookAction = (data) => {
return { type: ADD_BOOK, data }
}
// redux/actions/PersonAction
import { ADD_PERSON } from '../constant'
export const addPersonAction = (data) => {
return { type: ADD_PERSON, data }
}
编写reducer
:
// redux/reducers/bookReducer
import { ADD_BOOK } from '../constant.js'
export default function bookReducer(preState = [], action) {
const { type, data } = action
switch (type) {
case ADD_BOOK:
return [data, ...preState]
default:
return preState
}
}
// redux/reducers/personReducer
import { ADD_PERSON } from '../constant'
// 初始化state
const initState = [{ id: '001', name: '张三' }]
function personReducer(preState = initState, { type, data }) {
switch (type) {
case ADD_PERSON:
return [data, ...preState]
default:
return preState
}
}
export default personReducer
// redux/reducers/index.js
/* 汇总所有的reducer */
import { combineReducers } from 'redux'
// 引入bookReducer
import bookReducer from './bookReducer'
// 引入personReducer
import personReducer from './personReducer'
/**
*
* 通过combineReducers()方法汇总所有的reducer,变为一个reducer
* 返回的结果合并成一个 state 对象。 如: {a: xxx,b: xxx}
*/
const allReducer = combineReducers({
storeBook: bookReducer,
storePerson: personReducer
})
export default allReducer
编写store
:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import allReducer from '../redux/reducers'
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
export default store
编写Person
和Book
组件:
import React from 'react'
import { connect } from 'react-redux'
import { addBookAction } from '../redux/actions/bookAction'
class Book extends React.Component {
handleKeyUp(e) {
if (e.keyCode !== 13) return
const val = e.target.value
if (!val.trim() === '') return
this.props.addBookAction({ id: Date.now(), name: val })
e.target.value = ''
}
render() {
return (
<div>
<h1>我是Book组件</h1>
<input type="text" name="book" placeholder="添加一本书" onKeyUp={this.handleKeyUp.bind(this)} />
</div>
)
}
}
export default connect(
(state) => {
return { storeBook: state.storeBook }
},
{
addBookAction: addBookAction
}
)(Book)
import React from 'react'
import { connect } from 'react-redux'
import { addPersonAction } from '../redux/actions/personAction'
class Person extends React.Component {
render() {
return (
<div style={{ borderBottom: '1px solid #ccc' }}>
<h1>我是Person组件</h1>
{this.props.storePerson[0].name}的技术书有:
{this.props.storeBook.map(({ id, name }) => (
<span key={id}>{name},</span>
))}
</div>
)
}
}
export default connect(
(state) => {
return { storeBook: state.storeBook, storePerson: state.storePerson }
},
{
addPersonAction: addPersonAction
}
)(Person)
最后在App.js
里使用:
import React from 'react'
import Book from './components/Book'
import Person from './components/Person'
class App extends React.Component {
render() {
return (
<div>
<Person />
<Book />
</div>
)
}
}
export default App
18.4、reducer
纯函数
redux
的reducer
函数必须是一个纯函数。只要是同样的输入(实参),必须得到同样的输出(返回)。
- 不能改写参数数据。
- 不会产生任何副作用,例如网络请求。
- 不能使用
Date.now()
或者Math.random()
等不纯的方法。
因为比较两个javascript
对象中所有的属性是否完全相同,唯一的办法就是深比较。然而,深比较在真实的应用中非常耗性能的,需要比较的次数特别多,所以一个有效的解决方案就是做一个规定,当无论发生任何变化时,开发者都要返回一个新的对象,没有变化时,开发者返回旧的对象,这也就是redux
为什么要把reducer
设计成纯函数的原因。
18.5、redux开发者工具
redux
开发者工具不仅需要安装Chrome
插件,还要在项目里配置:
-
安装
Chrome
插件:Redux DevTools
-
项目里需要安装
redux-devtools-extension
:
// 安装
npm install redux-devtools-extension
- 然后在项目里
store.js
里配置:
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(allReducer,composeWithDevTools( applyMiddleware(thunk) ))
十九、Hook
Hook
是React 16.8
的新增特性。它可以在不编写class
的情况下使用state
以及其他的 React
特性。下面会介绍一下比较常用的Hook
。
19.1、useState()
State hook
让函数组件也有state
状态,并进行状态数据的读写操作。语法:
const [xxx, setXXX] = React.useState(initValue)
initValue
在第一次初始化的值在内部做缓存。返回值包含两个元素的数组,第一个为内部状态当前值,第二个为更新状态值的函数。
setXXX()
有两种写法:
setxxx(newValue)
:参数为非函效值,直接指定新的状态值,内部覆盖原来的状态值。setxxx(value=> newvalue)
:参数为函数,接收之前的状态值,返回新的状态值,内部覆盖原来的状态值。
import React, { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
function addCount() {
// 第一种写法
setCount(count + 1)
// 第二种写法
setCount((count) => {
return count + 1
})
}
return (
<div>
<p>count值为:{count}</p>
<button onClick={addCount}>点击+2</button>
</div>
)
}
19.2、Effect()
Effect Hook
可以在函数组件中使用生命周期钩子函数。可以把 useEffect Hook
看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
Effect()
传入一个回调函数,它在第一次渲染之后和每次更新之后都会执行:
import { useEffect } from 'react'
// 类似于componentDidMount和componentDidUpdate
useEffect(() => {
console.log('success')
})
Effect()
还可以传入第二个参数。它是一个数组指定需要监听的依赖项。有依赖项发生变化,才会重新渲染。默认不传表示监听全部。空数组[]
表示都不监听。
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
// 初始化时执行和状态改变时执行,类似于componentDidMount()和componentDidUpdate()
useEffect(() => {
// TODO
}) // 不传表示监听全部
// 初始化时执行,类似于componentDidMount()
useEffect(() => {
// TODO
}, []) // []表示都不监听
// 初始化时和count改变的时候会执行
useEffect(() => {
// TODO
}, [count]) // 只监听count
Effect()
可以返回一个函数,在组件卸载之前执行。相当于componentWillUnmount
钩子函数:
import { useEffect } from 'react'
import ReactDOM from 'react-dom'
function Demo() {
useEffect(() => {
return function () {
console.log('组件卸载前执行')
}
})
// 卸载
function handleUnmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<button onClick={handleUnmount}>卸载组件</button>
</div>
)
}
当点击卸载组件时,就会执行Effect()
返回的函数。
下面是一个定时器的案例:
import { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
// 定时器组件
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
let timer = setInterval(() => {
setCount((count) => count + 1)
}, 1000)
return () => {
clearInterval(timer) // 卸载组件前清除定时器
}
}, [])
// 卸载
function handleUnmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<p>count的值为:{count}</p>
<button onClick={handleUnmount}>卸载组件</button>
</div>
)
}
使用Effect()
需要注意⚠的一点是:如果有多个副效应,应该调用多个useEffect()
,而不应该合并写在一起:
import { useEffect, useState } from 'react'
function App() {
const [timerA, setTimerA] = useState(0)
const [timerB, setTimerB] = useState(0)
useEffect(() => {
setInterval(() => setTimerA((timerA) => timerA + 1), 1000)
}, [])
useEffect(() => {
setInterval(() => setTimerB((timerB) => timerB + 1), 2000)
}, [])
return (
<div>
{timerA},{timerB}
</div>
)
}
19.3、useRef()
在函数组件中使用String Ref、Callback Ref、Create Ref
会抛出错误。因为函数组件没有实例,所以函数组件中无法使用,需要使用useRef()
。
useRef
的作用:
- 获取
DOM
元素的节点。 - 获取子组件的实例。
- 渲染周期之间共享数据的存储。
获取DOM
元素的节点:
import { useEffect, useRef } from 'react'
function App() {
let inputRef = useRef()
useEffect(() => {
console.log(inputRef.current) // <input type="text">
}, [])
return <input type="text" ref={inputRef} />
}
因为函数组件没有实例,如果想用ref
获取子组件的实例,子组件组要写成类组件。获取子组件的实例:
import React, { useEffect, useRef } from 'react'
function App() {
let childRef = useRef()
useEffect(() => {
childRef.current.handleChild() // Child Component
}, [])
return <Child ref={childRef} />
}
class Child extends React.Component {
handleChild() {
console.log('Child Component')
}
render() {
return <div>Child组件</div>
}
}
渲染周期之间共享数据的存储:
import React, { useEffect, useRef } from 'react'
function App() {
const timerRef = useRef()
useEffect(() => {
let timerId = setInterval(() => {
// TODO
})
timerRef.current = timerId
return () => {
clearInterval(timerRef.current)
}
}, [])
return <div>App.js</div>
}
把定时器的ID
存入到useRef
中,定时器ID
不仅在useEffect
可以拿到,而且可以在整个组件函数中都可以获取到。
19.4、useContext()
createContext()
是一种组件间的通信方式,常用于祖孙组件通信。
假设有A,B,C
三个组件,它们是组孙关系。A
代表父组件,B
代表子组件,C
代表孙组件,A
组件的数据要传给C
组件:
使用createContext()
来传递跨级组件数据:
import React from 'react'
// 在组件外部建立一个 Context 容器对象
const MyContext = React.createContext()
// 父组件
class A extends React.Component {
constructor(props) {
super(props)
this.state = { username: '张三' }
}
render() {
return (
<>
<div>我是A组件</div>
{/* 使用MyContext.Provider 包裹子组件,并通过value传递数据 */}
<MyContext.Provider value={{ username: this.state.username }}>
<B />
</MyContext.Provider>
</>
)
}
}
// 子组件
class B extends React.Component {
render() {
return (
<>
<div>我是B组件</div>
<C />
</>
)
}
}
// 孙组件
class C extends React.Component {
static contextType = MyContext // 声明接收context
render() {
const { username } = this.context // 获取context
return <div>我是C组件,我从A组件得到的姓名是:{username}</div>
}
}
如果C
组件是函数组件,就需要使用Consume
来获取值,回调里的value
值就是context
的value
值:
function C() {
return <MyContext.Consumer>{(value) => <div>我是C组件,我从A组件得到的姓名是:{value.username}</div>}</MyContext.Consumer>
}
C
组件还可以使用useContext()
来获取值:
function C() {
const { username } = useContext(MyContext)
return <div>我是C组件,我从A组件得到的姓名是:{username}</div>
}
useContext(MyContext)
相当于class
组件中的static contextType = MyContext
或者 <MyContext.Consumer>
。
19.5、useReducer()
useReducer()
是useState
的替代方案。它的使用方式与Redux
相似:
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer()
可以传入三个参数:
-
第一个参数
reducer
它的使用方式跟Redux
中的Reducer
函数是非常相似的:(state, action) => newState
。 -
第二个参数
initialArg
就是状态的初始值。 -
第三个参数
init
是一个可选的用于懒初始化(Lazy Initialization)的函数,这个函数返回初始化后的状态。
下面是一个计数器的案例:
import { useReducer } from 'react'
// Reducer 函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
)
}
export default Counter
使用useReducer()
的场景还是很多的,下面举几个例子:
-
state
是一个数组或者对象。 -
state
变化很复杂,经常一个操作需要修改很多state
。 -
复杂的业务逻辑,
UI
和业务能够分离。 -
不同的属性被捆绑在了一起必须使用一个
state object
对象进行统一管理。
假如说有个登录的功能,分别使用useState()
和useReduer()
的方式:
// useState() 方式
import { useState, useReducer } from 'react'
// 登录组件
function LoginUI() {
const [name, setName] = useState('') // 用户名
const [pwd, setPwd] = useState('') // 密码
const [isLoading, setIsLoading] = useState(false) // 是否展示loading,发送请求中
const [error, setError] = useState('') // 错误信息
const [isLoggedIn, setIsLoggedIn] = useState(false) // 是否登录
async function LoginPage() {
setError('')
setIsLoading(true)
try {
await loginService({ name, pwd })
setIsLoggedIn(true)
setIsLoading(false)
} catch (err) {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
setError(error.message)
setName('')
setPwd('')
setIsLoading(false)
}
}
// 登录接口
function loginService() {
// ...
}
return <h2>Login Page</h2>
}
上面代码使用useState()
可以看到随着需求越来越复杂的时候,更多的state
被定义,更多的setState()
调用,很容易设置错误或者遗漏,可维护性也很差。下面来看一下使用useReducer()
的方式:
// useReducer() 方式
import { useReducer } from 'react'
const initState = { name: '', pwd: '', error: '', isLoading: false, isLoggedIn: false }
function loginReducer(state, action) {
switch (action.type) {
case 'login':
return { ...state, isLoading: true, error: '' }
case 'success':
return { ...state, isLoggedIn: true, isLoading: false }
case 'error':
return { ...state, error: action.payload.error, name: '', pwd: '', isLoading: false }
default:
return state
}
}
// 登录组件
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState)
const { name, pwd, error } = state
async function Login() {
dispatch({ type: 'login' })
try {
await loginService({ name, pwd })
dispatch({ type: 'success' })
} catch (err) {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
dispatch({
type: 'error',
payload: { error: error.message }
})
}
}
// 登录接口
function loginService() {
// ...
}
return <h2>Login Page</h2>
}
export default Login
参考
zhuanlan.zhihu.com/p/146773995