React 入门

React

一、React 基础

1. 什么是 React

React 是一个用于构建用户界面的前端框架。

它不是一个 MVC 的框架。它仅仅有 V 也就是视图层。也就是只负责视图的渲染。

2. React 的特点

  • 声明式

    只需要描述 UI 和写 HTML 是一样的

  • 基于组件

    组件是 React 最重要的内容。利用组件可以实现组合和复用

  • 学习一次,随处使用

    用处很多

3. React 的安装

安装命令:

npm i react react-dom

react 包是核心,提供创建元素、组件等功能

react-dom 包提供 DOM 相关功能

4. React 的使用

  1. 引入 react 和 react-dom 两个 js文件

    <script src="./node_modules/react/umd/react.development.js"></script>
    <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
    
  2. 创建 React 元素

    const title = React.createElement('h1', null, 'Hello React')
    
  3. 渲染React 元素到页面

    <div id="root"></div>
    <script>
    	// 第一个参数:创建的什么标签
     	// 第二个参数:标签上带什么属性
        // 第三个参数:子标签(例如文本标签)
    	const title = React.createElement('h1', null, 'Hello React')
        // 第一个参数:渲染的 React 元素
        // 第二个参数:渲染的位置。挂到哪个元素上。
    	ReactDOM.render(title, document.getElementById('root'))
    </script>
    

5. React 脚手架

  1. 意义

    1. 脚手架是开发 现代Web 应用的必备。

    2. 充分利用 Webpack、Babel、ESLint 等工具辅助项目开发。

    3. 零配置,无需手动配置繁琐的工具即可使用。

    4. 关注业务,而不是工具配置。

  2. 初始化项目

    初始化命令:

    npx create-react-app my-app
    或
    npm init react-app my-app
    或
    yarn create react-app my-app
    

    进入项目目录:

    cd my-app
    

    启动项目:

    npm start
    

二、JSX

1. JSX 的基本使用

从 createElement() 变成 JSX

React.createElement(
	'div',
    {className: 'shopping-list'},
    React.createElement('h1', null, 'Shopping List'),
    React.createElement( 
        'ul',
        null,
        React.createElement('li', null, 'Instagram'),
    	React.createElement('li', null, 'WhatsApp')
	)
)
<div className="shopping-list">
	<h1>Shopping List</h1>
	<ul>
		<li>Instagram</li>
		<li>WhatsApp</li>
	</ul>
</div>

JSX 是 react 的核心内容。

使用步骤:

  1. 使用 JSX 语法创建 react 元素:

    const title = <h1>Hello JSX</h1>
    
  2. 使用 ReactDOM.render() 方法渲染 React 元素到页面上:

    ReactDOM.render(title, root)
    

注意

JSX 不是标准的 ECMAScript 语法,但是在脚手架中有 babel 进行编译。所以才能在浏览器中使用。

React 元素的属性名使用驼峰命名法

class => className

没有子节点的 React 元素可以用 /> 结束

推荐:使用小括号包裹 JSX,从而避免 JS 中的自动插入分号陷阱

2. JSX 中使用 JavaScirpt 表达式

嵌入 JS 表达式

const name = 'jack'
const dv = (
	<div>你好,我叫:{name}</div>
)

注意

单大括号中可以使用任意的 JavaScript 表达式

JSX 自身也是 JS 表达式

不能在 {} 中出现语句(例如:if / for 等)

3. JSX 的条件渲染

使用if / else 或三元运算符或逻辑运算符来实现

const loadData = () => {
	if (isLoading) {
		return <div>数据加载中,请稍后...</div>
	}
	return (
		<div>数据加载完成,此处显示加载后的数据</div>
	) 
}
const dv = (
	<div>
		{loadData()}
	</div>
)

4. JSX 的列表渲染

如果这里是要渲染一组数据的话,可以利用数组的 map() 方法

const songs = [
	{id: 1, name: '痴心绝对'},
	{id: 2, name: '像我这样的人'},
	{id: 3, name: '南山南'},
]
const list = (
	<ul>{songs.map(item => <li key={item.id}>{item.name}</li>)}
	</ul> 
)

注意

要个每个渲染的数据加一个 key 属性

key 属性的值要保证唯一

原则:map() 遍历谁,就给谁添加 key 属性

尽量避免使用索引作为 key 可以使用 id 值

5. JSX 的样式处理

  1. 行内样式 —— style

    <h1 style={{ color: 'red', backgroundColor: 'skyblue' }}>
    	JSX的样式处理
    </h1>
    
  2. 类名 —— className(推荐)

    <h1 className="title">
    	JSX的样式处理
    </h1>
    

三、React 组件基础

1. React 组件介绍

组件是 React 的一等公民,使用 React 就是在使用组件。

2. React 组件的两种创建方式

  1. 使用函数创建组件

    函数组件:使用 JS 的函数(或箭头函数)创建的组件

    约定1:函数名称必须以大写字母开头

    约定2:函数组件必须有返回值,表示该组件的结构

    如果没有返回值,可以写返回值为 null 表示不渲染任何内容。

    function Hello(){
        return (
        	<div>这是我的第一个函数组件</div>
        )
    }
    
    const Hello = () => {
        return (
        	<div>这是我的第一个函数组件</div>
        )
    }
    
  2. 使用类创建组件

    类组件:使用 ES6 的 class 创建的组件

    约定1:类名称必须以大写字母开头

    约定2:类组件应该继承 ReactComponent 父类,从而可以使用父类中提供发方法或属性

    约定3:类租金啊必须提供 render() 方法

    约定4:render() 方法必须有返回值,表示该组件的结构

    class Hello extends React.Component {
        render(){
            return <div>Hello class component!</div>
        }
    }
    
    ReactDOM.render(<Hello />, root)
    
  3. 抽离为单独 JS 文件

项目多了之后,不同的组件要进行抽离。组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中。

3. React 事件处理

  1. 事件绑定

    注意

    React 事件绑定语法与 DOM 事件语法相似

    语法:on + 事件名称 = {事件处理程序},比如:onClick={() => {}}

    React 事件采用驼峰命名法

    在组件中绑定事件:

    function App(){
        function handleClick(){
            console.log('单击事件触发了')
        }
        
        return (
        	<button onClick={handleClick}>点我</button>
        )
    }
    
    
    class App extends React.Component{
        handleClick(){
            console.log('单击事件触发了')
        }
        render(){
            return (
            	<button onClick={handleClick}>点我</buttn>
            )
    	}
    }
    
  2. 事件对象

    可以通过事件处理程序的参数获取到事件对象

    React 中的事件对象叫做:合成事件(对象)

    合成事件:兼容所有浏览器,无须担心跨浏览器兼容性问题

4. 有状态组件和无状态组件

简单来说就是。函数组件是无状态组件。类组件时有状态组件。这个状态是什么呢。就是数据。也就是 state。

5. 组件中的 state 和 setState

  1. state 的基本使用

    state 状态就是数组,这个数据是私有的。只能在组件内部才能使用

    state 的值是对象,表示一个组件中可以有多个数据

    两种初始化 state 的方法

    // 利用类的 constructor
    constructor(){
        super()
        this.state = {
            count: 0
        }
    }
    
    // 或者简化语法直接初始化
    state = {
        count: 0
    }
    

    获取状态也就是获取数据:this.state

    class Hello extends React.Component {
    	// 简化语法
    	state= {
    		count: 0
    	}
    	render() {
    		return (
    		<div>有状态组件,{this.state.count}</div>
    		) 
    	} 
    }
    
  2. setState() 修改状态

    状态是可变的

    语法:this.state({要修改的数据})

    不能直接去修改state

    setState() 作用:1.修改 state 2.更新UI

    // 正确
    this.setState({
    	count: this.state.count + 1
    })
    // 错误
    this.state.count += 1
    

6. 事件绑定 this 指向

  1. 箭头函数

    利用箭头函数自身不绑定 this 的特点

    render() 方法中的 this 为组件实例,可以获取到 setState()

    class Hello extends React.Component {
    	onIncrement() {
    		this.setState({})
    	}
    	render() {
    		// 箭头函数中的this指向外部环境,此处为:render()方法
    		return (
    			<button onClick={() => this.onIncrement()}></button>
    		) 
    	} 
    }
    
  2. Function.prototype.bind()

    利用 ES5 中的 bind 方法,将事件处理程序中的 this 与组件实例绑定到一起

    class Hello extends React.Component {
    	constructor() {
    		super()
    		this.onIncrement = this.onIncrement.bind(this) 
        }
    	onIncrement() {
    		this.setState({})
    	}
    	render() {
    		return (
    			<button onClick={this.onIncrement}></button>
    		) 
    	} 
    }
    
  3. class 的实例方法

    利用箭头函数形式的 class 实例方法

    注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用

    class Hello extends React.Component {
    	onIncrement = () => {
    		this.setState({})
    	}
    	render() {
    		return (
    		<button onClick={this.onIncrement}></button>
    		) 
    	} 
    }
    

7. 表单处理

  1. 受控组件

    HTML 中的表单元素是可输入的,也就是有自己的可变状态

    而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改

    React将 state 与表单元素值value绑定到一起,由 state 的值来控制表单元素的值

    受控组件:其值受到 React 控制的表单元素

    步骤:

    • 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
    • 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
    state = {txt:''}
    <input type="text" value={this.state.txt} onClick={e => this.setState({txt:e.target.value})}></input>
    

    优化

    给表单元素添加一个 name 属性 名称与 state 相同

    这样就可以在多个表单的时候只写一个处理函数就好了

    // 根据表单元素类型获取值
    const value = target.type === 'checkbox' 
    	? target.checked
    	: target.value
    // 根据name设置对应state
    this.setState({
    	[name]: value
    })
    
    <input type="text" name="txt" value={this.state.txt} onChange={this.handleForm}
    />
    
  2. 非受控组件(DOM 方式)

    借助于 ref,使用原生 DOM 方式来获取表单元素值

    ref的作用:获取 DOM 或组件

    使用步骤:

    • 调用 React.createRef() 方法创建一个 ref 对象

    • 将创建好的 ref 对象添加到文本框中

    • 通过 ref 对象获取到文本框的值 current

      constructor() {
      	super()
      	this.txtRef = React.createRef()
      }
      
      <input type="text" ref={this.txtRef} />
      
          
      console.log(this.txtRef.current.value)
      

四、React 组件进阶

1. 组件通讯介绍

组件时独立且封闭的单元,默认情况下,只能使用组件中自己的数据。在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享一些数据。所以我们要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。

2. 组件的 props

组件是封闭的,要接收外部数据应该通过 props 来实现

props 的作用:接收传递给组件的数据

传递数据:给组件标签添加属性

接收数据:函数组件通过参数 props 接收数据,类组件通过 this.props 接收数据

function Hello (props){
    console.log(props)
    return (
    	<div>接收到数据: {props.name}</div>
    )
}


class Hello extends React.Component{
	render(){
        return (
    		<div>接收到数据: {this.props.name}</div>
    	)
    }    
}


<Hello name='jack' age={19} />

特点

  1. 可以给组件传递任意类型的数据

  2. props 是只读的对象,只能读取属性的值,无法修改对象

  3. 注意:使用类组件时,如果写了构造函数,应该将props 传递给 super(),否则,无法在构造函数中获取到 props

    class Hello extends React.Component {
    	constructor(props) {
    		super(props) 
        }
    	render() {
    		return <div>接收到的数据:{this.props.age}</div> 
        } 
    }
    

3. 组件通讯的三种方式

  1. 父组件 -> 子组件

    1. 父组件提供要传递的 state 数据
    2. 给子组件标签添加属性,值为 state 中的数据
    3. 子组件中通过 props 接收父组件中传递的数据
    class Parent extends React.Component {
    	state = { lastName: '王' }
    	render() {
    		return (
    			<div>
    				传递数据给子组件:<Child name={this.state.lastName} />
    			</div>
    		) 
    	} 
    }
    
    function Child(props){
        return <div>子组件接收到数据:{props.name}</div>
    }
    
  2. 子组件 -> 父组件

    思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。

    1. 父组件提供一个回调函数(用于接收数据)
    2. 将该函数作为属性的值,传递给子组件
    class Parent extends React.Component{
        getChildMsg = (msg) =>{
            console.log('接收到子组件数据', msg)
        }
        render(){
            return(
            	<div>
                	子组件:<Child getMsg={this.getChildMsg} />
                </div>
            )
        }
    }
    
  3. 兄弟组件

    将共享状态提升到最近的公共组件中,由共同的父组件管理这个状态

    思想:状态提升

    公共父组件职责:1.提供共享状态 2.提供操作共享状态的方法

    要通讯的子组件子需要通过 props 接收状态或操作状态的方法

    class Counter extends React.Component{
        // 提供共享状态
        state = {
    		count:0
        }
    
    	// 提供修改状态的方法
    	onIncrement = () => {
            this.setState({
                count: this.state.count +1
            })
        }
    
    	render(){
    		return (
            	<div>
                	<Child1 count={this.state.count} />
                	<Child2 onIncremnet={this.onIncrement} />
                </div>
            )
        }
    }
    
    const Child1 = (props) => {
        return <h1>计数器:{props.count}</h1>
    }
    
    const Child2 = (props) => {
        return <button onClick={props.onIncremnet()}>+1</button>
    }
    

4. Context

如果要跨越很多级子组件的时候进行数据传递怎么传递呢。

  • 使用 props 一层一层向下传递(过于繁琐)
  • 使用 Context

使用步骤

  1. 调用 React.createContext() 创建 Provider(提供数据)和 Consumer(消费数据)两个组件。

  2. 使用 Provider 组件作为父节点。

  3. 设置 value 属性,表示要传递的数据。

  4. 调用 Consumer 组件接收数据

    // 创建context得到两个组件
    const { Provider, Consumer } = React.createContext()
    
    class App extends React.Component {
      render() {
        return (
          <Provider value="pink">
            <div className="app">
              <Node />
            </div>
          </Provider>
        )
      }
    }
    
    const Node = props => {
      return (
        <div className="node">
          <SubNode />
        </div>
      )
    }
    
    const SubNode = props => {
      return (
        <div className="subnode">
          <Child />
        </div>
      )
    }
    
    const Child = props => {
      return (
        <div className="child">
          <Consumer>{data => <span>我是子节点 -- {data}</span>}</Consumer>
        </div>
      )
    }
    

5. props 深入

  1. children 属性

    children 属性:表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性

    children 属性与普通的 props 一样,值可以是任意值(文本、React 元素、组件,甚至是函数)

    function Hello(props) {
    	return (
    		<div>
    			组件的子节点:{props.children}
    		</div>
    	) 
    }
    <Hello>我是子节点</Hello>
    
  2. props 校验

    props 是从外边进入到组件内的,无法保证传进来的是什么格式的数据

    如果数据有问题,组件内部就会报错

    所以问题在于什么呢?问题在于组件的使用人。不知道自己明确的错误原因

    所以我们使用 props 校验,在创建组件的时候,就指定 props 的类型和格式等。

    作用:就是捕捉使用组件时候 props 导致的错误。给出明确的错误提示,增强组件的健壮性

    使用步骤

    1. 安装包 prop-types(npm i prop-types)

    2. 导入prop-types包

    3. 使用组件名.propTypes={} 来给组件的 props 添加校验规则

    4. 校验规则通过 PropTypes 对象来指定

      import PropTypes from 'prop-types'
      function App(props) {
      	return (
      		<h1>Hi, {props.colors}</h1>
      	) 
      }
      App.propTypes = {
      	// 约定colors属性为array类型
      	// 如果类型不对,则报出明确错误,便于分析错误原因
      	colors: PropTypes.array
      }
      

    约束规则

    1. 常见类型:array、bool、func、number、object、string

    2. React 元素类型:element

    3. 必填项:isRequired

    4. 特定结构的对象:shape({})

      // 常见类型
      optionalFunc: PropTypes.func,
      // 必选
      requiredFunc: PropTypes.func.isRequired,
      // 特定结构的对象
      optionalObjectWithShape: PropTypes.shape({
      	color: PropTypes.string,
      	fontSize: PropTypes.number
      })
      
  3. props 的默认值(defaultProps)

    在未传入 props 的时候生效。

    function App(props) {
    	return (
    		<div>
    			此处展示props的默认值:{props.pageSize}
    		</div>
    	) 
    }
    // 设置默认值
    App.defaultProps = {
    	pageSize: 10
    }
    // 不传入pageSize属性
    <App />
    

6. 组件的声明周期

组件是声明周期从创建到挂载再到卸载。

每一个阶段都有对应的声明周期的钩子函数。

只有 类组件 才有声明周期

生命周期的阶段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pu4UTVUG-1666060510360)(https://wanganyi-notes.oss-cn-beijing.aliyuncs.com/img/生命周期.png)]

  1. 创建时(挂载阶段)

    执行时机:组件创建时(页面加载时)

    执行顺序:constructor() --> render() --> componentDidMount

    image-20221011161804561

  2. 更新时(更新阶段)

    执行时机:1.setState() 2.forceUpdate() 3.组件接收到新的 props

    说明:以上三者任意一种变化,组件就会重新渲染

    执行顺序:render() --> componentDidUpdate()

    image-20221011162025557

  3. 卸载时(卸载阶段)

    执行时机:组件从页面消失

    常用于:清除定时器。清理手动加载的监听事件。

    image-20221011162126980

7. render-props

render-props 模式

使用步骤

  1. 创建 Mouse 组件,在组件中提供复用的状态逻辑代码

  2. 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部

  3. 使用 props.render() 的返回值作为要渲染的内容

    // 创建 Mouse 组件
    class Mouse extends React.Component{
        state = {
            x:0,
            y:0
        }
    	
    	// 鼠标移动事件的事件处理函数
    	handleMouseMove = (e) => {
            this.setState({
                x: e.clientX,
                y: e.clientY
            })
        }
    	// 监听鼠标移动事件
    	componentDidMount(){
            window.addEventListener('mousemove', this.handleMouseMove)
        }
    	render(){
            return this.props.render(this.state)
        }
    }
    
    class App extends React.Component{
        render(){
            return (
            	<div>
                	<h1>render props 模式</h1>
               		<Mouse render={(mouse) => {return (<p>鼠标位置:{mouse.x} {mouse.y}</p>)}} />
                </div>
            )
        }
    }
    

chidren 代替 render 属性

注意:并不是该模式叫 render props 就必须使用名为 render 的 prop,实际上可以使用任意名称的 prop

<Mouse>
	{({x, y}) => <p>鼠标的位置是 {x}{y}</p> }
</Mouse>
// 组件内部:
this.props.children(this.state)

8. 高阶组件

概述

  • 目的:实现状态逻辑复用
  • 采用包装(修饰)模式

思路分析

  • 高阶组件(HOC、Higher-Order Component) 是一个函数,接收要包装的组件,返回增强后的组件

  • 高阶组件内部创建了一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给被包装组件WrappedComponent

    const EnhancedComponent = withHOC(WrappedComponent)
    
    class Mouse extends React.Component{
        render(){
            return <WrappedComponent {...this.state} />
        }
    }
    

使用步骤

  • 创建一个函数,名称约定以with开头

  • 指定函数参数,参数应该以大写字母开头

  • 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回

  • 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件

  • 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面

    包装函数

    // 定义一个函数,在函数内部创建一个相应类组件
    function withMouse(WrappedComponent) {
        // 该组件提供复用状态逻辑
        class Mouse extends React.Component {
            state = {
                x: 0,
                y: 0
            }
            // 事件的处理函数
            handleMouseMove = (e) => {
                this.setState({
                    x: e.clientX,
                    y: e.clientY
                })
            }
            // 当组件挂载的时候进行事件绑定
            componentDidMount() {
                window.addEventListener('mousemove', this.handleMouseMove)
            }
            // 当组件移除时候解绑事件
            componentWillUnmount() {
                window.removeEventListener('mousemove', this.handleMouseMove)
            }
            render() {
                // 在render函数里面返回传递过来的组件,把当前组件的状态设置进去
                return <WrappedComponent {...this.state} />
            }
        }
        return Mouse
    }
    

    哪个组件需要加强,通过调用withMouse这个函数,然后把返回的值设置到父组件中即可

    function Position(props) {
        return (
            <p>
                X:{props.x}
                Y:{props.y}
            </p>
        )
    }
    // 把position 组件来进行包装
    let MousePosition = withMouse(Position)
    
    class App extends React.Component {
        constructor(props) {
            super(props)
        }
        render() {
            return (
                <div>
                    高阶组件
                    <MousePosition></MousePosition>
                </div>
            )
        }
    }
    

设置displayName

  • 使用高阶组件存在的问题:得到的两个组件名称相同

  • 原因:默认情况下,React使用组件名称作为 displayName

  • 解决方式:为 高阶组件 设置 displayName 便于调试时区分不同的组件

  • displayName的作用:用于设置调试信息(React Developer Tools信息)

  • 设置方式:

    Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
    function getDisplayName(WrappedComponent) {
    	return WrappedComponent.displayName || WrappedComponent.name || 'Component' 
    }
    

传递props

  • 问题:props丢失

  • 原因:高阶组件没有往下传递props

  • 解决方式:渲染 WrappedComponent 时,将 state 和 this.props 一起传递给组件

    <WrappedComponent {...this.state} {...this.props} />
    

五、 React 原理

1. setState() 的说明

  1. 更新数据

    setState() 是异步更新数据的

    注意:使用该语法的时候,后面的 setState() 不要依赖前面的 setState()

    可以多次调用 setState(),只会触发一次重新渲染

    this.state = {
        count:0
    }
    
    this.setState({
        count:this.state.count +1
    })
    console.log(this.state.count) // 0
    
    this.setState({
        count:this.state.count +1
    })
    console.log(this.state.count) // 0
    
    // state.count 最终还是,虽然加了两次
    
  2. 推荐语法

    既然存在上面的问题。所以推荐:

    使用 setState((state, props) => {}) 语法

    参数 state:表示最新的 state

    参数 props:表示最新的 props

    this.state = {
        count:0
    }
    
    this.setState((state, props) => {
        retrun{
        	count:this.state.count +1
        },
        () => {
            console.log('这个回调函数会在状态更新后并且重新渲染后立即执行' + this.state.count) // 此时这时候打印的count就是1了
        }
    })
    console.log(this.state.count) // 0
    
    this.setState((state, props) => {
        retrun{
        	count:this.state.count +1
        }
    })
    console.log(this.state.count) // 0
    
    // state.count 最后变成了 2
    

    注意:这样写还是异步的

2. JSX 语法的转化过程

其实 JSX 语法就是 createElement() 方法的语法糖

我们书写的 JSX 语法最终会被 @babel/preset-react 插件编译为 createElement() 方法

React 元素:是一个对象,用来描述你希望在屏幕上看到的内容

image-20221013105305542

3. 组件更新机制

我们学 setState() 的时候,说它有两个作用:1.修改 state 2.更新组件(UI)

这个过程是什么样子的呢?

​ 父组件重新渲染时,也会重新渲染子组件。但只会渲染当前组件子树(当前组件及其所有子组件)

image-20221013105558560

4. 组件性能优化

  1. 减轻 state

    在初始化 state 的时候,里面只储存与组件渲染相关的数据

    注意:不用做渲染的数据不要放在 state 中

    对于在多个方法中要用到的数据,应该放在 this 中

    class Hello extends Component {
        constructor() {
            super()
            state = {
                count: 0
            }
        }
    	componentDidMount() {
    		// timerId存储到this中,而不是state中
    		this.timerId = setInterval(() => {}, 2000)
    	}
    	componentWillUnmount() {
    		clearInterval(this.timerId)
    	}
    	render() { … }
    }
    
  2. 避免不必要的重新渲染

    父组件更新会引起子组件更新,这种思路是很清晰的。

    但是一个父组件的更新导致子组件更新,这个子组件里面的 UI 和 状态没有改变。我们还将它更新了。这是我们不想做的。

    解决方法:使用钩子函数 shouldComponentUpdate(nextProps, nextState)

    这个函数可以通过返回值判断是否要重新渲染,返回 true 表示重新渲染,false 表示不重新渲染

    触发时机:更新阶段的钩子函数,组件重新渲染之前执行(shouldComponentUpdate -> render)

    class Hello extends Component {
    	shouldComponentUpdate() {
    		// 根据条件,决定是否重新渲染组件
    		return false
    	}
    	render() {…}
    }
    

    例子:

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    /* 
      组件性能优化:
    */
    
    // 生成随机数
    class App extends React.Component {
      state = {
        number: 0
      }
    
      handleClick = () => {
        this.setState(() => {
          return {
            number: Math.floor(Math.random() * 3)
          }
        })
      }
    
      // 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
      shouldComponentUpdate(nextProps, nextState) {
        console.log('最新状态:', nextState, ', 当前状态:', this.state)
    
        return nextState.number !== this.state.number
    
        // if (nextState.number !== this.state.number) {
        //   return true
        // }
        // return false
    
        // if (nextState.number === this.state.number) {
        //   return false
        // }
        // return true
      }
    
      render() {
        console.log('render')
        return (
          <div>
            <h1>随机数:{this.state.number}</h1>
            <button onClick={this.handleClick}>重新生成</button>
          </div>
        )
      }
    }
    
    ReactDOM.render(<App />, document.getElementById('root'))
    
    import React from 'react'
    import ReactDOM from 'react-dom'
    
    /* 
      组件性能优化:
    */
    
    // 生成随机数
    class App extends React.Component {
      state = {
        number: 0
      }
    
      handleClick = () => {
        this.setState(() => {
          return {
            number: Math.floor(Math.random() * 3)
          }
        })
      }
    
      // 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
      // shouldComponentUpdate(nextProps, nextState) {
      //   console.log('最新状态:', nextState, ', 当前状态:', this.state)
      //   return nextState.number !== this.state.number
      // }
    
      render() {
        // console.log('render')
        return (
          <div>
            <NumberBox number={this.state.number} />
            <button onClick={this.handleClick}>重新生成</button>
          </div>
        )
      }
    }
    
    class NumberBox extends React.Component {
      shouldComponentUpdate(nextProps) {
        console.log('最新props:', nextProps, ', 当前props:', this.props)
        // 如果前后两次的number值相同,就返回false,不更新组件
        return nextProps.number !== this.props.number
    
        // if (nextProps.number === this.props.number) {
        //   return false
        // }
        // return true
      }
      render() {
        console.log('子组件中的render')
        return <h1>随机数:{this.props.number}</h1>
      }
    }
    
    ReactDOM.render(<App />, document.getElementById('root'))
    
  3. 纯组件

    纯组件:PureComponent 与 React.Component 形似

    区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较

    原理:纯组件内部通过分别对比 前后两次的 props 和 state 的值,来决定是否重新渲染组件

    class Hello extends React.PureComponent{
        render(){
            return (
            	<div>纯组件</div>
            )
        }
    }
    

    来说说这个比较:

    纯组件内部的对比是 shallow compare(浅层对比)

    对于值的类型来说,这个比较就是在比较两个值是否相同,这样是没有坑的。

    let number = 0
    let newNumber = number
    newNumber = 2
    console.log(number === newNumber) // false
    
    state = { number: 0 }
    setState({
    	number: Math.floor(Math.random() * 3)
    })
    // PureComponent内部对比:最新的state.number === 上一次的state.number 
    // false,重新渲染组件
    

    对于引用类型来说:只比较对象的引用(地址)是否相同

    const obj = { number: 0 }
    const newObj = obj
    newObj.number = 2
    console.log(newObj === obj) // true
    
    state = { obj: { number: 0 } }
    // 错误做法
    state.obj.number = 2
    setState({ obj: state.obj })
    // PureComponent内部比较:最新的state.obj === 上一次的state.obj 
    // true,不重新渲染组件
    

    例子:

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    /* 
      组件性能优化:
    */
    
    // 引用类型:
    const obj = { number: 0 }
    const newObj = obj
    newObj.number = 2
    console.log(newObj === obj) // true
    
    // 生成随机数
    class App extends React.PureComponent {
      state = {
        obj: {
          number: 0
        }
      }
    
      handleClick = () => {
        // 正确做法:创建新对象
        const newObj = { ...this.state.obj, number: Math.floor(Math.random() * 3) }
        this.setState(() => {
          return {
            obj: newObj
          }
        })
    
        // 错误演示:直接修改原始对象中属性的值
        /* const newObj = this.state.obj
        newObj.number = Math.floor(Math.random() * 3)
    
        this.setState(() => {
          return {
            obj: newObj
          }
        }) */
      }
    
      render() {
        console.log('父组件重新render')
        return (
          <div>
            <h1>随机数:{this.state.obj.number}</h1>
            <button onClick={this.handleClick}>重新生成</button>
          </div>
        )
      }
    }
    
    ReactDOM.render(<App />, document.getElementById('root'))
    
    // 数组
    // 正确!创建新数据
    const newObj = {...state.obj, number: 2}
    setState({ obj: newObj })
    // 正确!创建新数据
    // 不要用数组的push / unshift 等直接修改当前数组的的方法
    // 而应该用 concat 或 slice 等这些返回新数组的方法
    this.setState({
    	list: [...this.state.list, {新数据}]
    })
    

    注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!

5. 虚拟 DOM 和 Diff 算法

组件中只有一个 DOM 元素需要更新的时候,也要把整个组件的内容都重新渲染嘛?

答案显然是不是的。我们想要的就是,部分更新,只更新变化的地方

虚拟 DOM:本质上就是一个 JS 对象。就是在描述。我们要写的内容(UI)

image-20221013121320301

这个过程是怎么执行的呢?

  1. 初次渲染时候,React 会根据初始 state(Model),创建一个虚拟 DOM 对象(树)

  2. 根据虚拟 DOM 生成真正的 DOM,渲染到页面中

  3. 当数据变化后(setState(),重新根据新的数据,创建新的虚拟 DOM 对象(树)。

  4. 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。

  5. 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0GZxMb1u-1666060510366)(C:/Users/20946/AppData/Roaming/Typora/typora-user-images/image-20221013121653467.png)]

6. React 路由基础

  1. React 路由介绍

    前端应用大多都是 SPA(单页面应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体验更好、对服务器的压力更小。为了有效的使用单个页面来管理更多页面的功能,前端路由应然而生。

    • 前端路由的功能:让用户从一个视图(页面)导航到另一个视图(页面)
    • 前端路由是一套映射规则,在 React 中,是 URL 路径与组件的对应关系
    • 使用 React 路由简单来说,就是配置路径和组件(配对)
  2. 路由的基本使用

    使用步骤

    1. 安装:npm i react-router-dom

    2. 导入路由的三个核心组件:Router / Route / Link

      import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
      
    3. 使用 Router 组件包裹整个应用(重要)

      <Router>
      	<div className="App">
      		// … 省略页面内容
      	</div>
      </Router>
      
    4. 使用 Link 组件作为导航栏菜单(路由入口)

      <Link to="/first">页面一</Link>
      
    5. 使用 Route 组件配置路由规则和要展示的组件(路由出口)

      const First = () => <p>页面一的页面内容</p>
      <Router>
      	<div className="App">
      		<Link to="/first">页面一</Link>
      		<Route path="/first" component={First}></Route>
      	</div>
      </Router>
      

    Router 组件:只需要使用一次

    两种常用的 Router :HashRouter 和 BrowseRouter

    HashRouter:使用 URL 的哈希值实现(localhost:3000/#/first)

    (推荐)BrowseRouter:使用 H5 的 history API 实现(localhost:3000/first)

    Link组件:用于指定导航链接

    // to属性:浏览器地址栏中的pathname(location.pathname)
    <Link to="/first">页面一</Link>
    

    Route 组件:指定路由展示组件相关信息

    // path属性:路由规则
    // component属性:展示的组件
    // Route组件写在哪,渲染出来的组件就展示在哪
    <Route path="/first" component={First}></Route>
    
  3. 路由的执行过程

    1. 点击 Link 组件(a标签),修改了浏览器地址栏中的 url。

    2. React 路由监听到地址栏 url 的变化。

    3. React 路由内部遍历所有 Route 组件,使用路由规则(path )与 pathname 进行匹配。

    4. 当路由规则(path)能够匹配地址栏中的 pathname时,就展示该 Route 组件的内容。

  4. 编程式导航

    场景:点击登录按钮,登录成功后,通过代码跳转到后台首页,如何实现?

    编程式导航:通过 JS 代码来实现页面跳转

    history 是 React 路由提供的,用于获取浏览器历史记录的相关信息

    push(path):跳转到某个页面,参数 path 表示要跳转的路径

    go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)

    class Login extends Component {
    	handleLogin = () => {
    		// ...
    		this.props.history.push('/home')
    	}
    	render() {...省略其他代码}
    }
    
  5. 默认路由

    问题:现在的路由都是点击导航菜单后展示的,如何在进入页面的时候就展示呢?

    默认路由:表示进入页面时就会匹配的路由

    默认路由path为:/

    <Route path="/" component={Home} />
    
  6. 匹配模式

    1. 模糊匹配模式

      问题:当 Link组件的 to 属性值为 “/login”时,为什么 默认路由(/) 也被匹配成功?

      默认情况下,React 路由是模糊匹配模式

      模糊匹配规则:只要 pathname 以 path 开头就会匹配成功

      image-20221018103351612

    2. 精确匹配

      问题:默认路由任何情况下都会展示,如何避免这种问题?

      给 Route 组件添加 exact 属性,让其变为精确匹配模式

      精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由

      image-20221018103442847

      推荐:给默认路由添加 exact 属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值