React的特点和优势

  1. 虚拟DOM
    之前操作dom的⽅式是通过document.getElementById()的⽅式,这样的过程实际上是先去读取html的dom结构,将结构转换成变量,再进⾏操作
    ⽽reactjs定义了⼀套变量形式的dom模型,⼀切操作和换算直接在变量中,这样减少了操作真实dom,性能真实相当的⾼,和主流MVC框架有本质的区别,并不和dom打交道
  2. 组件系统
    react最核⼼的思想是将⻚⾯中任何⼀个区域或者元素都可以看做⼀个组件 component
    那么什么是组件呢?
    组件指的就是同时包含了html、css、js、image元素的聚合体
    使⽤react开发的核⼼就是将⻚⾯拆分成若⼲个组件,并且react⼀个组件中同时耦合了css、js、
    image,这种模式整个颠覆了过去的传统的⽅式
  3. 单向数据流
    其实reactjs的核⼼内容就是数据绑定,所谓数据绑定指的是只要将⼀些服务端的数据和前端⻚⾯绑定
    好,开发者只关注实现业务就⾏了
  4. JSX 语法
    在vue中,我们使⽤render函数来构建组件的dom结构性能较⾼,因为省去了查找和编译模板的过程,
    但是在render中利⽤createElement创建结构的时候代码可读性较低,较为复杂,此时可以利⽤jsx语法
    来在render中创建dom,解决这个问题,但是前提是需要使⽤⼯具来编译jsx

核心代码

main.js

全局定义将组件App放置在id='App'的容器里

1
2
3
4
5
6
7
8
9
import React from 'react'
import ReactDom from 'react-dom'
import App from './app.jsx'
// ReactDOM.render('要渲染的虚拟DOM元素', '要渲染到页面上的哪个位置中')
// 注意: ReactDOM.render() 方法的第二个参数,和vue不一样,不接受 "#app" 这样的字符串,而是需要传递一个 原生的 DOM 对象
ReactDom.render(
<App/>,
document.getElementById('App')
)

APP.jsx

1
2
3
4
5
6
7
8
9
10
11
12
// 在 react 中,如要要创建 DOM 元素了,只能使用 React 提供的 JS API 来创建,不能【直接】像 Vue 中那样,手写 HTML 元素
// React.createElement() 方法,用于创建 虚拟DOM 对象,它接收 3个及以上的参数
// 参数1: 是个字符串类型的参数,表示要创建的元素类型
// 参数2: 是一个属性对象,表示 创建的这个元素上,有哪些属性
// 参数3: 从第三个参数的位置开始,后面可以放好多的虚拟DOM对象,这写参数,表示当前元素的子节点
var myDiv = React.createElement('div', { title: 'this is a div', id: 'mydiv' }, '这是一个div', myH1)
//<div title="this is a div" id="mydiv">这是一个div</div>

// 由于,React官方,发现,如果直接让用户手写 JS 代码创建元素,用户会疯掉的,然后,用户就开始寻找新的前端框架了,于是,React 官方,就提出了一套 JSX 语法规范,能够让我们在 JS 文件中,书写类似于 HTML 那样的代码,快速定义虚拟DOM结构;
// 问题: JSX(符合 XML 规范的 JS 语法)的原理是什么?
// JSX内部在运行的时候,也是先把 类似于HTML 这样的标签代码,转换为了 React.createElement 的形式;(JSX是一个对程序员友好的语法糖)
//在JSX创建DOM的时候,所有的节点,必须有唯一的根元素进行包裹;
1
2
3
4
5
6
7
8
9
10
11
12
 // 在React中,构造函数,就是一个最基本的组件
// 如果想要把组件放到页面中,可以把 构造函数的名称,当作 组件的名称,以 HTML标签形式引入页面中即可
// 注意:React在解析所有的标签的时候,是以标签的首字母来区分的,如果标签的首字母是小写,那么就按照 普通的 HTML 标签来解析,如果 首字母是大写,则按照 组件的形式去解析渲染
// 结论:组件的首字母必须是大写
export default function Hello(props) {
// 在组件中,如果想要使用外部传递过来的数据,必须,显示的在 构造函数参数列表中,定义 props 属性来接收;
// 通过 props 得到的任何数据都是只读的,不能从新赋值
return (
<div>
<h1>这是在Hello组件中定义的元素 --- {props.name}</h1>
</div>)
}

组件的生命周期

https://react.docschina.org/docs/react-component.html

https://m.html.cn/qa/react/14367.html

从出生到成长,最后到死亡,这个过程的时间可以理解为生命周期。React的生命周期同理也是这么一个过程。
React的生命周期分为三个阶段:挂载期(也叫实例化期)、更新期(也叫存在期)、卸载期(也叫销毁期)。在每个周期中React都提供了一些钩子函数。
生命周期的描述如下:
挂载期:一个组件实例初次北创建的过程。
更新期:组件在创建后再次渲染的过程。
卸载期:组件在使用完后被销毁的过程。

组件初始化阶段

组件实例创建阶段的生命周期函数,在组件的一辈子中,只执行一次

  • constructor(props)

    仅用于以下两种情况:

    在为 React.Component 子类实现构造函数时,通过 super(props)调用父类React Component的构造函数,⽤来将⽗组件传来的 props 绑定到这个类中。否则,this.props 在构造函数中可能会出现未定义的 bug。

  • componentWillMount(17后已经过时): 组件将要被挂载,此时还没有开始渲染虚拟DOM,无法获取到页面上的任何元素,因为虚拟DOM和页面都还没有开始渲染呢。

    • 进⾏ajax请求,作者一开始也喜欢在React的willMount函数中进行异步获取数据(认为这可以减少白屏的时间),后来发现其实应该在didMount中进行。

    • 可以修改state

  • render: render() 方法是 class 组件中唯一必须实现的方法。 第一次开始渲染,创建虚拟dom,当render执行完,内存中就有了完整的虚拟DOM了。但是,页面上尚未真正显示DOM元素

  • componentDidMount: componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。

    • 网络请求获取数据
    • 对DOM进行操作
    • 这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

组件更新阶段

更新根据组件的state和props的改变,有选择性的触发0次或多次;

  • componentWillReceiveProps(17后已经过时):
    组件将要接收新属性props,此时,只要这个方法被触发,就证明父组件为当前子组件传递了新的属性值;
    如果我们使用 this.props 来获取属性值,这个属性值,不是最新的,是上一次的旧属性值

    1
    2
    3
    componentWillReceiveProps(nextProps){    
    console.log(this.props.pmsg + ' ---- ' + nextProps.pmsg
    );}
  • shouldComponentUpdate: 组件是否需要被更新,此时,组件尚未被更新,但是,state 和 props 肯定是最新的。 首次渲染或使用 forceUpdate() 时不会调用该方法。

    1
    shouldComponentUpdate(nextProps, nextState)
  • componentWillUpdate(17已经过时): 组件将要被更新,此时,尚未开始更新,内存中的虚拟DOM树还是旧的,页面上的 DOM 元素 也是旧的

  • render: 根据最新的 state 和 props 重新渲染一棵内存中的 虚拟DOM树,当 render 调用完毕,内存中的旧DOM树,已经被新DOM树替换了!此时页面还是旧的

  • componentDidUpdate: 此时,页面又被重新渲染了,state 和 虚拟DOM 和 页面已经完全保持同步

组件销毁阶段

也有一个显著的特点,一辈子只执行一次

1
componentWillUnmount: 组件将要被卸载,此时组件还可以正常使用;

image.png

新增

image-20200528113856923

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

错误处理
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError()
  • componentDidCatch()

废弃的生命周期

被废弃的三个函数都是在render之前

为什么废弃

由于React未来会推出新的渲染方式–异步渲染,一种生命周期可被打断的渲染方式(因为fiber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次),具体是在render()生成虚拟 dom 阶段可以打断重来, 这就会导致在dom挂载之前或是被更新之前的所有任务都会重复操作,所以componentWillMount()、·componentWillReceiveProps() componentWIllUpdate()方法可能会执行多次。(函数内部逻辑多次调用

componentWillMount

新版本中官方推荐将初始化的操作放在constructor()中, 将请求异步数据、订阅事件源、监听事件的操作放在componentDidMount()

componentWillReceiveProps

在老版本的React中,如果组件自身的state与其props密切相关的话,我们就会用到componentWillReceiveProps(nextProps)。常见的业务场景比如,tabs的激活状态,一般我们会在组件自身内通过state维持,但是当我们从其他页面返回时,想要保持离开之前时的tabs状态,这时我们可以通过props来传递,(破坏了数据源的单一性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//previous 
componentWillReceiveProps(nextProps, nextContext) {
if(nextProps.activeIndex !== this.state.activeIndex) {
this.setState({activeIndex: nextProps.activeIndex})
this.fetchData() //因异步中断,可能会重复操作
}
}

/** next
*将更新state与触发逻辑的操作分成两部分来执行,state更新部分在getDerivedStateFromProps中完成, 逻辑部分操作
*在componentDidUpdate()中完成
*/
static getDerivedStateFromProps(nextProps, prevState) { //此方法不能获取组件实例
if(nextProps.activeIndex !== prevState.activeIndex) {
return {activeIndex: nextProps.activeIndex}
}
}
componentDidUpdate() {
this.fetchData() //可以确保只执行一次
}

该生命周期函数按照上面图谱中应该是在props属性改变之后调用,但其实只要父组件重新渲染,无论子组件的props有没有更新,子组件都会调用componentWillReceiveProps 注意这里可能会造成死循环,即当子组件在该方法中调用了父组件通过props传递过来的函数时, 恰巧该函数中有能让父组件重新渲染的逻辑,就会造成死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Parent extends Component {
render() {
return (
<div>
{/* 迫使父组件更新, 子组件就会调用componentWillReceiveProps */}
<div onClick={() => this.forceUpdate()}> re-render </div>
<Child parentFun={() => this.setState({})} />
</div>
)
}
}

class Child extends Component {
componentWillReceiveProps(nextProps, nextContext) {
{/* 子组件调用了父组件通过props传递过来的函数,该函数会使父组件重新渲染,造成死循环 */}
nextProps.parentFun()
}
render() {
return (
<div> child component </div>
);
}
}

componentWillUpdate

getDerivedStateFromProps

使用getDerivedStateFromProps代替了旧的componentWillReceiveProps及componentWillMount

getDerivedStateFromProps是一个静态方法,在挂载和更新阶段时调用,可以返回一个对象来更新状态或者返回null不更新。

优点

  1. getDSFP是静态方法,在这里不能使用this,也就是一个纯函数,开发者不能写出副作用的代码

  2. 开发者只能通过prevState而不是prevProps来做对比,保证了state和props之间的简单关系以及不需要处理第一次渲染时prevProps为空的情况

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate代替了旧的componentWillUpdate。

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 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
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动​​位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

getDerivedStateFromError

1
static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state

1
2
3
4
5
6
7
8
9
10
11
12
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以显降级 UI return { hasError: true }; }
render() {
if (this.state.hasError) { // 你可以渲染任何自定义的降级 UI return <h1>Something went wrong.</h1>; }
return this.props.children;
}
}

forceUpdate

默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。

调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。

核心概念

jsx

条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
//或者
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
return (
{isLoggedIn?<UserGreeting />:<GuestGreeting />}
)
}
ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);

列表 & Key

key为每个元素加上唯一的标识,这样在执行diff的时候会加快位置的确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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')
);

在 JSX 中嵌入 map()

1
2
3
4
5
6
7
8
9
10
11
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}

key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。

如果使用数组索引,那么在对dom进行添加或删除,会出问题

页面渲染好了之后,3 个 input 输入框依次输入随机内容,当我们用 index 作为 key 的时候,点击删除第一项按钮会发现,左侧文字正确改变,input 输入框最后一项没了,这不是我们希望的样子。 因为当我们使用 index 作为 key 时,此时 key 为 0、1、2,删掉第一项后 key 变为 0、1,此时 react 在执行 diff 算法过程中,任务 key=0 存在,只需要更新子节点的值,所以左侧的 name 成功改变,而 input 的值非受控,不会更新。同时在对比计算中少了 key=2 这项,删除了最后一项。

添加样式的方式

第一种:行内样式

想给虚拟dom添加行内样式,需要使用表达式传入样式对象的方式来实现:

1
2
// 注意这里的两个括号,第一个表示我们在要JSX里插入JS了,第二个是对象的括号
<p style={{color:'red', fontSize:'14px'}}>Hello world</p>

动态添加样式

1
<div style={{display: (index===this.state.currentIndex) ? "block" : "none"}}>此标签是否隐藏</div>
1
<div className={index===this.state.currentIndex?"active":null}>此标签是否选中</div>

第二种:内嵌样式

1
<style>{`.operafor4{margin-top:42px !important}`}</style>

第三种:css modules

https://www.ruanyifeng.com/blog/2016/06/css_modules.html

CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。例如:父组件和子组件使用相同的class,父组件的class会覆盖子组件的样式

产生局部作用域的唯一方法,就是使用一个独一无二的class的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。

css Modules 添加多个className

1
<div className={`${styles.sAll} ${styles.s1}`}>aaaaaa</div>

CSS Modules 允许使用:global(.className)的语法,声明一个全局规则。

1
2
3
4
5
6
7
.title {
color: red;
}

:global(.title) {
color: green;
}

第四种:样式组件(styled-components)

styled-components是针对React写的一套css-in-js框架,简单来讲就是在js中写css。
styled-components是一个第三方包,要安装。Material框架中的样式也是如此

表单和受控组件

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

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 NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) { this.setState({value: event.target.value}); }
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

对于受控组件来说,输入的值始终由 React 的 state 驱动

props

1
2
3
4
5
6
7
function Welcome(props) {  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;ReactDOM.render(
element,
document.getElementById('root')
);

defaultProps

无论是函数组件还是 class 组件,都拥有 defaultProps 属性。可以通过配置特定的 defaultProps 属性来定义 props 的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}

// 指定 props 的默认值:
Greeting.defaultProps = {
name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);

PropTypes类型检查

PropTypes 进行类型检查,可用于确保组件接收到的props数据类型是有效的

导入包

1
import PropTypes from 'prop-types';

编写组件

1
2
3
4
5
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}

新增类型检查

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
Greeting.propTypes = {
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,

// 任何可被渲染的元素(包括数字、字符串、元素或数组)
// (或 Fragment) 也包含这些类型。
optionalNode: PropTypes.node,

// 一个 React 元素。
optionalElement: PropTypes.element,

// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,

// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),

// 你可以让你的 prop 只能是特定的值,指定它为
// 枚举类型。
optionalEnum: PropTypes.oneOf(['News', 'Photos']),

// 一个对象可以是几种类型中的任意一个类型
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),

// 可以指定一个数组由某一类型的元素组成
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// 可以指定一个对象由某一类型的值组成
optionalObjectOf: PropTypes.objectOf(PropTypes.number),

// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),

// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact({
name: PropTypes.string,
quantity: PropTypes.number
}),

// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,

// 任意类型的数据
requiredAny: PropTypes.any.isRequired,

// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
// 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},

// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
// 它应该在验证失败时返回一个 Error 对象。
// 验证器将验证数组或对象中的每个值。验证器的前两个参数
// 第一个是数组或对象本身
// 第二个是他们当前的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
}

this.props.children

将一个组件写在另一个组件的内容中,然后在外层组件中通过 this.props.children来接收内容中的组件

如果当前组件没有子节点,它就是 undefined ;
如果有一个子节点,数据类型是 Object;
如果有多个子节点,数据类型就是 Array。

setState

https://juejin.cn/post/6850418109636050958

https://juejin.cn/post/6959885030063603743#heading-0

1
2
3
4
5
6
7
state = {
number:1
};
componentDidMount(){
this.setState({number:3})
console.log(this.state.number)
}

img

setState是一个异步方法,如果每次调用setState都会触发更新,那么性能消耗就大,异步操作是为了提高性能,将多个状态更新合并一起进行批量更新,减少re-render调用。 setState() 视为请求而不是立即更新组件的命令。

1
2
3
for ( let i = 0; i < 100; i++ ) {
this.setState( { num: this.state.num + 1 } );
}

如果setState是一个同步执行的机制,那么这个状态会被重新渲染100次,这对性能是一个相当大的消耗。

React会将多个setState的调用合并为一个来执行,也就是说,当执行setState的时候,state中的数据并不会马上更新

回调函数

setState提供了一个回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据。还是以刚才的例子做示范:

1
2
3
4
5
6
7
8
9
state = {
number:1
};
componentDidMount(){
this.setState({number:3},()=>{
console.log(this.state.number)
})
}
复制代码

img

总结:

  • setState本身并不是异步(不会立即更新state的结果),只是因为react的性能优化机制体现为异步。在react的生命周期函数或者作用域下为异步,在原生的环境下为同步。

  • React18之前,react 无法对 setTimeout 的代码前后加上事务逻辑(除非 react 重写 setTimeout)。

    所以当遇到 setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调时,react 都是无法控制的(可以使用手动批处理)

    • 在setTimeout,Promise.then等异步事件中。setState和useState是同步执行的(立即更新state的结果)

ref

react的核心思想是虚拟DOM。react包含了生成虚拟DOM的函数react.createElement,及Component类。而react-dom包的核心功能就是把这些虚拟DOM渲染到文档中变成实际DOM。

原生JS获取Dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Component} from "react";
class App extends Component {
//定义获取Dom的函数
handleGetDom(){
let title = document.querySelector('#title');
console.log(title);
title.style.background = 'skyblue'
}
render() {
return (
<>
<h1 id="title">测试节点</h1>
<button onClick={this.handleGetDom}>点击操作Dom</button>
</>
)
}
}
export default App;

Ref

使用场景

  • 对Dom元素的焦点控制、内容选择、控制
  • 对Dom元素的内容设置及媒体播放
  • 对Dom元素的操作和对组件实例的操作
  • 集成第三方 DOM 库

回调 Ref

支持在函数组件和类组件内部使用

使用回调 refs需要将回调函数赋值给 React元素 的 ref 属性。这个函数接受 React 组件 或 HTML 元素作为参数,将其挂载到实例属性上

React 会在组件挂载时,调用 ref 回调函数并传入 DOM元素,当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 Refs 一定是最新的。

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
//类组件
import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = null;
this.setTextInputRef = (ele) => {
this.inputRef = ele;
}
this.setSonRef = (ele) => {
this.sonInfo = ele;
}
}
componentDidMount() {
this.inputRef && this.inputRef.focus();//获取input DOM
this.sonInfo.getSonInfo()//父组件执行子组件函数
}
render() {
return (
<input type="text" ref={this.setTextInputRef}/>
<MyInput ref={this.setSonRef}/>
)
}
}
//
function MyInput(props) {
state = {
info: {}
}
getSonInfo(){
console.log(this, state.info)
}
return <input type="text" ref={props.inputRef} />;
}
1
2
3
4
5
6
7
8
9
10
11
12
//函数组件
let tooltipRefs: any = {};
const Emotion: FC<OnlyProps> = (props) => {
const DraggableBox = ({ id, index }) => {
return (
<div>
<Tooltip
ref={ref => tooltipRefs[key] = ref}
>
</div >
)
};

createRef

支持在类组件中使用

Refs API使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
//分配给实例属性
this.inputRef = React.createRef(null);
}
componentDidMount() {
//通过 this.inputRef.current 获取对该节点的引用
this.inputRef && this.inputRef.current.focus();
}
render() {
//把 <input> ref 关联到构造函数中创建的 `inputRef` 上
return (
<input type="text" ref={this.inputRef}/>
)
}
}

ref 的值根据节点的类型而有所不同:

一、当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。

二、当 ref 属性用于自定义 class 组件时,ref 对象接收组件的 挂载实例 作为其 current 属性。

三、不能挂载到函数组件上,因为函数组件没有实例(instance)

但是,你可以在函数式组件中使用ref属性,就像你引用DOM元素和类组件一样。

useRef

只能在函数组件中使用

区别:https://zhuanlan.zhihu.com/p/105276393

useRef 用法类似于React.createRef(),区别:

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。useRef 返回的 ref 对象在组件的整个生命周期内保持不变。useRef 不仅仅是用来管理 DOM ref 的,它还相当于 this , 可以存放任何变量。useRef 可以很好的解决闭包带来的不方便性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useRef } from "react";
export default function UseRefHookExample() {
let inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
使用 useRef() hook:
<br />
<input type="text" ref={inputRef} />
<button onClick={handleClick}>
Click
</button>
</div>
);
}

useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])
  • 通过useImperativeHandle可以只暴露特定的操作
    • 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
    • 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
  • useImperativeHandle使用简单总结:
    • 作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
    • 参数1: 父组件传递的ref属性
    • 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
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
import React, { useRef, forwardRef, useImperativeHandle } from 'react'

const JMInput = forwardRef((props, ref) => {
const inputRef = useRef()
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return <input type="text" ref={inputRef} />
})

export default function ImperativeHandleDemo() {
// useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
// 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
// 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
const inputRef = useRef()

return (
<div>
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<JMInput ref={inputRef} />
</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
import React, { forwardRef, useImperativeHandle, useEffect, useRef } from 'react'

const TestRef = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
open() {
console.log("open")
}
}))
})
//或者
const TestRef = ((props) => {
const { ref } = props;
useImperativeHandle(ref, () => ({
open() {
console.log("open")
}
}))
})
function App () {
const ref = useRef()
useEffect(() => {
ref.current.open()
},[])

return(
<>
<div>石小阳</div>
<TestRef ref={ref}></TestRef>
</>
)
}
export default App

forwardRef 转发/传递

React.forwardRef是转发ref 获取组件内的DOM节点 ,

以下两种场景中特别有用:

  • 转发 refs 到 DOM 组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import React from 'react';

    const MyInput = React.forwardRef((props, ref) => {
    return (
    <input type="text" ref={ref} {...props} />
    )
    });
    function Form() {
    const inputRef = React.useRef(null);//class组件用createRef
    React.useEffect(() => {
    console.log(inputRef.current);//input节点
    })
    return (
    <MyInput ref={inputRef} />
    )
    }
    1. 调用 React.useRef 创建了一个 React ref 并将其赋值给 ref 变量。
    2. 指定 ref 为JSX属性,并向下传递 <MyInput ref={inputRef}>
    3. React 传递 refforwardRef 内函数 (props, ref) => ... 作为其第二个参数。
    4. 向下转发该 ref 参数到 <button ref={ref}>,将其指定为JSX属性
    5. ref 挂载完成,inputRef.current 指向 input DOM节点
  • 在高阶组件中转发 refs

findDOMNode()

当组件加载到页面上之后(mounted),你都可以通过 react-dom 提供的 findDOMNode() 方法拿到组件对应的 DOM 元素。

1
2
3
4
5
6
import { findDOMNode } from 'react-dom';

// Inside Component class
componentDidMound() {
const el = findDOMNode(this);
}

findDOMNode() 不能用在无状态组件上。

事件处理

  • react 事件的命名采用小驼峰式(camelCase),而不是纯小写。

  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

  • 不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault

    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
    class Toggle extends React.Component {
    constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
    this.setState(state => ({
    isToggleOn: !state.isToggleOn
    }));
    }

    render() {
    return (
    <button onClick={this.handleClick}>
    {this.state.isToggleOn ? 'ON' : 'OFF'}
    </button>
    );
    }
    }

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

在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined

绑定this:

  • 在constructor中用bind绑定

  • 箭头函数

  • 回调中使用箭头函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class LoggingButton extends React.Component {
    handleClick() {
    console.log('this is:', this);
    }

    render() {
    // 此语法确保 `handleClick` 内的 `this` 已被绑定。
    return (
    <button onClick={() => this.handleClick()}>
    Click me
    </button>
    );
    }
    }

组件

组件渲染机制

Componentstate改变,props改变,调用this.setState({...}),的时候都会进行渲染

  • 强制React组件重新渲染

    使用React的forceUpdate函数

    这是一个最明显的方式。在React类组件中,你可以通过调用这个方法,强制重渲染一个组件:

    1
    this.forceUpdate();

    在React hooks中强制更新组件

    在React hooks中,forceUpdate函数是无法使用的。你可以使用如下方式强制更新组件,并且不更改组件的state:

    1
    2
    const [state, updateState] = React.useState();
    const forceUpdate = React.useCallback(() => updateState({}), []);
  • shouldComponentUpdate

    1
    2
    3
    4
    5
    6
    7
    shouldComponentUpdate(nextProps,nextState) {    
    if(this.state.name === nextState.name) {
    return false
    }else {
    return true
    }
    }
  • 通过memo来判断指定的参数变化更新组件

  • componentWillReciveProps

状态组件

状态组件对比

使用 function 创建的组件,叫做【无状态组件】;使用 class 创建的组件,叫做【有状态组件】

  • 使用 function 构造函数创建的组件,内部没有 state 私有数据,只有一个props来接收外界传递过来的数据
  • 使用 class创建的组件,内部,除了有 this.props 这个只读属性之外,还有一个 专门用于 存放自己私有数据的this.state 属性,这个 state 是可读可写的!

有状态组件和无状态组件,最本质的区别:

  • 有无 state 属性;

  • class 创建的组件,有自己的生命周期函数,但是,function 创建的 组件,没有自己的生命周期函数;

问题来了:什么时候使用 有状态组件,什么时候使用无状态组件呢???

  1. 如果一个组件需要存放自己的私有数据,或者需要在组件的不同阶段执行不同的业务逻辑,此时,非常适合用 class 创建出来的有状态组件;
  2. 如果一个组件,只需要根据外界传递过来的 props,渲染固定的 页面结构就完事儿了,此时,非常适合使用 function 创建出来的 无状态组件;(使用无状态组件的小小好处: 由于剔除了组件的生命周期,所以,运行速度会相对快一丢丢)

class组件

用class关键字创建出来的组件:“有状态组件”

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
// 使用 class 创建的类,通过 extends 关键字,继承了 React.Component 之后,这个类,就是一个组件的模板了
// 如果想要引用这个组件,可以把 类的名称, 以标签形式,导入到 JSX 中使用
export default class Hello2 extends React.Component {
constructor(props) {
// 注意: 如果使用 extends 实现了继承,那么在 constructor 的第一行,一定要显示调用一下 super()
// super() 表示父类的构造函数
super(props)
// 在 constructor 中,如果想要访问 props 属性,不能直接使用 this.props, 而是需要在 constructor 的构造器参数列表中,显示的定义 props 参数来接收,才能正常使用;
// 注意: 这是固定写法,this.state 表示 当前组件实例的私有数据对象,就好比 vue 中,组件实例身上的 data(){ return {} } 函数
this.state = {
msg: '这是 Hello2 组件的私有msg数据',
info: '瓦塔西***'
}
}
// 保存信息1: No `render` method found on the returned component instance: you may have forgotten to define `render`.
// 通过分析以上报错,发现,提示我们说,在 class 实现的组件内部,必须定义一个 render 函数
render() {
// 报错信息2: Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
// 通过分析以上报错,发现,在 render 函数中,还必须 return 一个东西,如果没有什么需要被return 的,则需要 return null

// 虽然在 React dev tools 中,并没有显示说 class 组件中的 props 是只读的,但是,经过测试得知,其实 只要是 组件的 props,都是只读的;
// this.props.address = '123'
return <div>
<h1>这是 使用 class 类创建的组件</h1>
<h3>外界传递过来的数据是: {this.props.address} --- {this.props.info}</h3>
<h5>{this.state.msg}</h5>

//React中,提供的事件绑定机制,使用的 都是驼峰命名
// 在为 React 事件绑定 处理函数的时候,需要通过 this.函数名, 来把 函数的引用交给 事件
<input type="button" value="修改 msg" id="btnChangeMsg" onClick={this.changeMsg} />
<br />
</div>
}

changeMsg = () => {
// 注意: 这里不是传统网页,所以 React 已经帮我们规定死了,在 方法中,默认this 指向 undefined,并不是指向方法的调用者

// 直接使用 this.state.msg = '123' 为 state 上的数据重新赋值,可以修改 state 中的数据值,但是,页面不会被更新;
// 所以这种方式,React 不推荐,以后尽量少用;
// 如果要为 this.state 上的数据重新赋值,那么,React 推荐使用 this.setState({配置对象}) 来重新为 state 赋值
// 注意: this.setState 方法,只会重新覆盖那些 显示定义的属性值,如果没有提供最全的属性,则没有提供的属性值,不会被覆盖;
/* this.setState({
msg: '123'
}) */

// this.setState 方法,也支持传递一个 function,如果传递的是 function,则在 function 内部,必须return 一个 对象;
// 在 function 的参数中,支持传递两个参数,其中,第一个参数是 prevState,表示为修改之前的 老的 state 数据
// 第二个参数,是 外界传递给当前组件的 props 数据
this.setState(function (prevState, props) {
return {
msg: '123'
}
}, function () {
// 由于 this.setState 是异步执行的,所以,如果想要立即拿到最新的修改结果,最保险的方式, 在回调函数中去操作最新的数据
console.log(this.state.msg)
})
}
}

函数组件

函数/无状态组件是一个纯函数,它可接受接受参数,并返回react元素。这些都是没有任何副作用的纯函数。这些组件没有状态或生命周期方法

1
2
3
4
5
6
7
8
// 组件的首字母必须是大写
function Hello(props) {
// 在组件中,如果想要使用外部传递过来的数据,必须,显示的在 构造函数参数列表中,定义 props 属性来接收;
// 通过 props 得到的任何数据都是只读的,不能从新赋值
return <div>
<h1>这是在Hello组件中定义的元素 --- {props.name}</h1>
</div>
}

内置组件

PureComponent

shouldComponentUpdate模拟

https://blog.csdn.net/deng1456694385/article/details/88746797

1
2
3
4
5
6
7
8
9
10
11
12
class demo extent Component {
state = {
name: ''
}
componentDidMount() {
this.setState({name: ''})
}
render() {
console.log('render')
return <div>haha</div>
}
}

上面的组件会在this.setState调用后就会重新传染一次,但是我们可以看出name状态并没有没被我们用到,也没有改变,这种渲染就是无效渲染,所以为了优化我们通常会使用钩子函数shouldComponentUpdate来做一些逻辑判断,来确定是否要重新render一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class demo extent Component {    
state = {
name: ''
}
componentDidMount() {
this.setState({name: ''})
}
shouldComponentUpdate(nextProps,nextState) {
if(this.state.name === nextState.name) {
return false
}else {
return true
}
}
render <div>haha</div>
}

这样就可以避免无效渲染,优化性能,但是如果这种判断逻辑多到一定程度,光判断逻辑就很复杂,而且每次都要判断也会影响性能,所以才有了 PureComponent,**PureComponent的区别在于相当于自己写了一个shouldComponentUpdate钩子函数处理, 对propsstate进行浅比较,所谓浅比较就是之比较内部第一层的各个属性的值是否相同,像对象和数组这种数据类型,如果只改变内部的元素,就不会造成渲染**

PureComponent的浅比较

浅比较通过一个shallowEqual函数来完成:

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
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 首先对基本数据类型的比较
// !! 若是同引用便会返回 true
//其中is函数是自己实现的一个Object.is的功能,排除了===两种不符合预期的情况:
// +0 === -0 // true
// NaN === NaN // false
if (is(objA, objB)) {
return true;
}
// 只有一种情况是误判的,那就是object,所以在判断两个对象都不是object
// 之后,就可以返回false了
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
// 过滤掉基本数据类型之后,就是对对象的比较了
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

// 首先拿出key值,对key的长度进行对比
if (keysA.length !== keysB.length) {
return false;
}

// key相等的情况下,在去循环比较
for (let i = 0; i < keysA.length; i++) {
// key值相等的时候
// 借用原型链上真正的 hasOwnProperty 方法,判断ObjB里面是否有A的key的key值
// 属性的顺序不影响结果也就是{name:'daisy', age:'24'} 跟{age:'24',name:'daisy' }是一样的
// 最后,对对象的value进行一个基本数据类型的比较,返回结果
if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
return false;
}
}
return true;
}
Component vs PureComponent 总结

PureComponent相较于Component区别就是,对props和state默认进行判断来确定是否渲染,从而减少无效渲染次数. 大部分情况下直接用PureComponent比较好可以提高性能,但是如果遇到需要频繁修改值重新渲染的组件,用Component比较好,因为PureComponent频繁的判断也会影响性能.

memo

针对函数组件的,减少组件的不必要更新。 React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TextCell = memo(function(props:any) {
console.log('我重新渲染了')
return (
<p onClick={props.click}>ffff</p>
)
})

//父组件
const fatherComponent = () => {
const [number,setNumber] = useState(0);
return(
<div>
模块{number}
<TextCell/>
<Button onClick={()=>setNumber(number => number + 1)}>加加加</Button>
</div>
)
}

在这里如果没有用到memo 每次父组件重新setNumber,子组件都会重新渲染一次,加上了后只会在初始化的时候渲染(useMemo会在页面初始化的时候执行一次,并把执行的结果缓存一份),减少了子组件渲染的次数

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

1
2
3
4
5
6
7
8
9
10
11
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);

Fragment

无论是函数组件还是类组件,return 的 React 元素的语法必须是由一个标签包裹起来的所有虚拟 DOM 内容

一种是使用一个 div 标签将其包裹起来,另外一种方式就是使用 React 提供的 <React.Fragment> 将其包裹起来。但是我们不期望,增加额外的dom节点,所以react提供Fragment碎片概念,能够让一个组件返回多个元素。

1
2
3
4
5
6
7
8
render() {
return (
<React.Fragment>
Some text.
<h2>A heading</h2>
</React.Fragment>
);
}

组件通信

props

适用于父子组件通信

父组件->子组件

父组件将需要传递的参数通过key={xxx}方式传递至子组件,子组件通过this.props.key获取参数.

子组件->父组件

利用 props callback 通信,父组件传递一个 callback 到子组件,当事件触发时将参数放置到 callback 带回给父组件.

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 from 'react'
import Son from './son'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
callback = (value) => {
// 此处的value便是子组件带回
this.setState({
info: value,
})
}
render() {
return (
<div>
<p>{this.state.info}</p>
<Son callback={this.callback} />
</div>
)
}
}
export default Father

// 子组件
import React from 'react'
interface IProps {
callback: (string) => void
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange = (e) => {
// 在此处将参数带回
this.props.callback(e.target.value)
}
render() {
return (
<div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son

Context

https://zh-hans.reactjs.org/docs/context.html

数据是通过 props 属性自上而下(由父及子)进行传递的 ,需要显式地通过组件树的逐层传递 props。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
// context.js
import React from 'react'
//创建一个 Context 对象,并暴露Consumer和Provide
const { Consumer, Provider } = React.createContext(null)
export { Consumer, Provider }
//Father
import { Provider } from './context'
import React from 'react'
import Son from './son'
<Provider value={this.state.info}>
<div>
<p>{this.state.info}</p>
<Son />
</div>
</Provider>

//Son
import React from 'react'
import GrandSon from './grandson'
import { Consumer } from './context'
<Consumer>
{(info) => (
// 通过Consumer直接获取父组件的值
<div>
<p>父组件的值:{info}</p>
<GrandSon />
</div>
)}
</Consumer>

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextTypeuseContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

高阶函数与组件

高阶组件即高阶函数,前面我们讲到,React遵循函数式开发,而高阶组件这个概念其实是React社区繁衍出来的概念。

在这里我们要谨记这一句话,组件 = 函数

高阶函数,通俗的讲,就是把函数当作参数,传入另外一个函数当中,再返回一个函数。

实际应用场景

权限按钮

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
import React, { FC } from 'react';
import { useAccess } from '../../../hooks/useAccess';
import { message } from 'antd';

/**
* 权限高阶组件,使用示例:
*
* import WithAccess from '@components/WithAccess';
*
* const WithAccessBtn = WithAccess(你的组件, 可选'button' | 'menu' 默认为button);
*
* <WithAccessBtn permission='permission' />
*
* @param Comp 组件
* @param type 鉴权类型 按钮:button,菜单:menu
* @returns
*/
const WithAccess = (Comp, type = 'button') => {
const Access = props => {
const { getPermission } = useAccess();
const { permission, name, icon, onClick } = props;
//showVisible是否展示, available是否有权限
const { showVisible, available } = getPermission(permission, type) || {};
let initProps = props
console.log(props);
const config = () => {
if (available === 0) {
return {
onClick: () => {
message.info('按钮没有权限')
}
}
}
}
return showVisible ? <Comp {...initProps} {...config()}>{name}</Comp> : null;
}

return Access;
}

export default WithAccess;

使用高阶组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import usePermissionModel from "../../hox/access";
import WithAccess from './components'
import { Button, message } from 'antd';
import { LaptopOutlined } from "@ant-design/icons";

const WithAccessBtnYes = WithAccess(Button)
const WithAccessBtnNo = WithAccess(Button)
export default function AHooks(props) {
const { menus, set } = usePermissionModel();
console.log(menus, set)
return <div>
<WithAccessBtnYes permission='account:authorization:yes' name='按钮' icon={<LaptopOutlined />} onClick={() => { message.success('按钮有权限') }}></WithAccessBtnYes>
<WithAccessBtnNo permission='account:authorization:no' name='按钮' icon={<LaptopOutlined />} onClick={() => { message.success('按钮有权限') }}></WithAccessBtnNo>
</div>;
}

Hooks函数

http://www.ruanyifeng.com/blog/2019/09/react-hooks.html

  • 纯函数组件没有状态
  • 纯函数组件没有生命周期
  • 纯函数组件没有this

这就注定,我们所推崇的函数组件,只能做UI展示的功能,涉及到状态的管理与切换,我们不得不用类组件或者redux,但我们知道类组件的也是有缺点的,比如,遇到简单的页面,你的代码会显得很重,并且每创建一个类组件,都要去继承一个React实例,至于Redux,更不用多说,很久之前Redux的作者就说过,“能用React解决的问题就不用Redux”,等等一系列的话。关于React类组件redux的作者又有话说

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 render props 和高阶组件。

Hooks 优势

  1. 能优化类组件的三大问题
  2. 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
  3. 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。而React Hooks 就是我们所说的“钩子”。

useState():状态钩子

用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

原理

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
//useState模拟1.0
//因为每次调用myUseState时会重置state的值。经过改进,必须将state写在函数的外面。
let _state;
function myUseState(initialValue) {
_state = _state===undefined? initialValue:_state;
const setState = (newValue) => {
_state = newValue; //更新state值,
render(); //触发重新渲染
};
return [_state, setState];
}
/* 粗糙的渲染 */
const render = () => {
ReactDOM.render(<App />, document.getElementById("root"));
};
// 使用myUseState
const App = () => {
const [n, setN] = myUseState(0);
return (
<div classNam="App">
<p>n:{n}</p>
<button onClick={()=>{setN(n+1)}}>n+1</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));

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
//一个组件用了两个useState怎么办?useState模拟2.0
let _state=[];
let index=0;
function myUseState(initialValue) {
int currentIndex=index; //引入中间变量currentIndex就是为了保存当前操作的下标index。
_state[currentIndex] = _state[currentIndex]===undefined? initialValue:_state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index+=1;// 每次更新完state值后,index值+1
return [_state[currentIndex], setState];
}
const render = () => {
index=0; //重要的一步,必须在渲染前后将index值重置为0,不然index会一种增加1
ReactDOM.render(<App />, document.getElementById("root"));
};
// 使用myUseState
const App = () => {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
return (
<div classNam="App">
<p>n:{n}</p>
<button onClick={()=>{setN(n+1)}}>n+1</button>
<p>m:{m}</p>
<button onClick={()=>{setM(m+1)}}>n+1</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
  • 在正常的react的事件流里(如onClick等)

    • setState和useState是异步执行的(不会立即更新state的结果,所以console数据没有更新)

    • 多次执行setState和useState,只会调用一次重新渲染render

    • 不同的是,setState会进行state的合并,而useState会进行state的覆盖

  • 在setTimeout,Promise.then等异步事件中

    • setState和useState是同步执行的(立即更新state的结果,react17之后还是会批处理

    • 多次执行setState和useState,每一次的执行setState和useState,都会调用一次render

批处理

batch批量处理:在每次执行 useState 的时候,组件都要重新 render 一次,会造成无效渲染,浪费时间(因为最后一次渲染会覆盖掉前面所有的渲染效果)。 所以 react 会把一些可以一起更新的 useState/setState 放在一起,只渲染一次。

在React16版本及以前,React 会对所有React内部触发的事件监听函数中的更新(比如onClick函数)做批处理,如果是绕过react组件,如addEventListenr,或者异步调用如异步请求或者setTimeout等,不会进行批处理。在React17版本及之后,React会对所有的更新做批处理。

unstable_batchedUpdates手动批处理

1
2
3
4
5
6
7
8
9
10
11
function handleClick3() {
// 手动批处理
setTimeout(() => {
unstable_batchedUpdates(() => {
setCount1(count1 + 1);
console.log(count1);
setFlag((f) => !f);
});
}, 10);
// React 只会在最后重新渲染一次(这是批处理!)
}

Tip

  • react中useState更新了组件,但是页面上的组件没有刷新

    原因:useState更新的数据,是一个多层次的数据,react监听的时候,是浅层监听(默认开启 类 Object.is 的浅层比较,所以指向的地址不变),所以不一定及时刷新页面

    解决办法:深拷贝,把需要更新的数据深拷贝一份,再使用useState 存储,就能实现每次都及时更新页面

useContext():共享状态钩子

如果需要在层层组件之间共享状态,可以使用useContext()

第一步就是使用 React Context API,在组件外部建立一个 Context。

1
const AppContext = React.createContext({});

组件封装代码如下。

1
2
3
4
5
6
7
8
9
10
 // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>

上面代码中,AppContext.Provider提供了一个 Context 对象,这个对象可以被子组件共享。

Navbar 组件的代码如下。

1
2
3
4
5
6
7
8
9
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}

useReducer()钩子

useReducer适用于引用类型,而useState适合值类型

1
const [state, dispatch] = useReducer(reducer, initialArg, init)
  • useReducer 接收三个参数,第一个参数为一个 reducer 函数,第二个参数是reducer的初始值,第三个参数为可选参数,值为一个函数,可以用来惰性提供初始状态。

    reducer 接受两个参数一个是 state 另一个是 action ,用法原理和 redux 中的 reducer 一致

  • useReducer 返回一个数组,数组中包含一个 state 和 dispath,state 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的函数。

原理

useReucer 也是 useState 的内部实现

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
let memoizedState
function useReducer(reducer, initialArg, init) {
let initState = void 0
if (typeof init === 'function') {
initState = init(initialArg)
} else {
initState = initialArg
}
function dispatch(action) {
memoizedState = reducer(memoizedState, action)
// React的渲染
// render()
}
memoizedState = memoizedState || initState
return [memoizedState, dispatch]
}

function useState(initState) {
return useReducer((oldState, newState) => {
if (typeof newState === 'function') {
return newState(oldState)
}
return newState
}, initState)
}

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const initialState = {count: 0};

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, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

useEffect():副作用钩子

纯函数只能进行数据计算,那些不涉及计算的操作(比如ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志)应该写在哪里呢?

函数式编程将那些跟数据计算无关的操作,都称为 “副效应(side effect)

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

useEffect()的用法如下:

1
2
3
4
5
useEffect(()  =>  {
// Async Action
//return 则是在页面被卸载时调用.返回一个函数来指定如何“清除”副作用
return fn;
}, [dependencies])

上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()

它的常见用途有下面几种:

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

tips

  • 它在第一次渲染之后每次更新之后都会执行

  • 使用useEffect()时,有一点需要注意。如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起。

  • 在useEffect中,不仅会请求后端的数据,还会通过调用setData来更新本地的状态,这样会触发view的更新。

    但是,运行这个程序的时候,会出现无限循环的情况。useEffect在组件mount时执行,但也会在组件更新时执行。因为我们在每次请求数据之后都会设置本地的状态,所以组件会更新,因此useEffect会再次执行,因此出现了无限循环的情况。我们只想在组件mount时请求数据。我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新执行useEffect,只会在组件mount时执行。

    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
    import React, { useState, useEffect } from 'react';
    import axios from 'axios';

    function App() {
    const [data, setData] = useState({ hits: [] });

    useEffect(async () => {
    const result = await axios(
    'http://localhost/api/v1/search?query=redux',
    );

    setData(result.data);
    }, []);

    return (
    <ul>
    {data.hits.map(item => (
    <li key={item.objectID}>
    <a href={item.url}>{item.title}</a>
    </li>
    ))}
    </ul>
    );
    }

    export default App;

    升级加载loading

    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
    import React, { Fragment, useState, useEffect } from 'react';
    import axios from 'axios';

    function App() {
    const [data, setData] = useState({ hits: [] });
    const [query, setQuery] = useState('redux');
    const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
    );
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
    const fetchData = async () => {
    setIsLoading(true);

    const result = await axios(url);

    setData(result.data);
    setIsLoading(false);
    };

    fetchData();
    }, [url]);
    return (
    <Fragment>
    <input
    type="text"
    value={query}
    onChange={event => setQuery(event.target.value)}
    />
    <button
    type="button"
    onClick={() =>
    setUrl(`http://localhost/api/v1/search?query=${query}`)
    }
    >
    Search
    </button>

    {isLoading ? (
    <div>Loading ...</div>
    ) : (
    <ul>
    {data.hits.map(item => (
    <li key={item.objectID}>
    <a href={item.url}>{item.title}</a>
    </li>
    ))}
    </ul>
    )}
    </Fragment>
    );
    }

    export default App;

useCallback和useMemo

https://www.xiaye0.com/?p=113

https://www.jianshu.com/p/014ee0ebe959

useCallback和useMemo都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存useMemo返回缓存的变量,useCallback返回缓存的函数。

1
2
3
4
5
type DependencyList = ReadonlyArray<any>;

function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

React 中当组件的 props 或 state 变化时,会重新渲染视图

useCallback

父组件给子组件传递属性(函数),父组件重新渲染,会重新创建函数,对应函数地址改变,即传给子组件的属性发生了变化,导致子组件渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const TextCell = memo(function(props:any) {
console.log('我重新渲染了')
return (
<p onClick={props.click}>ffff</p>
)
})

//父组件
const fatherComponent = () => {
const [number,setNumber] = useState(0);

const handleClick = useCallback(()=>{
console.log(33)
},[])
return(
<div>
模块{number}
<TextCell click={handleClick}/>

<Button onClick={()=>setNumber(number => number + 1)}>加加加</Button>
</div>
)
}

这里如果不使用useCallback,哪怕子组件用memo包裹了 也还是会更新子组件,因为子组件的绑定的函数click在父组件更新的时候也会更新引用地址,导致子组件的更新,但是这个其实是没必要的更新,绑定的函数并不需要子组件更新,useCallback就是阻止这类没必要的更新而存在的

这里需要注意的是 如果是有参数需要传递,则需要这样写

1
<TextCell click={useCallback(()=>handleClick(‘传递的参数’),[])}/>

作用

  • 防止死循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 用于记录 getData 调用次数
    let count = 0;

    function App() {
    const [val, setVal] = useState("");

    function getData() {
    setTimeout(() => {
    setVal("new data " + count);
    count++;
    }, 500);
    }
    return <Child val={val} getData={getData} />;
    }

    function Child({val, getData}) {
    useEffect(() => {
    getData();
    }, [getData]);

    return <div>{val}</div>;
    }

    执行过程:

    1. App渲染Child,将valgetData传进去
    2. Child使用useEffect获取数据。因为对getData有依赖,于是将其加入依赖列表
    3. getData执行时,调用setVal,导致App重新渲染
    4. App重新渲染时生成新的getData方法,传给Child
    5. Child发现getData的引用变了,又会执行getData
    6. 3 -> 5 是一个死循环

useMemo

调试

useEventListener

如果你发现自己使用useEffect添加了许多事件监听,那你可能需要考虑将这些逻辑封装成一个通用的hook。

useWhyDidYouUpdate

这个hook让你更加容易观察到是哪一个prop的改变导致了一个组件的重新渲染。

useLockBodyScroll

有时候当一些特别的组件在你们的页面中展示时,你想要阻止用户滑动你的页面(想一想modal框或者移动端的全屏菜单)。

路由

路由是一种向用户显示不同页面的能力。 这意味着用户可以通过输入 URL 或单击页面元素在 WEB 应用的不同部分之间切换

这里需要说明一下 React Router 库中几个不同的 npm 依赖包,每个包都有不同的用途

相关组件 功能
react-router 实现了路由的核心功能,用作下面几个包的运行时依赖项(peer dependency)。
react-router-dom 基于 react-router 添加了浏览器运行环境的一些组件和功能。
react-router-native 适用于 React Native
react-router-redux React Router 和 Redux 的集成。
eact-router-config 提供可配置化的路由

React Routers三类组件

路由器Router

<BrowserRouter><HashRouter>,两者之间的主要区别是它们存储URL和与Web服务器通信的方式。

BrowserRouter

<BrowserRouter>使用常规的URL路径。但它们要求正确配置服务器。具体来说,您的Web服务器需要在所有由React Router客户端管理的URL上提供相同的页面

BrowserRouter提供了如下属性

  • basename (string) 当前位置的基准 URL。当应用程序放置于服务器上子目录中时,可以设置,比如 /public
  • forceRefresh (boolean),在导航的过程中整个页面是否刷新
  • getUserConfirmation (func),当导航需要确认时执行的函数。默认是:window.confirm
  • keyLength (number) location.key 的长度。默认是 6
  • children (node) 要渲染的子节点

HashRouter

<HashRouter>将当前位置存储在URLhash一部分中,因此URL看起来像http://example.com/#/your/page。由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置(在任意的路由进行页面的刷新都不会是 404)。

HashRouter提供了如下属性

  • basename: string, 同 <BrowserRouter>basename
  • getUserConfirmation: function, 同 <BrowserRouter>getUserConfirmation
  • hashType: string, Hash 编码类型,可选值 'slash'(默认) | 'noslash' | 'hashbang'
    • slash, 创建像 #/, #/user/1 这样的 hash 地址,默认值。
    • noslash, 创建像 #, #user/1 这样的 hash 地址
    • hashbang, 创建像 #!/, #!/user/1 这样的 ajax crawlable(已被 Google 遗弃) 的 hash 地址
  • children: node, 同 <BrowserRouter>children: node

原理

HashRouter:使用 URL 的哈希值实现

原理:监听 window 的 hashchange 事件来实现的

BrowserRouter(推荐):使用 H5 的 history.pushState() API 实现

原理:监听 window 的 popstate 事件来实现的

BrowserRouter组件都会创建一个 history 实例对象,它记录了当前的位置,还记录了堆栈中以前的位置。在当前位置发生变化时,页面会被重新渲染,于是你就有一种导航跳转的感觉。

那么如何改变当前的位置呢?也就是说如何做到导航跳转呢?这时候 history 的作用就来了,这个对象暴露了一些方法,比如 history.pushhistory.replace ,它们就可以拿来处理上面的问题。

当你点击一个 <Link> 组件时,history.push 就会被调用,而当你使用一个 <Redirect> 组件时,history.replace 就会被调用。其它的方法比如 history.Backhistory.Forward 可以用来在历史堆栈中回溯或前进。

参数

  • basename: string

    原因是:ngix服务器上面要放不止一个网站 根目录下面已经有一个网站,这个网站需单独建一个文件夹。

    作用:为所有位置添加一个基准URL
    使用场景:假如你需要把页面部署到服务器的二级目录,你可以使用 basename 设置到此目录。

路线匹配器Route

Route

内联函数

https://www.jianshu.com/p/76ee90125e9f

1
2
3
4
5
6
7
<span>
<button onClick={() => onRemoveItem(item)}>Dismiss</button>
</span>

<span>
<button onClick={handleRemoveItem}>Dismiss</button>
</span>

如果我们使用内联函数,则每次调用“render”函数时都会创建一个新的函数实例。

当 React 进行虚拟 DOM diffing 时,它每次都会找到一个新的函数实例;因此在渲染阶段它会会绑定新函数并将旧实例扔给垃圾回收。

因此直接绑定内联函数就需要额外做垃圾回收和绑定到 DOM 的新函数的工作。

三种渲染方式

https://www.cnblogs.com/ypSharing/p/15587340.html

优先级是 children > component > render。

  • <Route component>

    1
    2
    3
    <Route exact path="/home" component={Home} />   //推荐

    <Route exact path="/home" component={()=><Home />} /> // 内联函数

    参数:对象<Route path='/home' component={home}/>

    • 直接使用组件类–使用最多的方式
    • 缺点:不能把父组件中的数据通过props传递给路由组件中

    参数:函数<Route path='/home' component={()=><home/>} />

    • 使用函数,可以写条件判断,根据条件来渲染不同的组件

    • 可以通过props来完成父组件中的数据向路由渲染组件传递

    • 缺点:每次匹配路由成功都会从新创建组件—效率低下,不建议使用

      路由会使用React.createElement从指定的组件中创建一个新的React元素。这意味着,如果你向组件属性提供内置函数,则将在每个渲染中创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不是仅更新现有组件。使用内置函数进行内联渲染时,应使用renderchildren属性。

  • <Route render={(props)=>{return <component/>}}>

    • render方式渲染,使用函数方式

    • 如果匹配相同,则不重新创建,效率高

    • 建议如果组件对象方式渲染(函数方式)推荐使用render

      1
      2
      3
      4
      5
      6
      7
      <Route path='/home' render={(props)=>{
      if(this.state.count==1){
      return <Home1 count={this.state.count}/>
      }else{
      retutn <Home2/>
      }
      }}/>
  • <Route children>

    • 组件对象方式:必须匹配到path的路由规则才渲染和render与component一样
      <Route path="/about" children={<About />} />

    • 函数方式:不管是否和path匹配都渲染

      1
      2
      3
      4
      5
      6
      7
      // 在匹配时,容器的class是light,<Home />会被渲染
      // 在不匹配时,容器的class是dark,<About />会被渲染
      <Route path='/home' children={({ match }) => (
      <div className={match ? 'light' : 'dark'}>
      {match ? <Home/>:<About>}
      </div>
      )}/>

      一、它同 render 类似,是一个 function。不同的地方在于它会被传入一个 match 参数来告诉你这个 Route 的 path 和 location 匹配上没有。

      二、第二个特殊的地方在于,即使 path 没有匹配上,我们也可以将它渲染出来。秘诀就在于前面一点提到的 match 参数。我们可以根据这个参数来决定在匹配的时候渲染什么,不匹配的时候又渲染什么。

参数
  • exact 是否进行精确匹配,路由 /a 可以和 /a/、/a 匹配

    当exact为false时,根据路由匹配所有组件,例如/a/b/c 能匹配到/、/a、/a/b、/a/b/c 且匹配还是按顺序的

    例如路由设置的前后顺序为:
    1./ ;
    2./a;
    3./a/b ; 
    4./a/b/c
    且前3个路径都没有设置 exact,这样前3个组件都会被渲染并且默认将2当作1的子页面,3当作2的子页面

  • strict 是否进行严格匹配,指明路径只匹配以斜线结尾的路径,路由/a可以和/a匹配,不能和/a/匹配,相比 exact 会更严格些

  • path (string) 标识路由的路径,path属性可以使用通配符。

    1
    <Route path="/hello/:name">

    通配符的规则如下:

    • paramName

      :paramName匹配URL的一个部分,直到遇到下一个/?#为止。这个路径参数可以通过this.props.params.paramName取出。

    • ()

      ()表示URL的这个部分是可选的。

    • *

      *匹配任意字符,直到模式里面的下一个字符为止。匹配方式是非贪婪模式。

    • **

      ** 匹配任意字符,直到下一个/?#为止。匹配方式是贪婪模式。

  • component 表示路径对应显示的组件

  • location (object) 除了通过 path 传递路由路径,也可以通过传递 location 对象可以匹配

  • sensitive (boolean) 匹配路径时,是否区分大小写

Swtich

Swtich 就近匹配路由,仅渲染一个路由,路由的默认行为是匹配了就直接渲染

1
2
3
4
5
6
7
8
/// 假设你访问的URL为 /dog
<Route path='/dog' component={Dog}></Route> // 虽然这里匹配了,但不会停止查找
<Route path="/:dog" component={Husky}></Route> // 这个路由依然会被匹配,这样两个组件都会被渲染
...
<Switch>
<Route path='/dog' component={Dog}></Route> // Switch 匹配一个路由后就不会再去查找下一个路由,那么下面的路由就不会被匹配
<Route path="/:dog" component={Husky}></Route>
</Switch>

链接

<Link> 组件被用来在页面之间进行导航,它其实就是 HTML 中的 <a> 标签的上层封装,不过在其源码中使用 event.preventDefault 禁止了其默认行为,然后使用 history API 自己实现了跳转。我们都知道,如果使用 <a> 标签去进行导航的话,整个页面都会被刷新,这是我们不希望看到的。所以我们使用 <Link> 组件来导航到一个目标 URL,可以在不刷新页面的情况下重新渲染页面。

参数

  • to(string | object | function)

    • 为 string 时 就是一个明确的路径地址

      1
      <Link to="/courses?sort=name" />
    • 为 object 时有如下属性(就是一个location对象)

      1
      2
      3
      4
      5
      6
      7
      8
      <Link
      to={{
      pathname: "/courses",
      search: "?sort=name",
      hash: "#the-hash",
      state: { fromDashboard: true }
      }}
      />
      • pathname:URL路径。
      • search:URl中查询字符串。
      • hash:URL的hash分段,例如#a-hash。
      • state:表示location中的状态
    • 为 function 时,就是一个函数接收当前 location 为参数,然后以字符串或对象的形式返回位置形式

  • replace (boolean),当为 true 时,单击链接将替换历史堆栈中的当前记录,而不是添加一个新记录。

NavLink 功能与 Link 类似不过参数更多,并且可以设置被选中时的样式或者类

  • exact (boolean) 是否进行精确匹配

  • strict (boolean) 是否进行严格匹配

  • to(string | object) 需要跳转到的路径(pathname)或地址(location)

  • activeClassName (string) 是选中状态的类名,我们可以为其添加样式

    当激活(to 属性与当前 URL 匹配)时,会将这个 class 选择器名添加到元素上,默认值为 'active'

    1
    2
    3
    <NavLink to="/faq" activeClassName="selected">
    FAQs
    </NavLink>
  • activeStyle (Object) 元素处于选中状态时,应用于元素的样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <NavLink
    to="/faq"
    activeStyle={{
    fontWeight: "bold",
    color: "red"
    }}
    >
    FAQs
    </NavLink>
  • isActive(function) ,一个函数,用于添加额外的逻辑,以确定链接是否处于激活状态。如果您想做的不仅仅是验证链接的路径名是否与当前 URL 的路径名匹配,那么应该使用此方法来返回 truefalse

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <NavLink
    to="/events/123"
    isActive={(match, location) => {
    if (!match) {
    return false;
    }

    // only consider an event active if its event id is an odd number
    const eventID = parseInt(match.params.eventID);
    return !isNaN(eventID) && eventID % 2 === 1;
    }}
    >
    Event 123
    </NavLink>

Redirect

重定向,新位置将覆盖历史堆栈中的当前位置

from (string) 需要重定向的路径,可以包括动态参数

push (boolean) 为 true 时,重定向会将新条目推入历史记录,而不是替换当前条目

to (string | object) 重定向到的路径

exact (boolean) 是否要对 from 进行精确匹配

strict (boolean) 是否要对 from 进行严格匹配

sensitive (boolean) 匹配 from 时是否区分大小写

IndexRoute和IndexRedirect

Index Routes

通常情况下,我们会建立如下情况的路由:

1
2
3
4
5
6
<Router>
<Route path="/" component={App}>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>

当用户访问 / 时, App 组件被渲染,但组件内的子元素却没有, App 内部的 this.props.children 为 undefined 。 你可以简单地使用 {this.props.children ||} 来渲染一些默认的 UI 组件。

1
2
3
4
5
6
7
<Router>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>

如此配置后,我们再次访问 / 路由,你会发现页面渲染了 Home 组件的内容。这就是 IndexRoute 的功能,指定一个路由的默认页。

Index Redirects

上面这种情况比较常见,还有一种非常常见的方式就是当我们尝试访问 / 这个路由时,我们想让其直接跳转到 ‘/Accounts’,直接免去了默认页 Home,这样来的更加直接。由此我们就需要 IndexRedirect 功能。考虑如下路由:

1
2
3
4
5
6
7
<Router>
<Route path="/" component={App}>
<IndexRedirect to="/accounts"/>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>

这样设计路由后,我们再次访问 / 时,系统默认会跳转到 /accounts 路由。

总结

以上就是 IndexRoute 和 IndexRedirect 的功能介绍,让我们来总结一下他们两个的区别。

  • IndexRoute 一般情况下用于设计一个默认页且不改变 URL 地址,而 IndexRedirect 则是跳转默认地址且地址会发生改变。
  • IndexRoute 指定一个组件作为默认页,而 IndexRedirect 指定一个路由地址作为跳转地址。

Hooks

属性的隐式传递

this.props.history/match/location

所属 属性 类型 含义
history length number 表示history堆栈的数量
action string 表示当前的动作。比如pop、replace或push
location object 表示当前的位置
push(path, [state]) function 在history堆栈顶加入一个新的条目
replace(path, [state]) function 替换在history堆栈中的当前条目
go(n) function 将history堆栈中的指针向前移动
goBack() function 等同于go(-1)
goForward() function 等同于go(1)
block(promt) function 阻止跳转
match params object 表示路径参数,通过解析URL中动态的部分获得的键值对
isExact boolean 为true时,表示精确匹配
path string 用来做匹配的路径格式
url string URL匹配的部分
location pathname string URL路径
search string URl中查询字符串
hash string URL的hash分段
state string 表示location中的状态

useHistory

用以获取history对象,进行编程式的导航

1
2
3
4
5
6
7
8
9
10
11
const Husky = props => {
console.log(useHistory()); // 与 props.history 结果一致
console.log(props.history);
return <div>哈士奇</div>;
};
...
<Route path="/dog" component={Dog}></Route> // 必须这么写,props 才能拿到相关值
...
<Route path="/husky">
<Husky />
</Route> // 这样写的话 useHistory 可以正常取值,但是 props 不行

useLocation

用以获取location对象,可以查看当前路由信息

1
2
3
4
5
const Husky = props => {
console.log(useLocation()); // 与 props.location 结果一致
console.log(props.location);
return <div>哈士奇</div>;
};

useParams

useParams和props.match.params可以获取路由参数

1
2
3
4
5
6
7
8
9
10
11
12
<Route path="/blog/:eat">
<Husky />
</Route>

const Husky = props => {
console.log(useParams()) // 与 props.match.params 结果一致,但明显更简洁
console.log(props.match.params)
const {eat} = props.match.params;
return (
<div>哈士奇 吃 {eat}</div>
);
}

useRouteMatch

useRouteMatch,接受一个path字符串作为参数。当参数的path与当前的路径相匹配时,useRouteMatch会返回match对象,否则返回null。

useRouteMatch在对于一些,不是路由级别的组件。但是组件自身的显隐却和当前路径相关的组件时,非常有用。

比如,你在做一个后台管理系统时,网页的Header只会在登录页显示,登录完成后不需要显示,这种场景下就可以用到useRouteMatch

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
const Home = () => {
return (
<div>Home</div>
)
}
// Header组件只会在匹配`/detail/:id`时出现
const Header = () => {
// 只有当前路径匹配`/detail/:id`时,match不为null
const match = useRouteMatch('/detail/:id')
return (
match && <div>Header</div>
)
}
const Detail = () => {
return (
<div>Detail</div>
)
}
function App() {
return (
<div className="App">
<Router>
<Header/>
<Switch>
<Route exact path="/" component={Home}/>
<Route exact path="/detail/:id" component={Detail}/>
</Switch>
</Router>
</div>
);
}

实战

路由嵌套

可以通过嵌套 route 来实现路由嵌套,注意exact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//根路由
<Switch>
<Route path="router" component={Router}></Route>
</Switch>
//Router.jsx
<span>router</span>
<ul>
<li>
<Link to="/router/second/1">1</Link>
</li>
<li>
<Link to="/router/second/12">2</Link>
</li>
</ul>
//子路由的配置分散到各组件中
<Switch>
<Route exact path="/router/second/:id" component={Second}></Route>
</Switch>

注意:如果在父路由中开启 exact 匹配,就会导致子组件加载不出来

路由懒加载

https://zh-hans.reactjs.org/docs/code-splitting.html

Suspense和lazy

如果我们项目有三个模块,用户管理(UserManage)、资产管理(AssetManage)、考勤管理(AttendanceManage)。当我们进入首页的时候由于没有进入任何一个模块,为了提高响应效率是不需要进行模块资源加载的,同时当我们进入用户管理的时候只需要加载用户管理路由对应的模块资源,进入其他模块亦然。这时候我们就需要对代码进行拆分,React.lazy可以结合Router来对模块进行懒加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

// 懒加载引入组件 在用到路由组件时才发送请求
// 通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包

const Home = lazy(() => import('./routes/Home'));
const UserManage = lazy(() => import('./routes/UserManage'));
const AssetManage = lazy(() => import('./routes/AssetManage'));
const AttendanceManage = lazy(() => import('./routes/AttendanceManage'));

const App = () => (
<Router>
{/* 用Suspense包含所有需要注册的路由 fallback为响应未回来时显示的内容 */}
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/userManage" component={UserManage}/>
<Route path="/assetManage" component={AssetManage}/>
<Route path="/attendanceManage" component={AttendanceManage}/>
</Switch>
</Suspense>
</Router>
)

withRouter

本质: 高阶组件

作用: 可以在非路由组件中注入路由对象

在没有路由指向(就是没有Route对象)的组件默认this.props当中没有路由所需要的参数,使用withRouter可以添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import BackHome from './backhome';
export default class Test extends React.Component {
render () {
console.log(this.props)
return (
<div>
这是测试的内容
//返回首页的按钮不是通过route标签渲染的,所以该子组件的this.props中没有路由参数
<BackHome>返回首页</BackHome>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
//导入withRoute
import {withRouter} from 'react-router-dom';
class BackHome extends React.Component {
goHome = () => {
//必须在使用withRouter的情况下,该组件在this.props中才有路由参数和方法
//否则,会报错
this.props.history.push({
pathname: '/home',
state: {
name: 'dx' //同样,可以通过state向home路由对应的组件传递参数
}
})
}
render () {
return (
<button onClick={this.goHome}>this.props.children</button>
)
}
}
//导出的时候,用withRouter标签将backHome组件以参数形式传出
export default withRouter(BackHome)

路由传参

param动态路由传参

1
2
3
4
5
<Route path='/path/:name' component={Path}/>
<link to={ '/user/' + '2' }>xxx</Link>
this.props.history.push({pathname:"/path/" + name});

//读取参数用:this.props.match.params.name

优点:
1、传参和接收都比较简单
2、刷新页面参数不会丢失
缺点:
1、 当复杂数据对象或数组需要传参时,这样做比较麻烦,需要通过json字符串的方式进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义路由匹配
<Route path="/user/:data" component={Component} />;
let data = {
id: 3,
name: "tom",
age: 25,
};
let path = JSON.Stringify(data);

// 传递路由参数
<Link to={path}>用户</Link>;
this.props.history.push(path);

// 使用路由参数
const { id, name, age } = this.props.match.params.data;

2、多个参数的传递,url 会又长又不美观
3、参数会出现在url上,不够安全

search传参

1
2
3
4
5
<Route path='/web/departManange' component={DepartManange}/>
<link to="web/departManange?tenantId=12121212">xxx</Link>
this.props.history.push({pathname:"/web/departManange?tenantId" + row.tenantId});

//读取参数用: this.props.location.search

优点:
1、传参和接收都比较简单
2、刷新页面参数不会丢失
3、可以传递多个参数
缺点:
1、当复杂数据对象或数组需要传参时,这样做比较麻烦,需要通过json字符串的方式进行处理
2、参数会出现在url上,不够安全

query传参

1
2
3
4
5
<Route path='/query' component={Query}/>
<Link to={{ path : ' /query' , query : { name : 'sunny' }}}>
this.props.history.push({pathname:"/query",query: { name : 'sunny' }});

//读取参数用: this.props.location.query.name

优点:
1、传参和接收都比较简单
2、可以传递多个参数
3、传递对象数组等复杂参数方便
4、不会暴露给用户,比较安全
缺点:
1、如果手动刷新当前路由时,数据参数有可能会丢失

state传参

1
2
3
4
5
6
7
8
<Link to={{
pathname: 'about',
state: {
name: 'dx'
}
}}>关于</Link>

this.props.location.state

优点:
1、传参和接收都比较简单
2、可以传递多个参数
3、传递对象数组等复杂参数方便
4、不会暴露给用户,比较安全
缺点:
1、如果手动刷新当前路由时,数据参数有可能会丢失

react中,最外层包裹了BrowserRouter时,不会丢失,但如果使用的时HashRouter,刷新当前页面时,会丢失state中的数据

状态管理器

Redux

Redux是将整个应用状态存储到一个地方,称为store。里面保存一棵状态树(state tree)。组件可以派发(dispatch)行为(action)给store,action发出命令后将state放入reucer加工函数中,返回新的state。其它组件可以通过订阅store中的状态(state)来刷新自己的视图

img

三大原则

单一数据源

整个应用的state被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个store 中。

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。action就是改变state的指令,有多少操作state的动作就会有多少action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//添加todo任务的 action 是这样的:
const ADD_TODO = 'ADD_TODO'

//action创建函数,返回一个action对象
function addTodo(text) {
return{
type: ADD_TODO,//执行的动作
text: 'Build my first Redux app'
index:5//用户完成任务的动作序列号
}
}

//Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次dispatch 过程。
dispatch(addTodo(text))
//或者创建一个被绑定的 action 创建函数来自动 dispatch:
const boundAddTodo = text => dispatch(addTodo(text))
boundAddTodo(text);
//store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。

使用纯函数来执行修改

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

1
(previousState, action) => newState

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

这是一个redux的经典案例

  • 通过createStore创建store

  • actions 定义指令

  • 调用store.dispatch()发出修改state的命令

  • 定义reducer函数根据action的类型改变state

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
import { createStore } from 'redux';
//这里一个技巧是使用 ES6 参数默认值语法 来精简代码。
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
}
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
console.log(store.getState())
);

// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch(actions.increase()) // {count: 1}
store.dispatch(actions.increase()) // {count: 2}
store.dispatch(actions.increase()) // {count: 3}

store构建

目录结构

屏幕截图

action

存放描述行为的数据结构(本质上是 JavaScript 普通对象),一般来说你会通过 store.dispatch() 将 action 传到 store。

我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。

1
2
3
4
5
6
7
8
9
10
//	./actions/counter.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

export const increment = ()=>{
{type:INCREMENT}
}
export const decrement = ()=>{
{type:DECREMENT}
}

注意:当我们表示用户完成任务的动作序列号时,我们还需要再添加一个 action index 来,所以我们通过下标 index 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的。

1
2
3
4
5
6
7
8
9
10
11
12
//	./reducers/counter.js
import {INCREMENT, DECREMENT} from "../actions/counter"
export default function(state = 0, action){
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
}
1
2
3
4
5
6
7
8
//	./reducers/index.js
import { combineReducers } from 'redux'
import counter from './counter'

export default combineReducers({
counter
})

store

注意:Redux 应用只有一个单一的 store

我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。

Store 就是把它们联系到一起的对象。Store 有以下职责:

https://zhuanlan.zhihu.com/p/258017257

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 { createStore, applyMiddleware, compose } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
import reducers from './reducers'

function configureStore() {
const logger = createLogger({})

const middlewares = [thunk]

if (process.env.NODE_ENV !== 'production') {
middlewares.push(logger)
}

const composeEnhancers =
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose
const enhancer = composeEnhancers(applyMiddleware(...middlewares))
//createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
return createStore(reducers, enhancer)
}

export default configureStore()

redux 异步请求

https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html

img

Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
在实际的开发中,redux中管理的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。就是说在异步的网络请求中通过dispatch action来更新state中的数据。这时候就需要用到Redux中间件**(指这个框架允许我们在某个流程的执行中间插入我们自定义的一段代码)**。

Thunk middleware 并不是 Redux 处理异步 action 的唯一方式:

API

Provider 组件

<Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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了。

它的原理是React组件的context属性,请看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}

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

上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}

render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}

VisibleTodoList.contextTypes = {
store: React.PropTypes.object
}

React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store

connect

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

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

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

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

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

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

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

1
2
3
4
5
6
import { connect } from 'react-redux'

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

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

mapStateToProps()

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

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

1
2
3
4
5
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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对象。

1
2
3
4
5
6
7
8
9
10
// 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>

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

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

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

mapDispatchToProps()

mapDispatchToPropsconnect函数的第二个参数,用来建立各种dispatch变成props,让你可以直接使用 UI 组件的参数到store.dispatch方法的映射。也就是说,把各种dispatch变成了props让你可以直接使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}

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

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

1
2
3
4
5
6
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
实例:计数器

我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。

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
import React from "react";
import { connect } from "react-redux";
import { increment, decrement } from "../../store/actions/counter";

const Home = function (props) {
//生成props
const { count, onincrement, ondecrement} = props;
// console.log(props);
return (
<div>
<Button
variant="contained"
color="primary"
onClick={onincrement}
>
increment
</Button>
<Button
variant="contained"
color="primary"
onClick={ondecrement}
style={{marginLeft:'30px'}}
>
decrement
</Button>
<p style={{fontSize:'30px'}}>{count}</p>
</div>
);
};

上面代码中,这个 UI 组件有三个参数:count和 onincrement, ondecrement。前者需要从state计算得到,后者需要向外发出 Action。

接着,定义countstate的映射,以及onincrement, ondecrementdispatch的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
function mapStateToProps(state) {
console.log(state)
return {
count: state.counter.count,
};
}
function mapDispatchToProps(dispatch) {
return {
onincrement: () => dispatch(increment()),
ondecrement: () => dispatch(decrement())
};
}

然后,使用connect方法生成容器组件。

1
export default connect(mapStateToProps, mapDispatchToProps)(Home);

然后,定义这个组件的 Reducer。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Reducer
import {INCREMENT, DECREMENT} from "../actions/counter"
export default function(state = { count: 0}, action){
const count = state.count
switch (action.type) {
case INCREMENT:
return {count:count + 1};
case DECREMENT:
return {count:count - 1};
default:
return {count:count};
}
}

最后,生成store对象,并使用Provider在根组件外面包一层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import route from "../route/index.js";
import { Provider } from "react-redux";
import store from "../store";
export default function Menu() {
const classes = useStyles();
return (
<div className={classes.root}>
<Provider store={store}>
</Provider>
</div>
);
}

createStore

createStore(reducer, [preloadedState], enhancer)

创建一个 Redux store 来以存放应用中所有的 state。
应用中应有且仅有一个 store。

参数

  1. reducer (Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树
  2. [preloadedState] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。
  3. enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。

返回值

(Store): 保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createStore } from 'redux'

function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text])
default:
return state
}
}

let store = createStore(todos, ['Use Redux'])

store.dispatch({
type: 'ADD_TODO',
text: 'Read the docs'
})

console.log(store.getState())
// [ 'Use Redux', 'Read the docs' ]
  • 应用中不要创建多个 store!相反,使用 combineReducers 来把多个 reducer 创建成一个根 reducer。
  • 要使用多个 store 增强器的时候,你可能需要使用 compose

Store 方法

Store 就是用来维持应用所有的 state 树 的一个对象。 改变 store 内 state 的惟一途径是对它 dispatch 一个 action。

  • getState()
  • dispatch(action)
  • subscribe(listener)
  • replaceReducer(nextReducer)

combineReducers

随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。

1
2
3
4
5
6
import { combineReducers } from 'redux'
import counter from './counter'

export default combineReducers({
counter
})

combineReducers把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。

合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。

applyMiddleware

https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html

applyMiddleware(…middlewares)

使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

Middleware 最常见的使用场景是实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch 异步 actions

示例: 自定义 Logger Middleware

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 { createStore, applyMiddleware } from 'redux'
import todos from './reducers'

function logger({ getState }) {
return (next) => (action) => {
console.log('will dispatch', action)

// 调用 middleware 链中下一个 middleware 的 dispatch。
let returnValue = next(action)

console.log('state after dispatch', getState())

// 一般会是 action 本身,除非
// 后面的 middleware 修改了它。
return returnValue
}
}

let store = createStore(
todos,
[ 'Use Redux' ],
applyMiddleware(logger)
)

store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
// (将打印如下信息:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]

compose(...functions)

从右到左来组合多个函数。

这是函数式编程中的方法,为了方便,被放到了 Redux 里。
当需要把多个 store 增强器 依次执行的时候,需要用到它。

参数

  1. (arguments): 需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名。(译者注:compose(funcA, funcB, funcC) 形象为 compose(funcA(funcB(funcC())))

返回值

(Function): 从右到左把接收到的函数合成后的最终函数。

1
2
3
4
5
6
7
8
9
10
11
12
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers/index'

const store = createStore(
reducer,
compose(
applyMiddleware(thunk),
DevTools.instrument()
)
)

Mobx

作为了解的内容,在项⽬中使⽤redux的情况更多。

Mobx是⼀个功能强⼤,上⼿⾮常容易的状态管理⼯具。redux的作者也曾经向⼤家推荐过它,在不少情况下可以使⽤Mobx来替代掉redux。

hox

定义 Model: 用 createModel 包装后,就变成了持久化,且全局共享的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createModel } from 'hox';

/* 任意一个 custom Hook */
function useCounter() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return {
count,
decrement,
increment
};
}

export default createModel(useCounter)

使用 ModelcreateModel 返回值是个 Hook,你可以按 React Hooks 的用法正常使用它。

1
2
3
4
5
6
7
8
9
10
11
import { useCounterModel } from "../models/useCounterModel";

function App(props) {
const counter = useCounterModel();
return (
<div>
<p>{counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}

TS与React

jsx转ts

https://blog.yangteng.me/articles/2021/migrate-react-project-to-typescript/

配置TS

1
yarn add typescript

然后加入 TypeScript 的配置文件:将 tsconfig.json 放到项目的根目录下。

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
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"experimentalDecorators": true,
//有的项目webpack会设置一些module的别名alias,ts也得配一下防止找不到
"baseUrl": "./",
"paths": {
"@@/*": ["./*"],
"@/*": ["src/*"],
"@api/*": ["src/api/*"],
"@assets/*": ["src/assets/*"],
"@common/*": ["src/common/*"],
"@enum/*": ["src/enum/*"],
"@context/*": ["src/context/*"],
"@components/*": ["src/components/*"],
"@models/*": ["src/models/*"],
"@hooks/*": ["src/hooks/*"],
"@pages/*": ["src/pages/*"],
"@store/*": ["src/store/*"],
"@utils/*": ["src/utils/*"]
},
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["src"]
}

package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"devDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.8",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@types/react-redux": "^7.1.23",
"babel-loader": "^8.2.2",
"typescript": "^4.6.3",
}
"dependencies": {
"@types/node": "^17.0.23",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.7",
"less-loader": "^10.2.0",
}

配置 babel 和 webpack

将 babel 的 TypeScript 预设加入项目依赖中,并添加到 babel 的配置文件里。

1
yarn add @babel/preset-typescript --dev
1
2
3
4
5
6
7
8
9
10
11
// .babelrc

{
"presets": [
// other presets
// ...
"@babel/typescript"
]
// other settings
// ...
}

修改 webpack 的配置,将 TypeScript 文件加入 resolvebabel-loader 的 match 规则中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js

export default {
// other settings
// ...
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// ...
},
},
},
],
},
}

引入类型定义的 package

上一步完成后,实际上已经可以在代码中使用 TypeScript 了。但这时候如果你去写一个 React 组件,就会发现类似 Cannot find module 'react'. 的报错。这就需要将一些你用到的 library 的类型定义加进来了。

1
yarn add @types/react @types/react-dom @types/node #@types/<package-used-in-your-project>

TIP

  • antd,css

    1
    2
    3
    4
    5
    6
    7
    8
    {
    loader: 'less-loader', // 编译 Less 为 CSS
    options: {
    lessOptions: {
    javascriptEnabled: true,
    },
    },
    },

React.FC

React.FC是函数式组件,是在TypeScript使用的一个泛型。FC是FunctionComponent的缩写,React.FC可以写成React.FunctionComponent。这个类型定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { FC } from 'react';

/**
* 声明Props类型
*/
export interface MyComponentProps {
className?: string;
style?: React.CSSProperties;
}

export const MyComponent: FC<MyComponentProps> = props => {
return <div>hello react</div>;
};

NEXT

https://www.nextjs.cn/

背景

要从头开始使用 React 构建一个完整的 Web 应用程序,需要考虑许多重要的细节:

  • 必须使用打包程序(例如 webpack)打包代码,并使用 Babel 等编译器进行代码转换。
  • 你需要针对生产环境进行优化,例如代码拆分。
  • 你可能需要对一些页面进行预先渲染以提高页面性能和 SEO。你可能还希望使用服务器端渲染或客户端渲染。
  • 你可能必须编写一些服务器端代码才能将 React 应用程序连接到数据存储。

Next.js:React 开发框架

创建

1
npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"
1
cd nextjs-blog
1
npm run dev

在浏览器中打开 http://localhost:3000

页面

客户端导航

在 Next.js 中,页面是从pages目录中的文件导出的 React 组件。

页面与基于其文件名的路由相关联。例如,在开发中:

  • pages/index.js/路由相关联。
  • pages/posts/first-post.js/posts/first-post路由相关联。

在页面之间导航

1
import Link from 'next/link'
1
Read <Link href="/posts/first-post"><a>this page!</a></Link>

Link组件支持在同一个 Next.js 应用程序中的两个页面之间进行客户端导航

客户端导航意味着页面转换使用 JavaScript 进行,这比浏览器执行的默认导航更快。

Link组件支持在同一个 Next.js 应用程序中的两个页面之间进行客户端导航

客户端导航意味着页面转换使用 JavaScript 进行,这比浏览器执行的默认导航更快。

这是您可以验证的简单方法:

  • 使用浏览器的开发人员工具将backgroundCSS 属性更改<html>yellow
  • 单击链接可在两个页面之间来回切换。
  • 您会看到黄色背景在页面转换之间持续存在。

这表明浏览器加载完整页面并且客户端导航正在工作。

Links

如果您使用了<a href="…">代替<Link href="…">并执行了此操作,则链接点击时背景颜色将被清除,因为浏览器会完全刷新。

动态路由

Next.js 支持具有动态路由的 pages(页面)。例如,如果你创建了一个命名为 pages/posts/[id].js 的文件,那么就可以通过 posts/1posts/2 等类似的路径进行访问。

  • pages/blog/[slug].js/blog/:slug (/blog/hello-world)
  • pages/[username]/settings.js/:username/settings (/foo/settings)
  • pages/post/[...all].js/post/* (/post/2020/id/title)

代码拆分和预取

Next.js 会自动进行代码拆分,因此每个页面只加载该页面所需的内容。这意味着在呈现主页时,最初不会提供其他页面的代码。

这可确保即使您添加数百个页面,主页也能快速加载。

仅加载您请求的页面的代码也意味着页面变得孤立。如果某个页面抛出错误,应用程序的其余部分仍然可以工作。

此外,在 Next.js 的生产版本中,每当Link组件出现在浏览器的视口中时,Next.js 都会在后台自动预取链接页面的代码。当您单击链接时,目标页面的代码已在后台加载,页面转换将近乎即时!

HTML

html

<Head>使用 代替小写字母<head><Head>是一个内置于 Next.js 的 React 组件。它允许您修改<head>页面的名称。

1
import Head from 'next/head'
1
2
3
4
5
6
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
<meta name="Description" content={props.description}></meta>
<title>{props.title}</title>
</Head>

img

1
2
img统一放在public中,引用直接引用img,不需要添加图像路径
src="/head.jpg"

css

1
2
3
<style jsx>{`

`}</style>

这是使用一个名为styled-jsx的库。它是一个“CSS-in-JS”库——它允许你在 React 组件中编写 CSS,并且 CSS 样式将被限定(其他组件不会受到影响)。

Next.js 内置了对styled-jsx 的支持,但您也可以使用其他流行的 CSS-in-JS 库。我用的是materialUI框架中的css-in-js

  • 全局样式

    如果你希望每个页面都加载一些 CSS,添加pages/_app.js文件

    1
    2
    3
    4
    import '../styles/global.css'
    export default function App({ Component, pageProps }) {
    return <Component {...pageProps} />
    }

    创建一个顶级styles目录并global.css在里面创建。将其导入pages/_app.js

内置API

某些页面需要获取外部数据以进行预渲染。有两种情况,在每种情况下,您都可以使用 Next.js 提供的特殊功能:

  1. 您的页面 内容 取决于外部数据:使用 getStaticProps
  2. 你的页面 paths(路径) 取决于外部数据:使用 getStaticPaths (通常还要同时使用 getStaticProps)。

getStaticProps函数在构建时被调用,并允许你在预渲染时将获取的数据作为 props 参数传递给页面。getStaticProps不会在页面组件中生效

Next.js 允许你创建具有 动态路由 的页面。例如,你可以创建一个名为 pages/posts/[id].js 的文件用以展示以 id 标识的单篇博客文章。当你访问 posts/1 路径时将展示 id: 1 的博客文章。但是,在构建 id 所对应的内容时可能需要从外部获取数据。getStaticPaths函数在构建时被调用,并允许你指定要预渲染的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 此函数在构建时被调用
export async function getStaticPaths() {
// 调用外部 API 获取博文列表
const res = await fetch('https://.../posts')
const posts = await res.json()

// 据博文列表生成所有需要预渲染的路径
const paths = posts.map((post) => ({
params: { id: post.id },
}))

// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false }
}

为了让页面使用服务端渲染,你需要导出 getServerSideProps 异步函数。这个函数将在每次请求时在服务端被调用。例如,假设你的页面需要用最新的数据预渲染(通过外部的 api 获取数据)。你应该写下 getServerSideProps 来获取数据传递给 Page。

getServerSideProps 和 getStaticProps 很像,但是区别的是,getServerSideProps 是每个请求都会调用而不是在构建时。

mardown解析

插件

https://dev.to/imranib/build-a-next-js-markdown-blog-5777

  • react-markdown将帮助我们解析和渲染 Markdown 文件

  • 代码格式化:react-syntax-highlighter

  • gray-matter](https://www.npmjs.com/package/react-markdown) 将解析我们博客的顶部内容。(文件顶部的部分---

    我们需要这样的元数据titledatadescriptionslug。您可以在此处添加任何您喜欢的内容

参数 意义
slug 导航的参数
title 文章名称
data 最新时间
updated 文章更新日期
tags 文章標籤
category 文章分類
description 文章描述
  • raw-loader将帮助我们导入我们的markdown文件。

流程

https://dev.to/imranib/build-a-next-js-markdown-blog-5777

https://thetombomb.com/posts/adding-code-snippets-to-static-markdown-in-Next%20js

Tips

  • material,classname报错,每次刷新,material失去效果。添加_app.js和__document.js文件