React

3/20/2023

# 【写在开头】

本笔记来自学习dselegent-blog 互联网小白 | dselegent-blog (opens new window),仅供本人学习使用。

# 【01React入门】

# 1.React简介

React 是一个用于构建用户界面的 JavaScript 库。

# 2.快速使用

首先需要引入几个 react 包(React 核心库、操作 DOM 的 react 扩展库、将 jsx 转为 js 的 babel 库)

<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
1
2
3
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>hello_react</title>
  </head>
  <body>
    <!-- 准备好一个“容器” -->
    <div id="test"></div>

    <!-- 引入react核心库 -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <!-- 引入react-dom,用于支持react操作DOM -->
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <!-- 引入babel,用于将jsx转为js -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script type="text/babel">
      /* 此处一定要写babel */
      //1.创建虚拟DOM
      const VDOM = <h1>Hello</h1> /* 此处不要写引号,因为不是字符串 */
      //2.渲染虚拟DOM到页面
	const root = ReactDOM.createRoot(document.querySelector('#test'));
      root.render(VDOM);
    </script>
  </body>
</html>

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

# 3.JSX语法

JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。

# 3.1.注意点

1.只能有一个根标签

...
render () {
  return (
    <div>
      <div>第一个</div>
      <div>第二个</div>
    </div>
  )
}
...
1
2
3
4
5
6
7
8
9
10

2.自定义的组件都必须要用大写字母开头,普通的 HTML 标签都用小写字母开头

3.内敛样式要使用{{}}包裹

style={{color:'skyblue',fontSize:'37px'}}
1

4.class需要使用className代替

# 3.2.小练习

<body>

<div id="test"></div>

<script type="text/babel">
    const arr = ["Vue","React","Angular"]
    const VDOM = (
        <div>
            <ul>
                {arr.map((item,index)=>
                    <li key={index}>{item}</li>
                )}
            </ul>
        </div>
    )

    const root = ReactDOM.createRoot(document.querySelector('#test'));
    root.render(VDOM);
</script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 【02面向组件编程】

# 1.组件

# 1.1.只有两种方式的组件

  • 函数组件
  • 类式组件

函数式组件

 //1.创建函数式组件
      function MyComponent(props) {
        console.log(this) //此处的this是undefined,因为babel编译后开启了严格模式
        return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
      }

 //2.渲染组件到页面
      ReactDOM.render(<MyComponent />, document.getElementById('test'))

1
2
3
4
5
6
7
8
9

类式组件

class MyComponent extends React.Component {
        render() {
          console.log('render中的this:', this)
          return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
        }
      }

ReactDOM.render(<MyComponent />, document.getElementById('test'))

1
2
3
4
5
6
7
8
9

# 1.2.组合使用

function Hello(props) {
  return <h1>Hello, {props.name}</h1>;
}

function Hi(props) {
  return <h1>Hi, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Hello name="Ronin" />      
      <Hi name="Yuhao" />
     </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2.组件实例的三大属性之一state

# 2.1state

一个组件的显示形态是可以由它数据状态和配置参数决定的。一个组件可以拥有自己的状态,就像一个点赞按钮,可以有“已点赞”和“未点赞”状态,并且可以在这两种状态之间进行切换。React.js 的 state 就是用来存储这种可变化的状态的。

# 2.1setState

*setState 方法由父类 Component 所提供。当我们调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上。

注意:

1.当我们要改变组件的状态的时候,不能直接用 this.state = xxx 这种方式来修改,如果这样做 React.js 就没办法知道你修改了组件的状态,它也就没有办法更新页面。所以,一定要使用 React.js 提供的 setState 方法,它接受一个对象或者函数作为参数。

2.当你调用 setState 的时候,React.js 并不会马上修改 state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到 state 当中,然后再触发组件更新。所以如果你想在 setState 之后使用新的 state 来做后续运算就做不到了。

...
  handleClickOnLikeButton () {
    this.setState({ count: 0 }) // => this.state.count 还是 undefined
    this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN
    this.setState({ count: this.state.count + 2}) // => NaN + 2 = NaN
  }
...
1
2
3
4
5
6
7

使用接受一个函数作为参数的方式可以解决这个问题。React.js 会把上一个 setState 的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新 state 的对象

...
  handleClickOnLikeButton () {
    this.setState((prevState) => {
      return { count: 0 }
    })
    this.setState((prevState) => {
      return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
    })
    this.setState((prevState) => {
      return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
    })
    // 最后的结果是 this.state.count 为 3
  }
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.组件实例的三大属性之一props

state不同,state是组件自身的状态,而props则是外部传入的数据

# 3.1简单使用

1.单个传值

<body>
    <div id = "div"></div>
</body>

<script type="text/babel">
    class Person extends React.Component{
        render(){
          const { name, age, sex } = this.props
            return (
                <ul>
                  <li>姓名:{name}</li>
                  <li>性别:{sex}</li>
                  <li>年龄:{age + 1}</li>
                </ul>
          )
        }
    }
    //传递数据
    ReactDOM.render(<Person name="Ronin" age = {23} sex="男"/>,document.getElementById("div"));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

2.传递的数据可以是一个对象

<script type="text/babel">
    class Person extends React.Component{
        render(){
            return (
                <ul>
                    <li>{this.props.name}</li>
                    <li>{this.props.age}</li>
                    <li>{this.props.sex}</li>
                </ul>
            )
        }
    }
    const p = {name:"Daisy",age:"22",sex:"女"}
   ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.2类型检查

要在组件的 props 上进行类型检查,需要配置特定的 propTypes 属性

写在组件外面:

	//对标签属性进行类型、必要性的限制
    Person.propTypes = {
        name:PropTypes.string.isRequired, //限制name必传,且为字符串
        sex:PropTypes.string, //限制sex为字符串
        age:PropTypes.number, //限制age为数值
        //speak:PropTypes.func //限制speak为函数
    }
    //指定默认标签属性值
    Person.defaultProps = {
        sex:'男',//sex默认值为男
        age:23 //age默认值为23
    }
1
2
3
4
5
6
7
8
9
10
11
12

写在组件里面:

		//对标签属性进行类型、必要性的限制
        static propTypes = {
            name:PropTypes.string.isRequired, //限制name必传,且为字符串
            sex:PropTypes.string, //限制sex为字符串
            age:PropTypes.number, //限制age为数值
        }

        //指定默认标签属性值
        static defaultProps = {
            sex:'男',//sex默认值为男
            age:23 //age默认值为23
        }
1
2
3
4
5
6
7
8
9
10
11
12

# 3.3函数式组件使用props

函数在使用props的时候,是作为参数进行使用的(props)

<script type="text/babel">
      //创建组件
      function Person(props) {
        const { name, age, sex } = props
        return (
          <ul>
            <li>姓名:{name}</li>
            <li>性别:{sex}</li>
            <li>年龄:{age}</li>
          </ul>
        )
      }
      
      Person.propTypes = {
        name: PropTypes.string.isRequired, //限制name必传,且为字符串
        sex: PropTypes.string, //限制sex为字符串
        age: PropTypes.number, //限制age为数值
      }

      //指定默认标签属性值
      Person.defaultProps = {
        sex: '男', //sex默认值为男
        age: 23, //age默认值为23
      }
      //渲染组件到页面
      ReactDOM.render(<Person name="Daisy" sex="女" age=22/>, document.getElementById('test'))
    </script>
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

# 4.组件实例的三大属性之一refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

# 4.1三种操作refs的方法

  • 字符串形式
  • 回调形式
  • createRef形式

# 4.2推荐使用createRef形式:

1.创建 Refs

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
1
2
3
4
5
6
7
8
9

2.访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

const node = this.myRef.current;
1

Tips:

1.不能在函数组件使用 ref 属性,因为他们没有实例。但是在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件

2.不要过度的使用 ref

3.React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMountcomponentDidUpdate 生命周期钩子触发前更新

# 【03事件处理】

# 1.React事件

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase)
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 不能通过返回 false 的方式阻止默认行为,必须显式地使用 preventDefault

传统的 HTML

<button onclick="activateLasers()">
  Activate Lasers
</button>
1
2
3
<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>
1
2
3

React

<button onClick={activateLasers}>  
   Activate Lasers
</button>
1
2
3
function Form() {
  function handleSubmit(e) {
    e.preventDefault();  //显式地使用preventDefault
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.绑定事件

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

使用bind

...
handleUsername(e){
        this.setState({
            username:e.target.value
        })
    }
...
<input value={this.state.username} onChange={this.handleUsername.bind(this)} />
...
1
2
3
4
5
6
7
8
9

箭头函数

...
handleUsername = (e) => {
        this.setState({
            username:e.target.value
        })
    }
...
<input value={this.state.username} onChange={this.handleUsername} />
...
1
2
3
4
5
6
7
8
9
...
handleUsername(e){
        this.setState({
            username:e.target.value
        })
    }
...
<input value={this.state.username} onChange={(e) => this.handleUsername(e)} />
...
1
2
3
4
5
6
7
8
9

# 3.传递参数

常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
1
2

1.上述两种方式是等价的,分别通过箭头函数和Function.prototype.bind来实现

2.如果通过箭头函数的方式,事件对象必须显式的进行传递;而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递

# 4.受控和非受控组件

# 4.1受控组件

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

saveName = (event) =>{
    this.setState({name:event.target.value});
}

savePwd = (event) => {
    this.setState({pwd:event.target.value});
}

render() {
    return (
        <form action="http://www.baidu.com" onSubmit={this.login}>
            用户名:<input value={this.state.name} onChange={this.saveName} type = "text" />
            密码<input value={this.state.pwd} onChange={this.savePwd} type = "password"/>
            <button>登录</button>
        </form>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

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

# 4.2非受控组件

非受控组件其实就是表单元素的值不会更新state。输入数据都是现用现取的。

class Login extends React.Component{

    	login = (e) =>{
        	e.preventDefault(); //阻止表单默认事件
            console.log(this.name.value);
            console.log(this.pwd.value);
        }
        render() {
            return (
                <form action="http://www.baidu.com" onSubmit={this.login}>
                用户名:<input ref = {e => this.name = e } type = "text" name ="username"/>
                密码:  <input ref = {e => this.pwd = e } type = "password" name ="password"/>
                <button>登录</button>
                </form>
            )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

没有使用state来控制属性,使用的是事件来控制表单的属性值

# 5.函数的柯里化

# 5.1高阶函数

高阶函数是一个接收函数作为参数将函数作为输出返回的函数

# 5.2柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名的。

简单来说,柯里化是一项技术,它用来改造多参数的函数,比如:

// 这是一个接受3个参数的函数
const add = function(x, y, z) {
  return x + y + z
}
1
2
3
4

变换一下,可以得到这样一个函数:

// 接收一个单一参数
const curryingAdd = function(x) {
  // 并且返回接受余下的参数的函数
  return function(y, z) {
    return x + y + z
  }
}
1
2
3
4
5
6
7

从调用上来对比:

// 调用add
add(1, 2, 3)

// 调用curryingAdd
curryingAdd(1)(2, 3)
// 看得更清楚一点,等价于下面
const fn = curryingAdd(1)
fn(2, 3)
1
2
3
4
5
6
7
8

可以看到,变换后的的函数可以分批次接受参数,先记住这一点,下面会讲用处。甚至fncurryingAdd返回的函数)还可以继续变换:

const curryingAdd = function(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}
// 调用
curryingAdd(1)(2)(3)
// 即
const fn = curryingAdd(1)
const fn1 = fn(2)
fn1(3)
1
2
3
4
5
6
7
8
9
10
11
12
13

上面的两次变换过程,就是函数柯里化

简单讲就是把一个多参数的函数f,变换成接受部分参数的函数g,并且这个函数g会返回一个函数h,函数h用来接受其他参数。函数h可以继续柯里化。就是一个套娃的过程。

详情 函数柯里化 (opens new window)

下面回到React:

使用柯里化

 class Login extends React.Component{
    state = {name:"",pwd:""};

    //返回一个函数
    saveType = (type) =>{
        return (event) => {
            this.setState({[type]:event.target.value});
        }
    }

    //因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
    render() {
        return (
            <form>
                <input onChange = {this.saveType('name')} type = "text"/>
                <button>登录</button>
            </form>
        )
    }
}

ReactDOM.render(<Login />,document.getElementById("div"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

不使用函数柯里化

 class Login extends React.Component{
    state = {name:"",pwd:""};

    //返回一个函数
    saveType = (type,event) =>{
        this.setState({[type]:event.target.value});
    }

    //因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
    render() {
        return (
            <form>
                <input onChange = {event => this.saveType('name',event)} type = "text"/>
                <button>登录</button>
            </form>
        )
    }
}

ReactDOM.render(<Login />,document.getElementById("div"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 【04生命周期】

# 1.react生命周期(新)全览

React组件中包含一系列钩子函数{生命周期回调函数},会在特定的时刻调用

1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
                1.	constructor()
                2.	getDerivedStateFromProps 
                3.	render()
                4.	componentDidMount() =====> 常用
                	一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
                1.	getDerivedStateFromProps
                2.	shouldComponentUpdate()
                3.	render()
                4.	getSnapshotBeforeUpdate
                5.	componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
                1.	componentWillUnmount()  =====> 常用
                	一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

image-20221023222949399

# 2.初始化阶段

# 2.1该阶段生命周期

  1. constructor(props)
  2. static getDerivedStateFromProps(props,state)--替代了componentWillReceiveProps
  3. render()
  4. componentDidMount()

# 2.2constructor

数据的初始化

接收props和context,当想在函数内使用这两个参数需要在super传入参数,当使用constructor时必须使用super,否则可能会有this的指向问题,如果不初始化state或者不进行方法绑定,则可以不为组件实现构造函数

注意:避免将 props 的值复制给 state,如此做毫无必要(可以直接使用 this.props.color)

# 2.3static getDerivedStateFromProps(新)

很少使用

从props获取state

  1. 首先,该函数会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用;
  2. 该函数必须是静态的;
  3. 给组件传递的数据(props)以及组件状态(state),会作为参数到这个函数中;
  4. 该函数也必须有返回值,返回一个Null或者state对象。因为初始化和后续更新都会执行这个方法,因此在这个方法返回state对象,就相当于将原来的state进行了覆盖,所以倒是修改状态不起作用。

注意:state 的值在任何时候都取决于传入的 props ,不会再改变

# 2.4render

class组件中唯一必须实现的方法

render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。

注意:避免在 render 中使用 setState ,否则会死循环

当render被调用时,他会检查this.props.和this.state的变化并返回以下类型之一:

  1. 通过jsx创建的react元素
  2. 数组或者fragments:使得render可以返回多个元素
  3. Portals:可以渲染子节点到不同的dom树上
  4. 字符串或数值类型:他们在dom中会被渲染为文本节点
  5. 布尔类型或者null:什么都不渲染

# 2.5componentDidMount

在组件挂在后(插入到dom树中)后立即调用

componentDidMount 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作

这个挂载完成指的是:组件插入 DOM tree

可以在这里调用Ajax请求,返回的数据可以通过setState使组件重新渲染,或者添加订阅,但是要在conponentWillUnmount中取消订阅

# 3.更新阶段

# 3.1该阶段生命周期

  1. static getDerivedStateFromProps(nextProps, prevState)
  2. shouldComponentUpdate(nextProps,nextState)
  3. render()
  4. getSnapshotBeforeUpdate(prevProps,prevState)
  5. componentDidUpdate(prevProps,precState,snapshot)

# 3.2shouldComponentUpdate

唯一用于控制组件重新渲染的钩子

在渲染之前被调用,默认返回为true;

返回值是判断组件的输出是否受当前state或props更改的影响,默认每次state发生变化都重新渲染,首次渲染或使用forceUpdate(使用this.forceUpdate())时不被调用

主要用于性能优化,会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。不建议深层比较,会影响性能。如果返回false,则不会调用componentWillUpdate、render和componentDidUpdate

使用场景:

react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断

# 3.3getSnapshotBeforeUpdate(新)

在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用,替换componetnWillUpdate

它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null

和componentWillUpdate的区别

  • 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
  • getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。

# 3.4componentDidUpdate

组件在更新完毕后会立即被调用,首次渲染不会调用

可以在该方法调用setState,但是要包含在条件语句中,否则一直更新会造成死循环

当组件更新后,可以在此处对 DOM 进行操作。如果对更新前后的props进行了比较,可以进行网络请求。(当 props 未发生变化时,则不会执行网络请求)

componentDidUpdate(prevProps,prevState,snapshotValue) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}
1
2
3
4
5
6

注意:如果组件实现了 getSnapshotBeforeUpdate() 生命周期,则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshotValue” 参数传递。否则此参数将为 undefined。如果返回false就不会调用这个函数

# 4.卸载组件

# 4.1componentWillUnmount

在组件卸载和销毁之前调用

注意:在这之前必须执行一些清理操作,例如,清除timer(setTimeout,setInterval),取消网络请求,或者取消在componentDidMount的订阅,移除所有监听

使用一个flag可以有效防止销毁组件时,请求还未完成

componentDidMount() {
    this.isMount === true
    axios.post().then((res) => {
    this.isMount && this.setState({   // 增加条件ismount为true时
      rrr:res
    })
})
}
componentWillUnmount() {
    this.isMount === false
}
1
2
3
4
5
6
7
8
9
10
11

# 【05条件渲染】

# 1.条件判断语句

if--else if--else

# 2.三目运算符

condition ? a : b

# 3.与运算符&&

适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染

render() {
  const { type } = this.state
  return (
    <div>
      {type === 1 && <h2>第三种写法:type值等于1</h2>}
      {type !== 1 && <h2 className="other">第三种写法:type值不等于1</h2>}
    </div>
  )
}
1
2
3
4
5
6
7
8
9

在 JavaScript 中,true && expression 总是会返回 expression,而false && expression 总是会返回 false。因此,如果条件是 true&& 右侧的元素就会被渲染;如果是 false,React 会忽略并跳过它

注意:falsy 表达式 会使 && 后面的元素被跳过,但会返回 falsy 表达式的值。

在下面示例中,render 方法的返回值是 <div>0</div>

render() {
  const count = 0;
  return (
    <div>
      {count && <h1>Messages: {count}</h1>}
    </div>
  );
}
1
2
3
4
5
6
7
8

# 4.元素变量

render() {
  const { type } = this.state
  //2. 第二种方法 声明变量 给变量赋值
  let test = null
  if (type === 1) {
    test = <h2>第四种写法:type值等于1</h2>
  } else {
    test = <h2 className="other">第四种写法:type值不等于1</h2>
  }
  return <div>{test}</div>
}
1
2
3
4
5
6
7
8
9
10
11

# 5.阻止组件渲染

若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。

function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }

  return (
    <div className="warning">
      Warning!
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11

# 【06列表 & Key】

# 1.列表

可以通过使用 {} 在 JSX 内构建一个元素集合

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
  <li>{numbers}</li>
);

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<ul>{listItems}</ul>);
1
2
3
4
5
6
7

在一个组件中渲染列表

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

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);
1
2
3
4
5
6
7
8
9
10
11
12
13

上段代码会有a key should be provided for list items,所以创建一个元素时,必须包括一个特殊的 key 属性

<li>{number}</li> 
<li key={number.toString()}>{number}</li> //加上key
1
2

# 2.key

# 2.1意义&基本使用

key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此应当给数组中的每一个元素赋予一个确定的标识。

1.一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key。

2.当元素没有确定 id 的时候,万不得已可以使用元素索引 index 作为 key。

# 2.2 用 key 提取组件

如果提取出一个 ListItem 组件,应该把 key 保留在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上

不正确的使用 key 的方式

function ListItem(props) {
  const value = props.value;
  return (
    // 错误!你不需要在这里指定 key:
    <li key={value.toString()}>
      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 错误!元素的 key 应该在这里指定:
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

正确的使用 key 的方式

function ListItem(props) {
  // 正确!这里不需要指定 key:
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 正确!key 应该在数组的上下文中被指定
    <ListItem key={number.toString()} value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2.3 key 值在兄弟节点之间必须唯一

数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当生成两个不同的数组时,可以使用相同的 key 值

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>  //这里的兄弟节点为<li>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <div key={post.id}>   //这里的兄弟节点为<div>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);
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

# 2.4 在 JSX 中嵌入 map()

在上面的例子中,我们声明了一个单独的 listItems 变量并将其包含在 JSX 中

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

嵌入 map()后

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

# 3.diff算法

# 3.1虚拟 DOM

在谈 diff 算法之前,我们需要先了解虚拟 DOM。它是一种编程概念,在这个概念里,以一种虚拟的表现形式被保存在内存中。在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是 JavaScript 对象。

虚拟 DOM 只保留了真实 DOM 节点的一些基本属性,和节点之间的层次关系,它相当于建立在 JavaScript 和 DOM 之间的一层“缓存”

每个 DOM 元素的结构都可以用 JavaScript 的对象来表示。你会发现一个 DOM 元素包含的信息其实只有三个:标签名,属性,子元素。所以HTML的信息可以用虚拟 DOM 结构来表示:

<div class='box' id='content'>
  <div class='title'>Hello</div>
  <button>Click</button>
</div>
1
2
3
4
{
  tag: 'div',
  props: { className: 'box', id: 'content'},
  children: [
    {
      tag: 'div',
      props: { className: 'title' },
      children: ['Hello']
    },
    {
      tag: 'button',
      props: null,
      children: ['Click']
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3.2Diff算法

每个组件中的每个标签都会有一个key,不过有的必须显示的指定,有的可以隐藏。

  • 如果标签是静态的(生成后不会改变里面的内容),那么不需要指定key(不指定key时,React也会生成一个默认的标识),或者将index作为key,只要key不重复即可

  • 如果标签是动态的,是有可能刷新的,就必须显示的指定key

使用map进行遍历的时候就必须指定Key

不推荐使用Index作为Key去使用

最好使用每一条数据的唯一标识作为key 比如id,手机号,身份证号

Diff算法其实就是react生成的新虚拟DOM和以前的旧虚拟DOM的比较规则:

  • 如果旧的虚拟DOM中找到了与新虚拟DOM相同的key
    • 如果内容没有变化,就直接只用之前旧的真实DOM
    • 如果内容发生了变化,就生成新的真实DOM
  • 如果旧的虚拟DOM中没有找到与新虚拟DOM相同的key
    • 根据数据创建新的真实的DOM,随后渲染到页面上

# 【07收集表单数据】

# 1.状态属性

表单元素有这么几种属于状态的属性:

  • value,对应 <input><textarea> 所有
  • checked,对应类型为 checkboxradio<input> 所有
  • selected,对应 <option> 所有

注意:在 HTML 中 <textarea> 的值可以由子节点(文本)赋值,但是在 React 中,要用 value 来设置

# 2.表单元素的两种表现形式

# 2.1受控组件

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

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

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

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

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

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

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

# 2.2非受控组件

使用非受控组件,这时表单数据将交由 DOM 节点来处理。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。

# 2.2.1默认值

在非受控组件中,希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 可以指定一个 defaultValue 属性,而不是 value

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={this.input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

# 2.3总结

详情 选受控还是非受控 (opens new window)

feature uncontrolled controlled
one-time value retrieval (e.g. on submit)
validating on submit
instant field validation
conditionally disabling submit button
enforcing input format
several inputs for one piece of data
dynamic inputs

# 3.标签变化

# 3.1 textarea 标签

在 HTML 中, <textarea> 元素通过其子元素定义其文本

<textarea>
  你好, 这是在 text area 里的文本
</textarea>
1
2
3

在 React 中,<textarea> 使用 value 属性代替

...
handleChange(event) {
    this.setState({value: event.target.value});
  }
...
<textarea value={this.state.value} onChange={this.handleChange.bind(this)} />
...
1
2
3
4
5
6
7

# 3.2 select 标签

在 HTML 中,<select> 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表

<select>
  <option value="grapefruit">葡萄柚</option>
  <option value="lime">酸橙</option>
  <option selected value="coconut">椰子</option>
  <option value="mango">芒果</option>
</select>
1
2
3
4
5
6

React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'coconut'};

    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>
          选择你喜欢的风味:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">葡萄柚</option>
            <option value="lime">酸橙</option>
            <option value="coconut">椰子</option>
            <option value="mango">芒果</option>
          </select>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}
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

注意:可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项

<select multiple={true} value={['B', 'C']}>
1

# 3.3文件 input 标签

在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API (opens new window) 进行操作

<input type="file" />
1

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。应该使用 File API 与文件进行交互。

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<FileInput />);
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

# 【08状态提升】

# 1.介绍

使用 react 经常会遇到几个组件需要共用状态数据的情况,这就需要状态提升。所谓状态提升就是将各个子组件的共用state 提升到它们的父组件进行统一存储、处理。

# 2.子组件修改父组件的state

将父组件中负责setState的函数,以props的形式传给子组件,然后子组件在需要改变state时调用即可。

这是两个有关连的同级组件的传值,因为react的单项数据流,所以不在两个组件中进行传值,而是提升到 最近的共同的父级组件中,改变父级的state,进而影响了两个子级组件的render。

# 示例

让这两个温度保持一致(输入其中一个,另一个自动算出来)

image-20230326134008575

子组件

    class TemperatureInput extends React.Component {

        handleChange = (e) => {
            this.props.onTemperatureChange(e.target.value);
        };

        render() {
            return (
                <fieldset>
                    <legend>输入{scaleNames[this.props.scale]}:</legend>
                    <input type="number" value={this.props.temperature} onChange={this.handleChange}/>
                </fieldset>
            )
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

父组件

class Calculator extends React.Component {
    state = {
        celsius: '',
        fahrenheit: ''
    };

    onCelsiusChange = (value) => {
        this.setState({
            celsius: value,
            fahrenheit: tryConvert(value, toFahrenheit)
        });
    };

    onFahrenheitChange = (value) => {
        this.setState({
            celsius: tryConvert(value, toCelsius),
            fahrenheit: value
        });
    };

    render() {
        return (
            <div>
                <TemperatureInput scale='c' temperature={this.state.celsius}
                                  onTemperatureChange={this.onCelsiusChange}/>
                <TemperatureInput scale='f' temperature={this.state.fahrenheit}
                                  onTemperatureChange={this.onFahrenheitChange}/>
            </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

# 【09组合组件】

# 1.包含关系

有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中

组件标签里面包含的子元素会通过 props.children 传递进来

常用于抽取组件(样式)

import './One.css'

function One(props) {
 return (
    <div className={`one ${props.className}`}>{props.children}</div>
     				//模板字符串(使样式叠加)
  );
}

function Two(props) {
  return (
  //这使别的组件(Two)可以通过JSX嵌套,来将任意组件(div)作为子组件来传递给他们(One)
  <One className='two'> //可以将类名绑定的css样式传过去
      <div>Hello</div>
      <div>World</div>
  </One>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.特例关系

有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 AddDialogDeleteDialog可以说是 Dialog 的特殊实例

定制的Dialog可以给通用的Dialog传值,让它变成定制版的

function Dialog(props) {
  return (
      <h1>
        {props.title}
      </h1>
      <p>
        {props.message}
      </p>
  );
}

function AddDialog() {
  return (
    <Dialog
      title="Add"
      message="Do you wanna add this info?" />
  );
    
function DeleteDialog() {
  return (
    <Dialog
      title="Delete"
      message="Do you wanna delete this info?" />
  );
}
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

# 【10初始化脚手架】

React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我们提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用的最佳方式。

# 1.方式一:原生创建react项目

1.确保安装了 npmNode

npm i create-react-app -g
1

2.新建一个文件夹用于存放项目

在当前的文件夹下执行

create-react-app hello-react
1

3.在生成好的 hello-react 文件夹中执行如下命令启动项目

npm start
1
image-20230326173748969

# 2.方式二:使用vite创建react项目

vite官网 (opens new window)

  • 什么是vite?—— 新一代前端构建工具。
  • 优势如下:
    • 开发环境中,无需打包操作,可快速的冷启动。
    • 轻量快速的热重载(HMR)。
    • 真正的按需编译,不再等待整个应用编译完成。
## 创建工程

# npm 6.x
npm init vite@latest <project-name> --template react  
## 如: npm init vite@latest react-app --template react

# npm 7+,需要加上额外的双短横线
npm init vite@latest <project-name> --template react

## 使用 PNPM:
pnpm create vite <project-name> --template react
# pnpm create vite react-app -- --template react

## 进入工程目录
cd <project-name>
## 安装依赖
pnpm install
## 运行
pnpm run dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
image-20230326173544789

# 3.项目结构(方式一)

hello-react
├─ .gitignore               // 自动创建本地仓库
├─ package.json             // 相关配置文件
├─ public                   // 公共资源
│  ├─ favicon.ico           // 浏览器顶部的icon图标
│  ├─ index.html            // 应用的 index.html入口
│  ├─ logo192.png           // 在 manifest 中使用的logo图
│  ├─ logo512.png           // 同上
│  ├─ manifest.json         // 应用加壳的配置文件
│  └─ robots.txt            // 爬虫给协议文件
├─ src                      // 源码文件夹
│  ├─ App.css               // App组件的样式
│  ├─ App.js                // App组件
│  ├─ App.test.js           // 用于给APP做测试
│  ├─ index.css             // 样式
│  ├─ index.js              // 入口文件
│  ├─ logo.svg              // logo图
│  ├─ reportWebVitals.js    // 页面性能分析文件
│  └─ setupTests.js         // 组件单元测试文件
└─ package-lock.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最主要的就是App.js(组件)以及index.js(将组件渲染到页面中)

解读public目录下的 index.html 文件中的代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
      //指定浏览器图标的路径,这里直接采用 %PUBLIC_URL% 原因是 webpack 配置好了,它代表的意思就是 public 文件夹
    <meta name="viewport" content="width=device-width, initial-scale=1" />
      //用于做移动端网页适配
    <meta name="theme-color" content="#000000" />
      //用于配置安卓手机浏览器顶部颜色
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
      //用于描述网站信息
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
      //苹果手机触摸版应用图标
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
      //应用加壳时的配置文件
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
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

推荐使用这种目录结构去使用组件

image-20221025142952888

# 4.组件

在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种:基于函数的组件和基于类的组件。具体查看【02面向组件编程】。

# 4.1使用规范

为了使得项目结构更加的清晰,更易于维护,每个组件通常会存储到一个单独的文件中,并通过export导出。

//App.js
const App = () => {
    return <h1>我是一个React的组件!</h1>;
};

export default App;
1
2
3
4
5
6

在其他文件中使用时,需要先通过import进行引入。

//index.js
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
1
2
3
4
5
6

# 4.2组件化编码流程

1.拆分组件:拆分界面,抽取组件

2.实现静态组件

3.实现动态组件

  • 动态的显示初始化数据
    • 数据类型
    • 数据名称
    • 保存在哪个组件
  • 交互

注意事项:

1.拆分组件、实现静态组件。注意className、style的写法

2.动态初始化列表,如何确定将数据放在哪个组件的state中?

  • 某个组件使用:放在自身的state中
  • 某些组件使用:放在他们共同的父组件中【状态提升】

3.关于父子组件之间的通信

  • 父组件给子组件传递数据:通过props传递
  • 子组件给父组件传递数据:通过props传递,要求父组件提前给子组件传递一个函数

4.注意defaultChecked 和checked区别,defalutChecked只是在初始化的时候执行一次,checked没有这个限制,但是必须添加onChange方法类似的还有:defaultValue 和value

5.状态在哪里,操作状态的方法就在哪里

# 5.CSS样式

# 5.1内联样式

在React中可以直接通过标签的style属性来为元素设置样式。style属性需要的是一个对象作为值,来为元素设置样式。

<div style={{color:'skyblue'}}>
    我是Div
</div>
1
2
3

注意:如果样式名不符合驼峰命名法,需要将其修改为符合驼峰命名法的名字。

如:background-color改为backgroundColor。

如果内联样式编写过多,会导致JSX变得异常混乱,此时也可以将样式对象定义到JSX外,然后通过变量引入。

样式过多,JSX会比较混乱:

const StyleDemo = () => {
    return (
        <div style={{color:'red', backgroundColor:'#bfa', fontSize:20, borderRadius:12}}>
            我是Div
        </div>
    );
};

export default StyleDemo;
1
2
3
4
5
6
7
8
9

可以这样修改:

import React from 'react';

const StyleDemo = () => {
    const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}

    return (
        <div style={divStyle}>
            我是Div
        </div>
    );
};

export default StyleDemo;
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.2在内联样式中使用State

设置样式时,可以根据不同的state值应用不同的样式。

比如我们可以在组件中添加一个按钮,并希望通过点击按钮可以切换div的边框,代码可以这样写:

import React, {useState} from 'react';

const StyleDemo = () => {

    const [showBorder, setShowBorder] = useState(false);

    const divStyle = {
        color: 'red',
        backgroundColor: '#bfa',
        fontSize: 20,
        borderRadius: 12,
        border: showBorder?'2px red solid':'none'
    };

    const toggleBorderHandler = ()=> {
      setShowBorder(prevState => !prevState);
    };

    return (
        <div style={divStyle}>
            我是Div
            <button onClick={toggleBorderHandler}>切换边框</button>
        </div>
    );
};

export default StyleDemo;
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

上例中添加一个新的state,命名为showBorder,代码是这样的const [showBorder, setShowBorder] = useState(false);当该值为true时,我们希望div可以显示一条2像素的红色边框,当为false时,我们希望div没有边框。默认值为false。

divStyle的最后一个属性是这样设置的border: showBorder?'2px red solid':'none',这里我们根据showBorder的值来设置border样式的值,如果值为true,则设置边框,否则边框设置为none。

toggleBorderHandler 是负责修改showBorder的响应函数,当我们点击按钮后函数会对showBorder进行取反,这样我们的样式就可以根据state的不同值而呈现出不同的效果了。

# 5.3 外部样式表

直接在组件中import

//button.css
button{
    background-color: #bfa;
}
1
2
3
4
//button.js
import './Button.css';
const Button = () => {
    return <button>我是一个按钮</button>;
};
export default Button;
1
2
3
4
5
6

注意:

  1. 引入样式时直接import,无需指定名字,且引入样式必须以./或../开头
  2. 这种形式引入的样式是全局样式,有可能会被其他样式覆盖

# 5.4 css模块化

当组件逐渐增多起来的时候,就很有可能产生两个组件中样式名称有可能会冲突,这样会根据引入App这个组件的先后顺序,后面的会覆盖前面的。

使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以不用担心类名重复。

使用方式

CSS Module在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循如下几个步骤:

1.将css文件名修改: index.css --- >index.module.css

2.引入并使用的时候改变方式:

import React,{Component} from 'react'
import hello from './index.module.css'  //引入的时候给一个名称

export default class Hello extends Component{
    render() {
        return (
            <h1 className={hello.title}>Hello</h1>   //通过大括号进行调用
        )
    }
}
1
2
3
4
5
6
7
8
9
10

# 6.配置代理

需要下载axios

npm install axios
1

在使用的过程中很有可能会出现跨域的问题,这样就应该配置代理

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port), 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域 。

React怎么通过代理解决跨域问题?

利用服务器之间访问不会有跨域,在中间开启一个服务器,端口号和项目端口号一样

# 6.1方法一

在package.json中追加如下配置

"proxy":"请求的地址"      "proxy":"http://localhost:5000"  
1

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

# 6.2方法二

1.创建代理配置文件

在src下创建配置文件:src/setupProxy.js
1

2.编写setupProxy.js配置具体代理规则

const proxy = require('http-proxy-middleware')

module.exports = function(app) {
  app.use(
    proxy('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
      target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
      changeOrigin: true, //控制服务器接收到的请求头中host字段的值
      /*
      	changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
      	changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
      	changeOrigin默认值为false,但我们一般将changeOrigin值设为true
      */
      pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
    }),
    proxy('/api2', { 
      target: 'http://localhost:5001',
      changeOrigin: true,
      pathRewrite: {'^/api2': ''}
    })
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

# 6.3方法三(vite配置proxy)

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    // 是否自动打开浏览器
    open: true,
    // 代理
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:5000',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, ''),
      },
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 【11React路由5】

根据不同的路径,显示不同的组件(内容)

React使用的库react-router-dom

  • 专门来实现 SPA 应用

# 1.安装

npm i react-router-dom@5
1

# 2.ReactRouter三大组件

  1. Router:所有路由组件的根组件(底层组件),包裹路由规则的最外层容器

    ​ 属性:basename->设置跟此路由根路径,router可以在1个组件中写多个

  2. Route:路由规则匹配组件,显示当前规则对应的组件

  3. Link:路由跳转的组件

注意:如果要精确匹配,那么可以在route上设置exact属性

# 示例

import React from 'react';
import Hello from "../Hello";
import Welcome from "../Welcome";
//hash模式
//import {HashRouter as Router,Link,Route} from 'react-router-dom'
//history模式/后端匹配使用
import {BrowserRouter as Router,Link,Route} from 'react-router-dom'

const RouterTest = () => {
    return (
    <div>
        <Router>
        	<Link to="/hello">显示Hello页面</Link >
        	<br/>
        	<Link to="/welcome">显示Welcome页面</Link>
        	<Route path="/hello" component={Hello}></Route>
        	<Route path="/welcome" component={Welcome}></Route>
        <Router/>
    </div>)
}
            
export default RouterTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

注意点:

1.要在 Link 和 Route 标签的外层标签采用 BrowserRouter(或者HashRouter) 包裹,但是当路由过多时,要不停地更改标签包裹的位置,因此可以到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下

index.js

...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
      <App />
  </BrowserRouter>
);
...
1
2
3
4
5
6
7
8

2.Link组件可以设置to属性来进行页面的跳转,to属性可以直接写路径的字符串,也可以通过1个对象,进行路径的设置,如

...
let helloObj = {
            pathname:"/hello",//跳转的路径
            search:"?username=admin",//get请求参数
            hash:"#abc",//设置的HASH值
            state:{msg:'helloworld'}//传入组件的数据
        };
...
		<Router>
        	<Link to={helloObj}>显示Hello页面</Link >
        	<br/>
        	<Link to="/welcome">显示Welcome页面</Link>
        	<Route path="/hello" component={Hello}></Route>
        	<Route path="/welcome" component={Welcome}></Route>
        <Router/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.路由组件和一般组件

1.写法不同

一般组件:<Demo/>,路由组件:<Route path="/demo" component={Demo}/>

2.存放的位置不同

同时为了规范书写,将一般路由组件放在 pages/views 文件夹中,路由组件放在 components

而最重要的一点就是它们接收到的 props 不同,在一般组件中,如果我们不进行传递,就不会收到值。而对于路由组件而言,它会接收到 3 个固定属性 historylocation 以及 match

image-20221025230429965

重要的属性

history:
    go: ƒ go(n)
    goBack: ƒ goBack()
    goForward: ƒ goForward()
    push: ƒ push(path, state)
    replace: ƒ replace(path, state)
location:
    pathname: "/about"
    search: ""
    state: undefined

match:
    params: {}
    path: "/about"
    url: "/about"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.二级路由样式丢失

解决这个问题,有三个方法:

1.样式加载使用绝对位置

 <link href="/css/bootstrap.css" rel="stylesheet"> 
1

2.使用 %PUBLIC_URL%

 <link href="%PUBLIC_URL%/css/bootstrap.css" rel="stylesheet">
1

3.使用HashRouter

因为HashRouter会添加#,默认不会处理#后面的路径,所以也是可以解决的

# 5.模糊匹配和精准匹配

路由的匹配有两种形式:一种是精准匹配,一种是模糊匹配

React 中默认开启的是模糊匹配

模糊匹配可以理解为,在匹配路由时,只要有匹配到的就好了

精准匹配就是,两者必须相同

比如:

<Link to = "/home/a/b" >Home</Link>
1

此时该标签匹配的路由,分为三个部分 home a b;将会根据这个先后顺序匹配路由。

如下就可以匹配到相应的路由:

<Route path="/home" component={Home}/>
1

但是如果是下面这个就会失败,也就是说他是根据路径一级一级查询的,可以包含前面那一部分,但并不是只包含部分就可以。

<Route path="/a" component={Home}/>
1

当然也可以使用这个精确的匹配exact={true}

如以下:这样就精确的匹配/home,则上面的/home/a/b就不行了

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

# 6.Switch解决相同路径问题

<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
1
2
3

image-20221026132313014

原因:Route 的机制,当匹配上了第一个 /about 组件后,它还会继续向下匹配,因此会出现两个 About 组件

办法:采用 Switch 组件进行包裹,让switch组件中的route只匹配1个,只要匹配到了,剩余的路由规则将不再匹配

<Switch>
    <Route path="/home" component={Home}></Route>
    <Route path="/about" component={About}></Route>
    <Route path="/about" component={About}></Route>
</Switch>
1
2
3
4
5

# 7.路由重定向

如果访问某个组件时,如果有重定向组件,那么就会修改页面路径,使得页面内容显示为所定向路径的内容

function LoginInfo(props){
    //props.loginState = 'success';
    //props.loginState = "fail"
    console.log(props)
    if(props.location.state.loginState === 'success'){
        return <Redirect to="/admin"></Redirect>
    }else{
        return <Redirect to="/login"></Redirect>
    }
}
1
2
3
4
5
6
7
8
9
10

# 8.传递参数

# 8.1传递 params 参数

可以通过将数据拼接在路由地址末尾来实现数据的传递

 <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
1

注意:需要采用模板字符串以及 $ 符的方式来进行数据的获取

在注册路由时,可以通过 :参数名 来传递数据

<Route path="/home/message/detail/:id/:title" component={Detail} />
1

这样既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件

传递的数据被接收到了对象的 match 属性下的 params 中

因此可以在 Detail 组件中获取到由 Message 组件中传递来的 params 数据,并通过 params 数据中的 id 值,在详细内容的数据集中查找出指定 id 的详细内容

const { id, title } = this.props.match.params
const findResult = DetailData.find((detailObj) => {
    return detailObj.id === id
})
1
2
3
4

最后渲染数据即可

# 8.2传递 search 参数

首先在Link 中采用 ? 符号的方式来表示后面的为可用数据

<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
1

采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到

数据保存在了 location 对象下的 search 中,是一种字符串的形式保存的,可以引用一个库来进行转化 qs

采用 parse 方法,将字符串转化为键值对形式的对象

const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1)) // 从?后面开始截取字符串
1
2

最后渲染数据即可

querystring

qs是一个npm仓库所管理的包,可通过npm install qs命令进行安装

  1. qs.parse()将URL解析成对象的形式

  2. qs.stringify()将对象序列化成URL的形式,以&进行拼接

//qs.parse()
const str = "username='admin'&password='123456'";
console.log(qs.parse(str)); 
// Object { username: "admin", password: "123456" }

//qs.stringify()
const a = qs.stringify({ username: 'admin', password: '123456' });
console.log(a); 
// username=admin&password=123456
1
2
3
4
5
6
7
8
9
qs.stringify() 和JSON.stringify()有什么区别?

    var a = {name:'hehe',age:10};
    qs.stringify序列化结果如下
    name=hehe&amp;age=10
    --------------------
    而JSON.stringify序列化结果如下
    "{"a":"hehe","age":10}"
1
2
3
4
5
6
7
8

# 8.3传递 state 参数

<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
1

首先需要在 Link 中注册跳转时,传递一个路由对象,包括一个跳转地址名,一个 state 数据,这样就可以在 Detail 组件中获取到这个传递的 state 数据

注意:采用这种方式传递,无需声明接收

然后可以在 Detail 组件中的 location 对象下的 state 中取出我们所传递的数据

const { id, title } = this.props.location.state
1

image-20221026133411288

为了解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如,

const { id, title } = this.props.location.state || {}
1

当获取不到 state 时,则用空对象代替

这里的 state 和状态里的 state 有所不同

# 8.4小结

1.params参数
            路由链接(携带参数):&lt;Link to='/demo/test/tom/18'}>详情&lt;/Link>
            注册路由(声明接收):&lt;Route path="/demo/test/:name/:age" component={Test}/>
            接收参数:this.props.match.params
2.search参数
            路由链接(携带参数):&lt;Link to='/demo/test?name=tom&amp;age=18'}>详情&lt;/Link>
            注册路由(无需声明,正常注册即可):&lt;Route path="/demo/test" component={Test}/>
            接收参数:this.props.location.search
            备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
3.state参数
            路由链接(携带参数):&lt;Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情&lt;/Link>
            注册路由(无需声明,正常注册即可):&lt;Route path="/demo/test" component={Test}/>
            接收参数:this.props.location.state
            备注:刷新也可以保留住参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14

接收参数

// 接收params参数
const {id,title} = this.props.match.params 

// 接收search参数
const {search} = this.props.location
const {id,title} = qs.parse(search.slice(1))

// 接收state参数
const {id,title} = this.props.location.state || {}
1
2
3
4
5
6
7
8
9

# 9.路由跳转

# 9.1push 与 replace 模式

默认开启push 模式,即每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址

当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶

我们只需要在需要开启的链接上加上 replace 即可

<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msg
1

# 9.2编程式路由导航

借助this.prosp.history对象上的API对操作路由跳转、前进、后退
        -this.prosp.history.push()
        -this.prosp.history.replace()
        -this.prosp.history.goBack()
        -this.prosp.history.goForward()
        -this.prosp.history.go(1)
1
2
3
4
5
6

可以采用绑定事件的方式实现路由的跳转,在按钮上绑定一个 onClick 事件,当事件触发时,执行一个回调

//push跳转+携带params参数
// this.props.history.push(`/home/message/detail/${id}/${title}`)

//push跳转+携带search参数
// this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)

//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})

//replace跳转+携带params参数
//this.props.history.replace(`/home/message/detail/${id}/${title}`)

//replace跳转+携带search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)

//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 9.3 withRouter

当需要在页面内部添加回退前进等按钮时,由于这些组件一般通过一般组件的方式去编写,因此会遇到一个问题:无法获得 history 对象,这正是因为采用的是一般组件造成的。

只有路由组件才能获取到 history 对象

可以利用 react-router-dom 对象下的 withRouter 函数来对导出的 Header 组件进行包装,这样就能获得一个拥有 history 对象的一般组件

需要对哪个组件包装就在哪个组件下引入

// Header/index.jsx
import { withRouter } from 'react-router-dom'
// 在最后导出对象时,用 `withRouter` 函数对 index 进行包装
export default withRouter(index);
1
2
3
4

这样就能让一般组件获得路由组件所特有的 API

# 10.BrowserRouter 和 HashRouter 的区别

# 底层实现原理不一样

  1. BrowserRouter 使用的是 React 为它封装的 history API ,这里的 history 和浏览器中的 history 有所不同。通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。

  2. HashRouter是通过 URL 的哈希值

    可以理解为是锚点跳转,因为锚点跳转会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,HashRouter 不会将 # 符号后面的内容请求,兼容性更好

# 地址栏的表现形式不一样

  • HashRouter 的路径中包含 # ,例如 localhost:3000/#/demo/test

# 刷新后路由 state 参数改变

  1. 在BrowserRouter 中,state 保存在 history 对象中,刷新不会丢失
  2. HashRouter 则刷新会丢失 state

# 【12React路由6】

# 1.安装

npm install react-router-dom@6
1

# 2.V6常用路由组件和hooks

组件名 作用 说明
<Routers> 一组路由 代替原有<Switch>,所有子路由都用基础的Router children来表示
<Router> 基础路由 Route 用来定义一个访问路径与 React 组件之间的关系
<Link> 导航组件 在实际页面中跳转使用
<NavLink> 导航组件 作用和 Link 类似,多了一个导航高亮效果
<Outlet/> 自适应渲染组件 根据实际路由url自动选择组件
hooks名 作用 说明
useParams 返回当前参数 根据路径读取参数
useNavigate 返回当前路由 代替原有V5中的 useHistory
useOutlet 返回根据路由生成的element
useLocation 返回当前的location 对象
useRoutes 同Routers组件一样,只不过是在js中使用
useSearchParams 用来匹配URL中?后面的搜索参数

# 3.Routes 与 Route

  1. v6版本中移出了先前的<Switch>,引入了新的替代者:<Routes>
  2. <Routes><Route>要配合使用,且必须要用<Routes>包裹<Route>
  3. <Route> 相当于一个 if 语句,如果其路径与当前 URL 匹配,则呈现其对应的组件。
  4. <Route caseSensitive> 属性用于指定:匹配时是否区分大小写(默认为 false)。
  5. 当URL发生变化时,<Routes>都会查看其所有子<Route> 元素以找到最佳匹配并呈现组件 。
  6. <Route> 也可以嵌套使用,且可配合useRoutes()配置 “路由表” ,但需要通过 <Outlet> 组件来渲染其子路由
<Routes>
    /*path属性用于定义路径,element属性用于定义当前路径所对应的组件*/
    <Route path="/login" element={<Login />}></Route>

		/*用于定义嵌套路由,home是一级路由,对应的路径/home*/
    <Route path="home" element={<Home />}>
        /*test1 和 test2 是二级路由,对应的路径是/home/test1 或 /home/test2*/
      <Route path="test1" element={<Test/>}></Route>
      <Route path="test2" element={<Test2/>}></Route>
	</Route>
	
		//Route也可以不写element属性, 这时就是用于展示嵌套的路由 .所对应的路径是/users/xxx
    <Route path="users">
       <Route path="xxx" element={<Demo />} />
    </Route>
</Routes>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.默认页路径实现(如 404 页)

在路由列表 Routes 中,可以加入一个 catch all 的默认页面,比如用来作 404 页面。

只要在最后加入 path* 的一个路径,意为匹配所有路径

function App() {
  return <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  </BrowserRouter>
}

// 用来作为 404 页面的组件
const NotFound = () => {
  return <div>你来到了没有知识的荒原</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5.useRoutes注册路由实现(推荐)

src文件夹下新建子文件夹:routesroutes下新建文件:index.js 路由表独立成 js 文件:src/routes/index.js

//routes/index.js

import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";

const routes = [
    {
        path:"/about",
        element:<About/>
    },
    {
        path:"/home",
        element:<Home/>
    },
    {
        path:"/",
        element:<Navigate to="/about"/>
    }
]

export default routes;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//App.js

import React from 'react'
import {NavLink,useRoutes} from "react-router-dom";
import routes from "./routes";

export default function App() {
  const element = useRoutes(routes);
  return (
    <div>
        <NavLink to="/about">About</NavLink>
        <NavLink to="/home">Home</NavLink>
        <div className="content">
        	{element}
        </div>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 6.嵌套路由的实现

路由结构如下:

  • /about,About组件
  • /home,Home组件
    • /home/news,News组件
    • /home/message,Message组件

children 来嵌套路由。

//routes/index.js

import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
import News from "../pages/News";
import Message from "../pages/Message";

const routes = [
    {
        path:"/about",
        element:<About/>
    },
    {
        path:"/home",
        element:<Home/>,
        children:[
            {
                path:"news",
                element:<News/>
            },
            {
                path:"message",
                element:<Message/>
            },
        ]
    },
    {
        path:"/",
        element:<Navigate to="/about"/>
    }
]

export default routes;
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

<Outlet>

作用:当<Route>产生嵌套时,渲染其对应的后续子路由。

image-20230412150416784

点击News下面就显示News组件的内容

点击Message下面就显示News组件的内容

如果不加<Outlet>,就没有内容显示

//Home/index.jsx

import React from 'react';
import { NavLink,Outlet } from 'react-router-dom';

export default function Home() {
  return (
    <div>
      <h2>Home组件内容</h2>
      <div>
        <ul className="nav nav-tabs">
          <li>
            <NavLink to="/home/news">News</NavLink>
            {/* <NavLink to="./news">News</NavLink> */}
            {/* <NavLink to="news">News</NavLink> */}
          </li>
          <li>
            <NavLink to="/home/message">Message</NavLink>
          </li>
        </ul>
        <Outlet/>
      </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
  • 路由链接中的to属性值,可以是
    • to="/home/news",即全路径(推荐这样写,不然直接看不知道是不是子路由)
    • to="./news",即相对路径
    • to="news"

# 7.路由传/接参

# 7.1 params

useParams()

需求:点击“消息x”,显示其id、title和content

//routes/index.js
//注意路径写法

import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
import News from "../pages/News";
import Message from "../pages/Message";
import Detail from "../pages/Detail";

const routes = [
    {
        path:"/about",
        element:<About/>
    },
    {
        path:"/home",
        element:<Home/>,
        children:[
            {
                path:"news",
                element:<News/>
            },
            {
                path:"message",
                element:<Message/>,
                children:[
                    {
                        path:"detail/:id/:title/:content",
                        element:<Detail/>
                    }
                ]
            },
        ]
    },
    {
        path:"/",
        element:<Navigate to="/about"/>
    }
]

export default routes;
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
//Message/index.jsx
//传值到Detail组件

import React,{useState} from 'react'
import { NavLink,Outlet } from 'react-router-dom'

export default function Message() {
  const [message] = useState([
    { id: "001", title: "消息1", content: "窗前明月光" },
    { id: "002", title: "消息2", content: "疑是地上霜" },
    { id: "003", title: "消息3", content: "举头望明月" },
    { id: "004", title: "消息4", content: "低头思故乡" }
  ])

  return (
    <div>
      <hr />
      <h3>Message组件内容</h3>
      <ul>
        {
          message.map(msgObj => {
            return (
              <li key={msgObj.id}>
                <NavLink to={`/home/message/detail/${msgObj.id}/${msgObj.title}/${msgObj.content}`}>{msgObj.title}</NavLink>
              </li>
            )
          })
        }
      </ul>
      <hr />
      <Outlet />
    </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
  • 使用新Hook useParams 接 params 参数
//Detail/index.jsx
//接收Message组件传来的值

import React from 'react'
import { useParams } from 'react-router-dom'
// import { useMatch } from 'react-router-dom'

export default function Detail() {
  const {id,title,content} = useParams();
  // const {params:{id,title,content}}= useMatch("/home/message/detail/:id/:title/:content");

  return (
    <ul>
      <li>消息编号:{id}</li>
      <li>消息标题:{title}</li>
      <li>消息内容:{content}</li>
    </ul>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

useSearchParams

需求:点击“消息x”,显示其id、title和content

//routes/index.js

...
			{
                path:"message",
                element:<Message/>,
                children:[
                    {
                        path:"detail",
                        element:<Detail/>
                    }
                ]
            }
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Message/index.jsx
//传值到Detail组件

export default function Message() {
    return (
        <div>
            <ul>
            {
                message.map(msgObj => {
                    return (
                        <li key={msgObj.id}>
                            <Link to={`detail?id=${msgObj.id}&title=${msgObj.title}&content=${msgObj.content}`}>{msgObj.title}</Link>
                        </li>
                    )
                })
            }
            </ul>
            <hr />
            <Outlet/>
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 使用新Hook useSearchParams 接 search参数
//Detail/index.jsx
//接收Message组件传来的值

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

export default function Detail() {
  const [search,setSearch] = useSearchParams();
  const id = search.get("id");
  const title = search.get("title");
  const content = search.get("content");

  return (
    <ul>
      <li>消息编号:{id}</li>
      <li>消息标题:{title}</li>
      <li>消息内容:{content}</li>
    </ul>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用setSearch可以设置参数的值

...
<button onClick={()=>setSearch('id=008&title=哈哈&content=嘻嘻')}>点我设置search参数</button>
...
1
2
3

也可以用 useLocation接 search 的值,但是结果如下

image-20230412153834862

这时需要qs来parse

import { useLocation } from 'react-router-dom'
import qs from "qs";

export default function Detail() {
  const {search} = useLocation();
  const {id,title,content} = qs.parse(search.slice(1));

  return (
    <ul>
      <li>消息编号:{id}</li>
      <li>消息标题:{title}</li>
      <li>消息内容:{content}</li>
    </ul>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3 state(推荐)

//routes/index.js

...
        {
            path:"message",
                element:<Message/>,
                    children:[
                        {
							path:"detail",
                            element:<Detail/>
                        }
                    ]
        }
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Message/index.jsx

...
<NavLink to='detail' state={msgObj}>{msgObj.title}</NavLink>
...
1
2
3
4
5
  • 使从新Hook useLocation 中获取 state 参数
//Detail/index.jsx

...
	console.log(useLocation())
// 直接连续结构获取 location 中的 state 的属性
    const { state: { id, title, info } } = useLocation()
...
1
2
3
4
5
6
7
image-20230412160733748

# 8.编程式路由导航

  • 使用新 Hook useNavigate 实现编程式路由导航

    • useNavigate()

      获取的函数对象可以进行路由跳转,可以传入两个参数

      • path
      • 参数对象
        • replace:跳转模式
        • state:就是state参数

1.Detail组件:接收参数方式还是一样,具体看7.路由传/接参

2.routes/index.js里面的path方式也要对应

# 8.1路由传递params参数

//Message/index.jsx

import React,{useState} from 'react'
import { NavLink,Outlet,useNavigate } from 'react-router-dom'

export default function Message() {
    const [message] = useState([
        {id:"001",title:"消息1",content:"窗前明月光"},
        {id:"002",title:"消息2",content:"疑是地上霜"},
        {id:"003",title:"消息3",content:"举头望明月"},
        {id:"004",title:"消息4",content:"低头思故乡"}
    ])

    const navigate = useNavigate();

    const handleClick = msgObj => {
        const {id,title,content} = msgObj
        navigate(`detail/${id}/${title}/${content}`,{replace:false})
    }

    return (
        <div>
            <ul>
            {
                message.map(msgObj => {
                    return (
                        <li key={msgObj.id}>
                            <button onClick={() => handleClick(msgObj)}>查看消息</button>
                        </li>
                    )
                })
            }
            </ul>
            <hr />
            <Outlet/>
        </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

# 8.2路由传递search参数

//Message/index.jsx

export default function Message() {

    const navigate = useNavigate();

    const handleClick = msgObj => {
        const {id,title,content} = msgObj
        navigate(`detail?id=${id}&title=${title}&content=${content}`,{replace:false})
    }

    return (
        <div>
            <ul>
            {
                message.map(msgObj => {
                    return (
                        <li key={msgObj.id}>
                            <button onClick={() => handleClick(msgObj)}>查看消息</button>
                        </li>
                    )
                })
            }
            </ul>
            <hr />
            <Outlet/>
        </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

# 8.3路由传递state参数(推荐)

//Message/index.jsx

export default function Message() {

    const navigate = useNavigate();

    const handleClick = msgObj => {
        navigate("detail",{
            replace: false,
            state: msgObj
        })
    }

    return (
        <div>
            <ul>
            {
                message.map(msgObj => {
                    return (
                        <li key={msgObj.id}>
                            <button onClick={() => handleClick(msgObj)}>查看消息</button>
                        </li>
                    )
                })
            }
            </ul>
            <hr />
            <Outlet/>
        </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

# 8.4 useNavigate 实现前进后退操作

//Header.jsx

import React from 'react'
import { useNavigate } from 'react-router-dom'

export default function Header() {

    const navgate = useNavigate()

    const back = () => {
        navgate(-1)
    }

    const forward = () => {
        navgate(1)
    }


    return (
        <div className="row">
            <div className="col-xs-offset-2 col-xs-8">
                <div className="page-header">
                    <h1>React Router Demo</h1>
                    <button onClick={back}>后退</button>
                    <button onClick={forward}>前进</button>
                </div>
            </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

# 9.汇总

# Components

  • BrovserRouter

  • HashRouter

  • Routes + Route

  • Link

  • NavLink

    • 作用和 Link 类似,多了一个导航高亮效果(默认高亮样式类名是 active,可省略不写)

    • <NavLink end>, end 属性表示子路由高亮时此路由不再高亮

    • V6移除了 activeClassName ,如果高亮样式类名是 active ,需要配置 className 为一个函数

      • 函数接收两个参数

        • isActive:是否激活
        • isPending
      • const activeClassName = ({isActive}) => 
        	isActive ? 'list-group-item active' : 'list-group-item'
        
        <NavLink className={activeClassName} to="/about" >About</NavLink>
        
        1
        2
        3
        4
  • Navigate

    • V6中用 <Navigate> 顶替了 <Redirect>。用于路由重定向
    • 只要 <Navigate> 组件被渲染,就会修改路径,切换视图
    • replace 属性用于控制跳转模式(push 或 replace,默认是 push)
  • Outlet

    • 嵌套路由中,需要使用 <Outlet> 设置子路由的路由出口,即在何处渲染子路由
    • 设置二级路由链接时,可以是 to="news"to="./news",但不能是 to="/news"

# Hooks

  • useRoutes()

  • useNavigate()

  • useParams()

  • useSearchParams()

  • useLocation()

  • useMatch()

  • useInRouterContext()

    • 如果组件在 Router 的上下文中呈现(处在路由组件内),则 useInRouterContext() 钩子返回 true,否则返回 false
  • useNavigationType()

    • 返回当前的导航类型(用户是如何来到当前页面的)
    • 返回值:POPPUSHREPLACE
    • 备注:POP是指在浏览器中直接打开了这个路由组件(刷新页面或者路由首页)
  • useOutlet()

    • 用来呈现当前组件中要渲染的嵌套路由

    • const result = useOutlet()
      // 如果嵌套路由没挂载:null
      // 如果嵌套路由已挂载:嵌套路由对象
      
      1
      2
      3
  • useResolvedPath().

    • 给定一个URL值,解析其中的 path、search、hash 值

# 【13Hook】

# 1.什么时候会用 Hook

如果在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在可以在现有的函数组件中使用 Hook。

# 2.State Hook

# 2.1声明 State 变量

在函数组件中,没有 this,所以不能分配或读取 this.state。可以直接在组件中调用 useState Hook:

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量
  const [count, setCount] = useState(0);
  console.log(count, setCount)
}
1
2
3
4
5
6
7

它让函数式组件能够维护自己的 state ,它接收一个参数,作为初始化 state 的值,赋值给 count,因此 useState 的初始值只有第一次有效,它所映射出的两个变量 countsetCount 我们可以理解为 setState 来使用

useState 能够返回一个数组,第一个元素是 state ,第二个是更新 state 的函数

# 2.2读取 State变量

<p>You clicked {count} times</p>
1

# 2.3 更新 State

<button onClick={() => setCount(count + 1)}>
    Click me
 </button>
1
2
3

# 2.4 使用多个 state 变量

将 state 变量声明为一对 [something, setSomething] 也很方便,因为如果想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:

function ExampleWithManyStates() {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
...
}
...
function handleOrangeClick() {
    setFruit('orange');
}
1
2
3
4
5
6
7
8
9
10
11

# 3.Effect Hook

useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染

通过使用这个Hook,我们可以设置React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。

提示

如果熟悉 React class 的生命周期函数,可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

# 3.1副作用操作

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的

副作用:React组件有部分逻辑可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。

例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

# 3.2不需要清除的副作用

发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作

使用 Hook 的示例

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。

传递给 useEffect 的函数在每次渲染中都会有所不同,这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

为什么在组件内部调用 useEffect

useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?

是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

提示:与 componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffectHook 供你使用,其 API 与 useEffect 相同。

# 3.3需要清除的 effect

例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。

使用 Hook 的示例:

假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribe(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

如果effect 返回一个函数,React 将会在下一次effect执行前调用它,我们可以在这个函数中清除掉前一次effect执行所带来的影响。

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

注意

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

# 3.4使用 Hook 的示例

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
1
2
3
4
5

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
1
2
3

传入 [count] 作为第二个参数的作用:

如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect

对于有清除操作的 effect 同样适用:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribe(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
1
2
3
4
5
6
7
8
9
10

如果要用这种优化方式,确保使用了第二个参数里的变量的函数在useEffect里面

# 4.useRef

const refContainer = useRef(initialValue);
1

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

一个常见的用例便是命令式地访问子组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

useRef()返回的是一个普通的JS对象,JS对象中有一个current属性,它指向的便是原生的DOM对象。上例中,如果想访问div的原生DOM对象,只需通过inputEl.current即可访问,它可以调用DOM对象的各种方法和属性。

# 5.useReducer

# 5.1基本使用

为了解决复杂State带来的不便,React为我们提供了一个新的使用State的方式。reducer的作用就是将那些和同一个state相关的所有函数都整合到一起,方便在组件中进行调用。

工具都有其使用场景,Reducer也不例外,它只适用于那些比较复杂的state

const [state, dispatch] = useReducer(reducer, initialArg, init);
1

它的返回值和useState()类似

第一个参数是state用来读取state的值。

第二个参数dispatch是一个函数,不同于setState(),这个函数我们可以称它是一个“派发器”,通过它可以向reducer()发送不同的指令,控制reducer()做不同的操作。

它的参数有三个,第三个我们暂且忽略,只看前两个:

1.reducer()是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state值。当我们调用dispatch()时,dispatch()会将消息发送给reducer()reducer()可以根据不同的消息对state进行不同的处理。

2.initialArg就是state的初始值,和useState()参数一样。

以下是用 reducer写的的计数器示例:

/*
*   参数:
*       reducer : 整合函数
*           对于我们当前state的所有操作都应该在该函数中定义
*           该函数的返回值,会成为state的新值
*           reducer在执行时,会收到两个参数:
*               state 当前最新的state
*               action 它需要一个对象
*                      在对象中会存储dispatch所发送的指令
*       initialArg : state的初始值,作用和useState()中的值是一样
*   返回值:
*       数组:
*           第一个参数,state 用来获取state的值
*           第二个参数,state 修改的派发器
*                   通过派发器可以发送操作state的命令
*                   具体的修改行为将会由另外一个函数(reducer)执行
* */

// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
function countReducer(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 [count, countDispatch] = useReducer(countReducer, {count: 0});
    // 这里本来初始值是直接给0的,但是为了countReducer函数中的state写成对象形式
    
  return (
    <>
      Count: {count.count}
      <button onClick={() => countDispatch({type: 'decrement'})}>-</button>
      <button onClick={() => countDispatch({type: 'increment'})}>+</button>
    </>
  );
}
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

# 5.2state初始化的两种方式

指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:

const [state, dispatch] = useReducer(
reducer,
{count: 0}  );
1
2
3

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

export default function App() {
  return (
    <div>
      <Counter initialCount={0} />
    </div>
  )
}

function countInit(initialCount) {
  return {count: initialCount};
}

function countReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return countInit(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [count, countDispatch] = useReducer(countReducer, initialCount, countInit);
  return (
    <>
      Count: {count.count}
      <button
        onClick={() => countDispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => countDispatch({type: 'decrement'})}>-</button>
      <button onClick={() => countDispatch({type: 'increment'})}>+</button>
    </>
  );
}
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

# 5.3 跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 (opens new window) 来比较 state。)

这里的state如果是个对象,还是会渲染子组件,因为我们返回的是一个新对象,应该比较的是地址,如果直接将state返回,子组件是不会重新渲染的

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以不必担心。

# 6.React.memo(高阶组件)

# 6.1基本介绍

React.memo() 是一个高阶组件,如果组件在相同 props 的情况下渲染相同的结果,那么可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。

  • 它接收另一个组件作为参数,并且会返回一个包装过的新组件
  • 包装过的新组件就会具有缓存功能,这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
  • 包装过后,只有组件的props发生变化,才会触发组件的重新的渲染,否则总是返回缓存中结果。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

# 6.2使用场景

当组件的父组件重新渲染,而自身的state没有改变时,没有必要重新渲染

//APP.jsx
import React, { useState } from 'react'
import A from './A'

export default function App() {
  console.log('App渲染')

  const [count, setCount] = useState(1)

  const clickHandler = () => {
    setCount(prevState => prevState + 1)
  }

  return (
    <div>
      <h2>App -- {count}</h2>
      <button onClick={clickHandler}>增加</button>
      <A />
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用React.memo包裹A组件

//A.jsx
const A = () => {
  console.log('A渲染')
  return <div>我是A组件</div>
}

export default React.memo(A)
1
2
3
4
5
6
7

没有直接使用A组件,而是在A组件外层套了一层函数React.memo(),这样一来,返回的A组件就增加了缓存功能,只有当A组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染

注意:

1.此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

2.与 class 组件中 shouldComponentUpdate()方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反

# 6.3容易出错的情况

我们把App组件clickHandler方法传递给A组件,让A组件也能够改变App组件state

//APP.jsx
import React, { useState } from 'react'
import A from './A'

export default function App() {
  console.log('App渲染')

  const [count, setCount] = useState(1)

  const clickHandler = () => {
    setCount(prevState => prevState + 1)
  }

  return (
    <div>
      <h2>App -- {count}</h2>
      <button onClick={clickHandler}>增加</button>
      <A clickHandler={clickHandler} />
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//A.jsx
const A = props => {
  console.log('A渲染')
  return (
    <div>
      我是A组件
      <button onClick={props.clickHandler}>A组件的增加</button>
    </div>
  )
}

export default React.memo(A)
1
2
3
4
5
6
7
8
9
10
11
12

点击A组件的增加,发现A组件也重新渲染了

image-20221030175830062

这是因为App组件重新渲染的时候,clickHandler也重新创建了,这时传递给子组件的clickHandler和上一次不一样,所以react.memo失效了。

这个问题可以用useCallback解决。

# 7.useCallback

# 7.1基本介绍

/*
*   useCallback()
*		这个hook会缓存方法的引用
*       参数:
*           1. 回调函数
*           2. 依赖数组
*               - 当依赖数组中的变量发生变化时,回调函数才会重新<创建>
*               - 如果不指定依赖数组,回调函数每次都会重新创建
*               - 一定要将回调函数中使用到的所有变量都设置到依赖数组中
*                   除了(setState)
* */
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

把内联回调函数及依赖数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

# 7.2使用场景

解决6.3的问题

clickHandler方法改造一下

  const clickHandler = useCallback(() => {
    setCount(prevState => prevState + 1)
  }, [])
1
2
3

第二个参数一定要加,不然和平常写没有区别

依赖项[]的意思是只有第一次渲染时才会创建,之后都不会重新创建了

点击A组件的增加,发现只有App组件重新渲染了。因为clickHandler没有重新创建,传给子组件的没有变化,所以子组件这次没有重新渲染。

image-20221030180349406

# 7.3测试第二个参数

改造代码:增加一个num,让每一次count的增加比上次多1

import React, { useState, useCallback } from 'react'

export default function App() {
  console.log('App渲染')

  const [count, setCount] = useState(1)
  // 增加
  const [num, setNum] = useState(1)

  const clickHandler = useCallback(() => {
    setCount(prevState => prevState + num)
  // 增加
    setNum(prevState => prevState + 1)
  }, [])

  return (
    <div>
      <h2>App -- {count}</h2>
      <button onClick={clickHandler}>增加</button>
      <A clickHandler={clickHandler} />
    </div>
  )
}

const A = React.memo(props => {
  console.log('A渲染')
  return (
    <div>
      我是A组件
      <button onClick={props.clickHandler}>A组件的增加</button>
    </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

现在这样写是有问题的

image-20221030181249832

点击了两次增加后,预期值应该是4,但是显示的是3,是为什么呢?

因为clickHandler只在初次渲染的时候创建,当时num的值是1,这个函数一直没有重新创建,内部用的num一直是1

这时我们可以加一个依赖项

const clickHandler = useCallback(() => {
    setCount(prevState => prevState + num)
    setNum(prevState => prevState + 1)
  }, [num])
1
2
3
4

这样num变化了,这个函数也会重新创建。

image-20221030181534667

点击了两次增加后,count变成了预期值4

# 8.useMemo

# 8.1基本介绍

useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分复杂的逻辑,执行速度比较慢。避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,如:

const result = useMemo(()=>{
    return 复杂逻辑函数();
},[依赖项])
1
2
3

useMemo中的函数会在依赖项发生变化时执行,执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。

注意:是执行,这点和useCallback不同,useCallback是创建。

# 8.2使用场景

import React, { useMemo, useState } from 'react'

const App = () => {
  const [count, setCount] = useState(1)

  let a = 123
  let b = 456

  function sum(a, b) {
    console.log('sum执行了')
    const begin = +new Date()
    while (true) {
      if (Date.now() - begin > 3000) break   //这个函数起码3秒才能执行完
    }
    return a + b
  }
  return (
    <div>
      <h1>App</h1>
      <p>sum的结果:{sum(a, b)}</p>
      <h3>{count}</h3>
      <button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
    </div>
  )
}

export default 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
image-20221107143451560

这个时候因为sum函数要3秒才能执行完,导致下面数字显示也变慢了3秒

使用 useMemo 解决上面的问题

改写sum`方法的调用

<p>sum的结果:{useMemo(() => sum(a, b), [])}</p>
1

第一次加载慢是不可避免的,但是这个钩子函数将sum函数的返回值缓存起来,这样模板重新渲染时就没有再去执行sum函数,而是直接使用上一次的返回值

# 8.3测试第二个参数

改造上面的代码,把Sum单独抽离成一个组件

//Sum.jsx
import React from 'react'

export default function Sum(props) {
  console.log('Sum执行了')
  return <span>{props.a + props.b}</span>
}
App.jsx
1
2
3
4
5
6
7
8

添加一个功能可以变换a的值

import React, { useMemo, useState } from 'react'
import Sum from './Sum'

const App = () => {
  const [count, setCount] = useState(1)

  let a = 123
  let b = 456

  if (count % 2 === 0) a = a + 1

  const result = useMemo(() => <Sum a={a} b={b} />, [])

  return (
    <div>
      <h1>App</h1>
      <p>sum的结果:{result}</p>
      <h3>{count}</h3>
      <button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
    </div>
  )
}

export default 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

现在有一个问题,如果Sum组件接收的值变化了,网页上显示的还是原来的缓存值,这个时候就要利用第二个参数。

image-20221107145159066

//App.jsx
const result = useMemo(() => <Sum a={a} b={b} />, [a])
1
2

如果a的值变化了,将会重新计算

image-20221107145403725

# 实际开发使用场景

1.用户停止输入一秒后向服务器发送请求

  • 设置定时器
  • 在useEffect中返回一个清除定时器的函数
红色高跟鞋
峰源萨克斯