概念

官方 是这样定义的:用于构建用户界面的 JavaScript 库

它有如下几个特点:

  1. 声明式编程;
  2. 组件化开发;
  3. 多平台适配;

开发依赖

我们使用 React 开发必须依赖这三个库:

  1. react:包含所必须的核心代码;
  2. react-dom:react 渲染在不同平台所需要的核心代码;
  3. babel:将 jsx 转换成 react 代码的工具;

React 不是像 Vue 那样只需要依赖一个 vue.js 文件即可。这三个库都是各司其职,让每一个库都只做自己的事情。

react-dom 针对 web 和 native 所完成的事情是不同的:

  1. web:会将 jsx 最终渲染成真实的 DOM 并显示在浏览器中;
  2. native:会将 jsx 最终渲染成原生的控件;

我们有三种方式可以添加这些依赖:

  1. CDN 引入;
  2. 下载并添加本地依赖;
  3. 脚手架;

下面我们暂时就使用 CDN 方式来引入:

1
2
3
4
// @16 可以自定义版本号
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

crossorigin 这个属性目的是拿到远程脚本的错误信息。

注意:

  1. 使用 jsx,并希望 script 中的 jsx 代码被解析,必须给 script 标签添加 type 属性:

    1
    <script type="text/babel"></script>
  2. 在部署上线时,需要将 development.js 替换为 production.min.js


Hello World

1
<div id="app">app</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<script type="text/babel">
let message = 'hello world'
function btnClick() {
message = 'hello react'
render()
}
function render() {
ReactDOM.render(
<div>
<h2>{message}</h2>
<button onClick={btnClick}>改变内容</button>
</div>,
document.getElementById("app")
)
}
render()
</script>

注意:ReactDOM.render() 的第一个参数必须只能有一个根节点。

还可以这样写更加明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class App extends React.Component {
constructor() {
super()
this.state = {
message: 'hello world'
}
}
render() {
return (
<div>
<h1>{this.state.message}</h1>
<button onClick={this.btnClick.bind(this)}>改变文本</button>
</div>
)
}
btnClick() {
this.setState({
message: 'hello react'
})
}
}
ReactDOM.render(<App />, document.getElementById('app'))

下面我们就来开始正式的学习之旅了。


JSX语法

介绍

JSX 是一种 JavaScript 的语法扩展(eXtension),在很多地方被称为 JavaScript XML,因为看起来像是一段 XML 语法。

那我们来看一段 JSX 语法:

1
2
3
4
<script type="text/babel">
const element = <h1>hello react</h1>
ReactDOM.render(element, document.getElementById('app'))
</script>

JSX 的书写规范如下:

  1. JSX 顶层只能有一个根元素;
  2. 通常在 JSX 的外层包裹一个小括号();
  3. JSX 的标签可以是单标签,也可也是双标签。如果是单标签,必须以 /> 结尾;

语法

注释

在 JSX 中写注释的格式为:{/* 我是注释 */}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class App extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return (
<div>
{/* 我是注释 */}
{/*
我是注释
*/}
<h2>hello</h2>
<h3>react</h3>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))

嵌入变量

在 JSX 中嵌入变量有以下三种情况:

  1. 正常显示:变量类型为 Number、String、Array 时;
  2. 不正常显示:变量类型为 Null、Undefined、Boolean 时;
  3. 对象类型不能作为子元素展示;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
// 正常显示
name: "LqZww",
age: 18,
arr: [1, 2, 3],

// 不显示
test1: null,
test2: undefined,
test3: false,
test4: true,

obj: {
name: "LqZww",
age: 18
}
}
}
render() {
return (
<div>
{/* 显示 */}
<h2>{this.state.name}</h2>
<h2>{this.state.age}</h2>
<h2>{this.state.arr}</h2>

{/* 不显示 */}
<h2>{this.state.test1}</h2>
<h2>{this.state.test2}</h2>
<h2>{this.state.test3}</h2>
<h2>{this.state.test4}</h2>

{/* 报错 */}
<h2>{this.state.obj}</h2>

{/* 不报错 */}
<h2>{this.state.obj.name}</h2>
<h2>{this.state.obj.age}</h2>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))

如果我们非要把 Null、Undefined、Boolean 类型展示出来可以使用如下方法:

  1. Boolean 类型可以使用 toString() 方法;
  2. 使用 String() 方法:<h2>{String(this.state.test)}</h2>
  3. 使用空字符串拼接:<h2>{this.state.test + ""}</h2>

嵌入表达式

我们可以在 JSX 中嵌入表达式,比如:运算表达式、三元表达式、函数等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
firstName: "Lq",
lastName: "Zww",
isShow: true
}
}

render() {
const { firstName, lastName, isShow } = this.state
return (
<div>
<h2>{firstName + lastName}</h2>
<h2>{10 + 20}</h2>
<h2>{isShow ? '显示' : "不显示"}</h2>
<h2>{this.getMyName()}</h2>
</div>
)
}

getMyName() {
return this.state.firstName + this.state.lastName
}
}
ReactDOM.render(<App />, document.getElementById('app'))

绑定属性

下面我们就来简单的看看在 JSX 中如何绑定属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
title: "标题",
imgUrl: "/imgurl",
active: true
}
}

render() {
const { title, imgUrl, active } = this.state
return (
<div>
{/* 绑定普通属性 */}
<h2 title={title}>标题</h2>
<img src={imgUrl} />

{/* 绑定class */}
<div className="box">class</div>
<div className={"box " + (active ? "active" : "")}>class</div>

<label htmlFor=""></label>

{/* 绑定style */}
<div style={{ color: "red", fontSize: "30px" }}>style</div>
</div>
)
}

}
ReactDOM.render(<App />, document.getElementById('app'))

事件绑定

基本使用

在原生 JavaScript 中,我们监听事件有如下两种方式:

  1. 获取 DOM,添加监听事件;
  2. 绑定 onClick;

在 JSX 中,也是使用 onClick 来进行事件绑定,但是对于 this 有以下几种情况:

  1. 使用 bind 来绑定 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class App extends React.Component {
    constructor(props) {
    super(props)
    this.state = {
    title: "hello",
    }

    this.btnClick1 = this.btnClick1.bind(this)
    }

    render() {
    return (
    <div>
    <button onClick={this.btnClick.bind(this)}>按钮</button>
    <button onClick={this.btnClick1}>按钮</button>
    </div>
    )
    }

    btnClick() {
    console.log(this.state.title); // hello
    }
    btnClick1() {
    console.log(this.state.title); // hello
    }
    }
    ReactDOM.render(<App />, document.getElementById('app'))
  2. 定义函数时,使用箭头函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class App extends React.Component {
    constructor(props) {
    super(props)
    this.state = {
    count: 99
    }
    }

    render() {
    const { } = this.state
    return (
    <div>
    <button onClick={this.addCount}>+1</button>
    </div>
    )
    }

    addCount = () => {
    console.log(this.state.count);
    }
    }
    ReactDOM.render(<App />, document.getElementById('app'))
  3. 直接传入一个箭头函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class App extends React.Component {
    constructor(props) {
    super(props)
    this.state = {
    title: "hello",
    count: 99
    }
    }

    render() {
    const { } = this.state
    return (
    <div>
    <button onClick={() => { this.redCount() }}>-1</button>
    </div>
    )
    }

    redCount = () => {
    console.log(this.state.count);
    }
    }
    ReactDOM.render(<App />, document.getElementById('app'))

传参

下面就来演示一下事件绑定如何传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
list: ['lq', 'zww', 'LqZww']
}
this.btnClick = this.btnClick.bind(this)
}

render() {
return (
<div>
{/* 1. */}
<button onClick={this.btnClick}>+1</button>

{/* 2. */}
<ul>
{
this.state.list.map((item, index, arr) => {
return <li onClick={(e) => { this.liClick(item, index, e) }}>{item}</li>
})
}
</ul>
</div>
)
}

btnClick(e) {
console.log(e);
}
liClick(item, index, e) {
console.log(item, index, e);
}
}
ReactDOM.render(<App />, document.getElementById('app'))

条件渲染

在 Vue 中,一般我们会通过使用指令来进行控制,比如:v-ifv-show

在 React 中所有的条件判断都与普通的 JavaScript 代码一致。

那么常见的条件渲染方式有哪几种呢:

  1. 条件判断语句;
  2. 三元运算符;
  3. 逻辑与 &&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isShow: true
}
}

render() {
// 1. if 判断
let show = null
let text = null
if (this.state.isShow) {
show = <h2>show</h2>
text = "happy"
} else {
show = <h2>no show</h2>
text = "sad"
}

return (
<div>
{show}
<button>{text}</button>

{/* 2. 三元运算符 */}
<button onClick={e => this.switchBtn()}>{this.state.isShow ? "happy" : "sad"}</button>

{/* 3. 逻辑与 */}
<h2>{this.state.isShow && "hello React"}</h2>
</div>
)
}

switchBtn() {
this.setState({
isShow: !this.state.isShow
})
}
}
ReactDOM.render(<App />, document.getElementById('app'))

下面我们就来实现一下 Vue 中 v-show 的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isShow: true
}
}

render() {
const { isShow } = this.state;
const changeDisplay = isShow ? 'block' : 'none';
return (
<div>
<h2 style={{ display: changeDisplay }}>hello React</h2>
<button onClick={e => this.switchBtn()}>{isShow ? '隐藏' : '显示'}</button>
</div>
)
}

switchBtn() {
this.setState({
isShow: !this.state.isShow
})
}
}
ReactDOM.render(<App />, document.getElementById('app'))

列表渲染

在 React 中不像 Vue 那样使用 v-for,而是需要使用到 map 来进行列表渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [1, 2, 3, 4, 5],
filterList: [321, 3123, 12, 32, 1, 434, 34, 121]
}
}

render() {
return (
<div>
<ul>
{
this.state.list.map((item, index, arr) => {
return <li>{item} - {index}</li>
})
}
</ul>

<hr />

<ul>
{
this.state.filterList.filter(item => {
return item >= 200
}).map(item => {
return <li>{item}</li>
})
}
</ul>
</div>
)
}

}
ReactDOM.render(<App />, document.getElementById('app'))

本质

实际上,JSX 只是 React.createElement(component,props,...children) 函数的语法糖。

所有的 JSX 最终都会被转换成 React.createElement 的函数调用。

1
2
3
4
const mes1 = <h2>hello react</h2>
const mes2 = React.createElement("h2", null, "hello react")

ReactDOM.render(mes1, document.getElementById('app'))

当我们把 mes1 换成 mes2 后,会发现渲染的内容是一样的。

React.createElement(type,config,children) 需要传递三个参数:

  1. type:当前 ReactElement 的类型,如果是标签就使用字符串来表示(”div”),如果是组件就直接使用组件名称;
  2. config:所有 JSX 中的属性都在 config 中以对象的属性和值的形式存储;
  3. children:存放标签中的内容,以数组的方式存储;

我们可以去 babeljs.io 中将 JSX 代码进行转换。


脚手架

安装

在安装 React 脚手架之前,我们必须先安装 node

安装 node 后会默认安装好 npm 包管理工具,除了它还有大名鼎鼎的 yarn。

React 脚手架默认也是使用的 yarn

下面我们就可以来安装脚手架了:

1
npm install -g create-react-app

使用如下命令检查是否安装成功:

1
create-react-app --version

创建

下面我们就来创建一个 React 项目。

使用下面这个命令来创建:

1
create-react-app 项目名称

注意:项目名称不能使用大写字母。

创建完成后进入目录,并运行:

1
2
cd 项目名称
yarn start

目录结构

下面就来看一看 React 脚手架的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── node_modules                // 依赖
├── public
│ ├── favicon.ico // 图标
│ ├── index.html // 入口页面
│ ├── logo192.png // logo
│ ├── logo512.png // logo
│ ├── manifest.json // 与web app配置相关
│ └── robots.txt // 设置爬虫规则
├── src
│ ├── App.css // app 组件样式文件
│ ├── App.js // app 组件代码文件
│ ├── App.test.js // 测试用例
│ ├── index.css // 全局样式文件
│ ├── index.js // react 代码入口
│ ├── logo.svg // svg图
│ ├── reportWebVitals.js // 默认写好的注册 PWA 相关代码
│ └── setupTests.js // 做测试前的初始化文件
├── .gitignore // git的忽略文件
├── package.json // 项目配置的管理文件
├── README.md // 项目的描述
└── yarn.lock // 记录真实的版本依赖

生命周期

常用生命周期函数

很多事物都有从创建到销毁的整个过程,这个过程被称为生命周期。

生命周期与生命周期函数的关系:

  • 生命周期的整个过程分为很多个阶段:
    1. 装载阶段(Mount):组件第一次在 DOM 树中被渲染的过程;
    2. 更新阶段(Update):组件状态发生变化,重新更新渲染的过程;
    3. 卸载阶段(Unmount):组件从 DOM 树中移除的过程;
  • React 会对组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
    1. 比如实现 componentDidMount 函数:组件已经挂载到 DOM 上就会回调;
    2. 比如实现 componentDidUpdate 函数:组件发生了更新就会回调;
    3. 比如实现 componentWillUnmount 函数:组件即将被移除就会回调;

我们来看看挂载阶段的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default class App extends Component {
constructor() {
super()
console.log("执行constructor");
}

render() {
console.log("执行render");
return (
<div></div>
)
}

componentDidMount() {
console.log("执行componentDidMount");
}
}

挂载阶段的执行顺序为:执行constructor -> 执行render -> 执行componentDidMount


下面是更新阶段的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default class App extends Component {
constructor() {
super()
this.state = {
count: 0
}
console.log("执行constructor");
}

render() {
console.log("执行render");
return (
<div>
<button onClick={e => this.addBtn()}>+1</button>
<h2>{this.state.count}</h2>
</div>
)
}

addBtn() {
this.setState({
count: this.state.count + 1
})
}

componentDidMount() {
console.log("执行componentDidMount");
}

componentDidUpdate() {
console.log("执行componentDidUpdate");
}
}

当我们点击按钮时,它的执行顺序为:执行render -> 执行componentDidUpdate


最后我们再来看看卸载阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Cpn extends Component {
render() {
return <h2>cpn组件</h2>
}

componentWillUnmount() {
console.log("执行cpn的componentWillUnmount");
}
}

export default class App extends Component {
constructor() {
super()
this.state = {
count: 0,
isShow: true
}
console.log("执行constructor");
}

render() {
console.log("执行render");
return (
<div>
<button onClick={e => this.addBtn()}>+1</button>
<h2>{this.state.count}</h2>
<hr />
{this.state.isShow && <Cpn />}
<button onClick={e => this.switchBtn()}>切换</button>
</div>
)
}

switchBtn() {
this.setState({
isShow: !this.state.isShow
})
}

addBtn() {
this.setState({
count: this.state.count + 1
})
}

componentDidMount() {
console.log("执行componentDidMount");
}

componentDidUpdate() {
console.log("执行componentDidUpdate");
}
}

当我们点击切换按钮后,会发现执行顺序为:执行render -> 执行cpn的componentWillUnmount -> 执行componentDidUpdate


constructor 通常只做下面两件事:

  1. this.state 赋值对象来初始化内部的 state;
  2. 为事件绑定实例;

componentDidMount 会在组件挂载后(插入DOM)立即调用,它通常在下面几点进行操作:

  1. 依赖于 DOM 的操作;
  2. 发送网络请求;
  3. 添加订阅(在 componentWillUnmount 取消订阅);

componentDidUpdate 会在更新后立即被调用,首次渲染不会执行:

  1. 当组件更新后,可以对 DOM 进行操作;
  2. 如果对更新前后的 props 进行了比较,也可以选择在此处进行网路请求;

componentWillUnmount 会在组件卸载及销毁前调用。可以在此方法执行必要的清理操作,例如:清除定时器、取消网络请求或清除在 componentDidMount() 中创建的订阅等。


不常用生命周期函数

React 中还有一些不常用的生命周期函数:

  1. getDerivedStateFromProps:state 值在任何时候都依赖于 props 时使用,该方法返回一个对象来更新 state;
  2. getSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的函数,可以获取 DOM 更新前的一些信息;
  3. shouldComponentUpdate:在重新渲染 render() 函数时调用前被调用的函数,在性能优化方面常用。

除这些外,还有一些过时的生命周期,官方已推荐不建议使用。


组件化

概念

在我们开发中,常常会使用到组件化思想

  • 将一个完整的页面分成多个组件;
  • 每个组件都用于实现页面的一个功能块;
  • 每一个组件又可以进行细分;
  • 组件本身可以在多个地方进行复用;

在 React 的组件相对于 Vue 中更加灵活和多样,按照不同的方式可以分成很多类型的组件:

  1. 根据组件的定义方式:函数组件和类组件;
  2. 根据组件内部是否有状态需要维护:无状态组件和有状态组件;
  3. 根据组件的职责:展示型组件和容器型组件;

组件的定义方式

类组件

类组件定义有下面几个要求:

  1. 组件名称必须以大写字符开头;
  2. 类组件需要继承自 React.Component;
  3. 类组件必须实现 render 函数;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default class App extends Component {
constructor() {
super()
this.state = {
message: 'hello'
}
}

render() {
return (
<div>
<h2>{this.state.message}</h2>
</div>
)
}
}

函数式组件

函数式组件使用 function 来定义函数,这个函数会返回和类组件中 render 函数返回一样的内容。

函数式组件有如下几个特点:

  1. 没有 this 对象;
  2. 没有内部的状态;
  3. 没有生命周期函数,但会被更新并挂载。
1
2
3
4
5
export default function App() {
return (
<h2>hello</h2>
)
}

组件间的通信

父传子

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'

function Child(props) {
const { name, age } = props
return (
<div>子组件数据:{"name:" + name + "age:" + age}</div>
)
}

export default class App extends Component {
render() {
return (
<div>
<Child name="LqZww" age="18" />
</div>
)
}
}

类组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Child extends Component {
constructor(props) {
super(props)
}

render() {
const { name, age } = this.props
return (
<div>子组件数据:{"name:" + name + "age:" + age}</div>
)
}
}

export default class App extends Component {
render() {
return (
<div>
<Child name="LqZww" age="18" />
</div>
)
}
}

参数验证

在传递数据给子组件时,我们希望对数据进行验证一下,这就需要使用到 propTypes

在 React v15.5 起,将 React.PropTypes 移动到一个库中:porp-types 库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { Component } from 'react'

import PropTypes from 'prop-types'

function Child(props) {
const { name, age } = props
return (
<div>子组件数据:{"name:" + name + "age:" + age}</div>
)
}

Child.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
Child.defaultProps = {
name: 'hhhh',
age: 99
}

export default class App extends Component {
render() {
return (
<div>
<Child name="LqZww" age={18} />
<Child name="qqqq" />
</div>
)
}
}

上面代码中:

  1. Child.propTypes:设置数据验证;
  2. PropTypes.string:该项为 string 类型;
  3. isRequired: 表示必传项;
  4. Child.defaultProps: 表示设置默认值;

在类组件中,也可以使用上面这种方法,但还有一种方法可以写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component } from 'react'

import PropTypes from 'prop-types'

class Child extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
static defaultProps = {
name: 'hhhh',
age: 99
}

constructor(props) {
super(props)
}

render() {
const { name, age } = this.props
return (
<div>子组件数据:{"name:" + name + "age:" + age}</div>
)
}
}

export default class App extends Component {
render() {
return (
<div>
<Child name="LqZww" age={18} />
<Child name="qqqq" />
</div>
)
}
}

子传父

函数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react'

class CounterButton extends Component {
render() {
const { onClick } = this.props
return <button onClick={onClick}>+1</button>
}
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}

render() {
return (
<div>
<h2>当前计数为:{this.state.counter}</h2>
<button onClick={e => this.increment()}>+</button>
<CounterButton onClick={e => this.increment()} />
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

案例

下面我们就来做一个 tab 切换的一个案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app.js
import React, { Component } from 'react'
import TabControl from './TabControl'

export default class App extends Component {
constructor(props) {
super(props)

this.titles = ['tab1', 'tab2', 'tab3']
this.state = {
currentTitle: "tab1",
}
}

render() {
const { currentTitle } = this.state
return (
<div>
<TabControl
titles={this.titles}
itemClick={index => this.itemClick(index)} />
<h2>{currentTitle}</h2>
</div>
)
}

itemClick(index) {
this.setState({
currentTitle: this.titles[index]
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// TabControl.js
import React, { Component } from 'react'
import PropTypes from 'prop-types';

export default class TabControl extends Component {
constructor(props) {
super(props)
this.state = {
currentIndex: 0
}
}
render() {
const { titles } = this.props
const { currentIndex } = this.state
return (
<div className='tab-control'>
{
titles.map((item, index) => {
return (
<div
key={index}
className={'tab-item ' + (index === currentIndex ? 'active' : '')}
onClick={e => this.itemClick(index)}
>
<span>{item}</span>
</div>
)
})
}
</div>
)
}
itemClick(index) {
this.setState({
currentIndex: index
})

const { itemClick } = this.props
itemClick(index)
}
}

TabControl.propTypes = {
titles: PropTypes.array.isRequired
}

实现Vue中slot功能

方案一(不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// App.js
import React, { Component } from 'react'

import NavBar from './NavBar'

export default class App extends Component {
render() {
return (
<div>
<NavBar>
<span>1</span>
<span>2</span>
<span>3</span>
</NavBar>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// NavBar.js
import React, { Component } from 'react'

export default class NavBar extends Component {
render() {
return (
<div className='nav-bar'>
<div className='nav-left'>
{this.props.children[0]}
</div>
<div className='nav-center'>
{this.props.children[1]}
</div>
<div className='nav-right'>
{this.props.children[2]}
</div>
</div>
)
}
}

这种方式会有个局限性就是必须要按着顺序进行摆放书写。这种一般适用于只有一个插槽数据的时候使用。


方案二(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// App.js
import React, { Component } from 'react'

import NavBar from './NavBar'

export default class App extends Component {
render() {
return (
<div>
<NavBar
leftSlot={<span>1</span>}
centerSlot={<span>2</span>}
rightSlot={<span>3</span>}
/>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// NavBar.js
import React, { Component } from 'react'

export default class NavBar extends Component {
render() {
const { leftSlot, centerSlot, rightSlot } = this.props

return (
<div className='nav-bar'>
<div className='nav-left'>
{leftSlot}
</div>
<div className='nav-center'>
{centerSlot}
</div>
<div className='nav-right'>
{rightSlot}
</div>
</div>
)
}
}

跨组件通信

方案一(不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { Component } from 'react'

function ProfileHeader(props) {
return (
<div>
<h2>name:{props.name}</h2>
<h2>age:{props.age}</h2>
</div>
)
}

function Profile(props) {
return (
<div>
<ProfileHeader name={props.name} age={props.age} />
<ul>
<li>li1</li>
<li>li2</li>
<li>li3</li>
</ul>
</div>
)
}

export default class App extends Component {
constructor(props) {
super(props)

this.state = {
name: 'LqZww',
age: 18
}
}

render() {
const { name, age } = this.state;

return (
<div>
<Profile name={name} age={age} />
</div>
)
}
}

我们可以通过 props 一层一层的传递下去。

以上代码中:

1
2
3
<ProfileHeader name={props.name} age={props.age} />
{/* 等价于 */}
<ProfileHeader {...props} />

这叫做 属性展开。我们可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。


方案二(Context)

对于非父子组件的数据共享,如果我们像方案一那样一层一层的传递数据是非常繁琐的,并且代码也是多余的。

在 React 中提供了一个 API:Context。它提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树逐层传递 props。

它设计的目的是为了共享那些对于一个组件树而言是 “全局” 的数据。在当前认证的用户、主题、首选语言这些场景都可以使用。

它有如下几个相关的 API:

  1. React.createContext
    • 创建一个需要共享的 Context 对象;
    • 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 Context 值;
    • defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用的默认值;
    • const MyContext = React.createContext(defaultValue);
  2. Context.Provider
    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化;
    • Provider 接收一个 value 属性,传递给消费组件;
    • 一个 Provider 可以和多个消费组件有对应关系;
    • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
    • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
    • <MyContext.Provider value={} />
  3. Class.contextType
    • 挂载在 class 上的 contextType 属性会被重新赋值为一个由 React.createContext() 创建的 Context 对象;
    • 它能让你使用 this.context 来消费最近 Context 上的那个值;
    • 可以在任何生命周期中访问它,包括 render 函数中;
    • MyClass.contextType = MyContext;
  4. Context.Consumer
    • 它能订阅到 context 变更,让我们可以在函数式组件中完成订阅 context;
    • 它需要函数作为子元素;
    • 此函数接收当前的 context 值,返回一个 React 节点;
    • <MyContext.Consumer>{value=>{}}</MyContext.Consumer>
基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { Component } from 'react'

// 1. 创建 Context 对象,并可以赋上默认值
const UserContext = React.createContext({
name: '默认名字',
age: 1
})

class ProfileHeader extends Component {
render() {
return (
<div>
<h2>name:{this.context.name}</h2>
<h2>age:{this.context.age}</h2>
</div>
)
}
}

// 3. 设置 contextType
ProfileHeader.contextType = UserContext

function Profile(props) {
return (
<div>
<ProfileHeader />
</div>
)
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'LqZww',
age: 18
}
}

render() {
return (
<div>
{/* 2. 使用 UserContext.Provider 组件 */}
<UserContext.Provider value={this.state}>
<Profile />
</UserContext.Provider>
</div>
)
}
}

如果我们把 <Profile /> 没有放到 <UserContext.Provider value={this.state}></UserContext.Provider> 里面的话,而是放到外面的,将会展示默认值


函数式组件中的使用

上面中我们是在类组件中的使用,下面就来看看在函数式组件中该如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { Component } from 'react'

const UserContext = React.createContext({
name: '默认名字',
age: 1
})

function ProfileHeader() {
return (
<UserContext.Consumer>
{
value => {
return (
<div>
<h2>name:{value.name}</h2>
<h2>age:{value.age}</h2>
</div>
)
}
}
</UserContext.Consumer>
)
}

function Profile(props) {
return (
<div>
<ProfileHeader />
</div>
)
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'LqZww',
age: 18
}
}

render() {
return (
<div>
<UserContext.Provider value={this.state}>
<Profile />
</UserContext.Provider>
</div>
)
}
}

多个context的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React, { Component } from 'react'

const UserContext = React.createContext({
name: '默认名字',
age: 1
})

const ThemeContext = React.createContext({
theme: 'white'
})

function ProfileHeader() {
return (
<UserContext.Consumer>
{
value => {
return (
<ThemeContext.Consumer>
{
theme => {
return (
<div>
<h2>name:{value.name}</h2>
<h2>age:{value.age}</h2>
<h2>主题:{theme.theme}</h2>
</div>
)
}
}
</ThemeContext.Consumer>
)
}
}
</UserContext.Consumer>
)
}

function Profile(props) {
return (
<div>
<ProfileHeader />
</div>
)
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 'LqZww',
age: 18
}
}

render() {
return (
<div>
<UserContext.Provider value={this.state}>
<ThemeContext.Provider value={{ theme: 'black' }}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
}

在开发中,如果我们有多个组件之间需要共享数据,一般不会这样使用的,而是会使用 redux


全局事件传递

安装 events 依赖:

1
yarn add events

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { PureComponent } from 'react';

// 1. 导入
import { EventEmitter } from 'events'
// 2. 定义
const eventBus = new EventEmitter()

class Home extends PureComponent {

componentDidMount() {
// 4. 事件监听
eventBus.addListener('sayHello', this.handleSayListener)
}

componentWillUnmount() {
// 5. 事件销毁
eventBus.removeListener('sayHello', this.handleSayListener)
}

handleSayListener(name, age) {
// 获取传递的参数
console.log(name, age);
}

render() {
return (
<div>
home

</div>
)
}
}

class Profile extends PureComponent {
render() {
return (
<div>
profile
<button onClick={e => this.emmitEvent()}>btn</button>
</div>
)
}

emmitEvent() {
// 3. 发送事件
eventBus.emit('sayHello', 'hello home', 18)
}
}

export default class App extends PureComponent {
render() {
return (
<div>
<Home />
<Profile />
</div>
);
}
}

setState

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from 'react'

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}

render() {
return (
<div>
<h2>当前数:{this.state.counter}</h2>
<button onClick={e => this.increment()}>+</button>
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

setState异步更新

当我们在 setState 下面打印 counter 的值,我们会发现当我们点击了一次按钮后,却还是打印的是 0。这是因为,setState 是一个异步更新的。由此可见,我们不能在执行完 setState 后立即拿到最新的结果。

至于为什么 setState 设置为异步,可以参考此回答

简单的总结就是:

  1. 设置为异步可以显著的提高性能;
  2. 如果同步更新了 state,但还没有执行 render 函数,那么 stateprops 就不能保持同步;

那如果我们要获取更新后的数据该怎么办呢,这里有下面两种方式:

  1. setState 的第二个参数(回调函数)中获取;
    1
    2
    3
    4
    5
    6
    7
    increment() {
    this.setState({
    counter: this.state.counter + 1
    }, () => {
    console.log(this.state.counter); // 1
    })
    }
  2. componentDidUpdate 生命周期函数中获取;
    1
    2
    3
    componentDidUpdate() {
    console.log(this.state.counter); // 1
    }

注意:componentDidUpdate 的执行顺序是早于 setState 回调函数的。


setState同步更新

在某些情况下 setState 也是同步更新的:

  1. setState 放到定时器中使用:
    1
    2
    3
    4
    5
    6
    7
    8
    increment() {
    setTimeout(() => {
    this.setState({
    counter: this.state.counter + 1
    })
    console.log(this.state.counter); // 1
    }, 0)
    }
  2. 使用原生的 DOM 事件监听:
    1
    2
    3
    4
    5
    6
    7
    8
    componentDidMount() {
    document.getElementById('btn').addEventListener('click', () => {
    this.setState({
    counter: this.state.counter + 1
    })
    console.log(this.state.counter); // 1
    })
    }

setState不可变数据

我们先来看看下例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { Component } from 'react'

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
list: [
{ id: 0, name: 'lq', age: 18 },
{ id: 1, name: 'zww', age: 19 }
]
}
}
render() {
return (
<div>
<ul>
{
this.state.list.map(item => {
return <li key={item.id}>{item.name}</li>
})
}
</ul>
<button onClick={e => this.addData()}>add</button>
</div>
)
}
shouldComponentUpdate(newProps, newState) {
if (newState.list !== this.state.list) {
return true
}
return false
}

addData() {
// 强烈不推荐
const newData = { id: Math.random(), name: 'lqzww', age: 20 }
this.state.list.push(newData)
this.setState({
list: this.state.list
})
}
}

当我们点击按钮后,会发现页面并没有任何变化!这是因为数组是一个引用类型,存储的是一个内存地址。

当我们对 addData 方法做如下更改:

1
2
3
4
5
6
7
8
addData() {
const newData = { id: Math.random(), name: 'lqzww', age: 20 }
const newList = [...this.state.list]
newList.push(newData)
this.setState({
list: newList
})
}

我们会发现数据正确的添加了。这是因为使用展开运算符会在内存中新开辟一个空间,并把对象的引用地址全部拷贝到新空间中,此时 newData 指向的是新地址。当把数据 push 进去后是给新地址添加数据,最后旧地址与新地址在进行判断的时候是不相同,就直接返回 true,并重新调用 render 函数。

如果觉得上面使用 shouldComponentUpdate 的操作很麻烦,我们可以使用继承 PureComponent,它会自动的对数据进行浅层比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
list: [
{ id: 0, name: 'lq', age: 18 },
{ id: 1, name: 'zww', age: 19 }
]
}
}
render() {
return (
<div>
<ul>
{
this.state.list.map(item => {
return <li key={item.id}>{item.name}</li>
})
}
</ul>
<button onClick={e => this.addData()}>add</button>
</div>
)
}

addData() {
const newData = { id: Math.random(), name: 'lqzww', age: 20 }
const newList = [...this.state.list]
newList.push(newData)
this.setState({
list: newList
})
}
}

React更新机制

React 的渲染流程大致为:JSX -> 虚拟DOM -> 真实DOM;

而更新流程为:props/state改变 -> render函数重新执行 -> 产生新的DOM树 -> 新旧DOM树进行diff -> 计算出差异进行更新 -> 更新到真实DOM;

下面我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react'

class Footer extends Component {
render() {
console.log('footer 被调用');
return <h2>footer</h2>
}
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
render() {
console.log('app 被调用');
return (
<div>
<h2>当前数:{this.state.counter}</h2>
<button onClick={e => this.increment()}>+</button>
<Footer />
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

我们会发现,当初次进入页面的时候就会打印出两条信息。当我们点击加号时,发现控制台还是打印了两条信息,但是这并不是我们想要的,因为我们只在 App 上更新了数据,并不想 Footer 被调用。如果每次点击全部都重新调用了,那将会很浪费性能。因此,这时候我们可以使用 shouldComponentUpdate 生命周期函数来解决,如下:

1
2
3
4
5
6
7
shouldComponentUpdate(nextProps, nextState) {
console.log(nextProps, nextState);
if (this.state.counter != nextState.counter) {
return true
}
return false;
}

我们还可以使用 PureComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component, PureComponent } from 'react'

class Footer extends PureComponent {
render() {
console.log('footer 被调用');
return <h2>footer</h2>
}
}

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
render() {
console.log('app 被调用');
return (
<div>
<h2>当前数:{this.state.counter}</h2>
<button onClick={e => this.increment()}>+</button>
<Footer />
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

此时,第一次刷新会全部打印,当点击加号后只会打印 app 被调用

除此之外,我们还可以使用 memo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { Component, memo } from 'react'

const FooterMemo = memo(function Footer() {
console.log('footer 被调用');
return <h2>footer</h2>
})

export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
render() {
console.log('app 被调用');
return (
<div>
<h2>当前数:{this.state.counter}</h2>
<button onClick={e => this.increment()}>+</button>
<FooterMemo />
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1
})
}
}

此时点击加号后也只会打印 app 被调用


1.11