React基础

React理论基础——基于函数组件

一、修改状态

1. useState

作用:1. 修改state;2. 更新UI

思想:数据驱动视图

建议:从JSX抽离逻辑代码,注意类组件的this指向

二、表单处理

1. 受控组件

state = {txt: ''};
<input type='text' value={this.state.txt} onChange={e => this.setState({txt: e.target.value})} />;
state = {isChecked: false};
<input type='checkbox' checked={this.state.isChecked} onChange={e => this.useState({isChecked: e.target.checked})} />

2. 多表单元素优化

步骤:

  1. 给表单元素添加name属性,名称与state相同
  2. 根据表单元素类型获取对应值
  3. change事件处理程序中通过[name]来修改对应的state
const handleForm = (e) => {
  const target = e.target;
  // 根据表单元素类型获取值
  const value = target.type === 'checkbox' ? target.checked : target.value;
  // 根据name设置对应的state
  this.setState({[target.name]: value})
}
<input type='text' name='txt' value={this.state.txt} onChange={this.handleForm.bind(this, e)} />

3. 非受控组件

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

使用步骤:

  1. 调用React.createRef()创建一个ref对象
  2. 将创建好的ref对象添加到文本框中
  3. 通过ref获取文本框的值
txtRef = React.createRef();
<input type='text' ref={this.txtRef} />;
console.log(this.txtRef.current.value);

三、组件间通讯

传递数据:给组件传递属性

接收数据:props

props的特点:

  1. 可以给组件传递任意类型的数据;
  2. props是只读属性,不能修改

1. 父到子

a. 父组件提供要传递的state数据

b. 父组件给子组件传递属性,值为state中的数据

c. 子组件通过props接受通过属性传过来的值

2. 子到父

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

a. 父组件提供一个回调函数(用于接收数据)

b. 将该函数作为属性值传递给子组件,子组件传入实参(子组件需要传递的数据)调用该函数

3. 兄弟间

思想:状态提升

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

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

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

4. Context跨组件传递数据

使用步骤:

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

    cont {Provider, Consumer} = React.createContext();
    
  2. 使用Provider组件作为父节点

    <Provider>
    	<div className='App'>
      	<Child />
      </div>
    </Provider>
    
  3. 设置value属性,表示要传递的数据

    <Provider value='pink' />
    
  4. 调用Consumer组件接收数据

    <Consumer>
    	{data => <span>data参数表示接收到的数据 -- {data}</span>}
    </Consumer>
    

四、props深入

1. children属性

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

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

2. props校验

对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,如果传入的数据格式不对,可能会导致组件内部报错。

关键问题:组件的使用者不知道明确的错误原因

props校验:允许在创建组件的时候,就指定props的类型、格式等

作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性

使用步骤:

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

  2. 导入prop-types

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

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

  5. 校验规则通过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

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

    PropTypes.shape({
    	color: PropTypes.string,
    	fontSize: PropTypes.number
    })
    

3. props的默认值

props设置默认值,未传入props时生效

场景:显示分页下每页的条数

const App = props => {
	return (
		<div>
    	<h1>此处展示props的默认值:{props.pageSize}</h1>
    </div>
	)
}
App.defaultProps = {
  pageSize = 10
}

五、组件的生命周期

1. 生命周期的三个阶段(针对class组件)

创建时(挂载阶段)

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

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

    钩子函数 触发时机 作用
    constructor 创建组件时,最先触发 1. 初始化state
    2. 为事件处理程序绑定this
    render 每次组件渲染都会触发 渲染UI(注意:不能调用setState(),会导致递归渲染报错)
    componentDidMount 组件挂载(完成DOM渲染)后 1. 发送网络请求
    2. DOM操作

更新时(更新阶段)

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

    钩子函数 触发时机 作用
    render 每次组件渲染都会触发 渲染UI(与挂载阶段是同一个render
    componentDidUpdate(更新前的值) 组件更新(完成DOM渲染)后 1. 发送网络请求
    2. DOM操作
    注意:如果要setState()必须放在一个if条件中

    注意:setState会导致render,出现递归更新,故需要放在if条件中if (比较更新前后的props是否相同) {执行逻辑}

  • 导致组件重新渲染的三种情况:1. setState() 2. forceUpdate() 3. 组件接收到新的props

卸载时(卸载阶段)

  • 执行时机:组件从页面消失
  • componentWillUnmount,组件卸载(从页面消失时)触发,执行清理工作(比如:清理定时器等)

六、React组件复用

1. render props模式

思路分析:将要复用的state和操作state的方法封装到一个组件中

通过向复用组件传递一个函数实现,形参用来放state中的变量,返回值为需要渲染的UI

只实现了状态逻辑代码的复用,没有实现UI结构的复用

例如:创建一个复用鼠标位置的组件(复用子组件,相当于通过子给父传递数据)

function Son(props) {
	const [x, setX] = useState(0);
	const [y, setY] = useState(0);
	const handleMouseover = e => {
		setX(e.clientX);
		setY(e.clientY);
	};
  useEffect(() => {
		window.addEventListener('mousemove', handleMouseover);
		return () => {
			// 组件卸载时移除事件监听对象
			window.removeEventListener('mousemove', handleMouseover);
		};
	}, []);
	return <div>{props.children({ x, y })}</div>;
}

import propTypes from 'prop-types';
// 添加props校验
Son.propTypes = {
  children: PropTypes.func.isRequireds
}

function Father() {
	return (
		<Son>
    	{mouse => (
				<p>
					当前鼠标的位置为:x-{mouse.x}, y-{mouse.y}
				</p>
			)}
    </Son>
	);
}

2. 高阶组件(HOC

目的:实现状态逻辑复用

采用包装(装饰)模式,通过包装组件,增强组件功能

思路:

  • Higher-Order Component是一个函数,名称约定以with开头,接收要包装的组件,返回增强后的组件

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

  • const EnhancedComponent = withHOC(WrappedComponent)

使用步骤:

  1. 创建一个函数,名称约定以with开头
  2. 指定函数形参,参数应该以大写字母开头
  3. 在函数内部创建一个组件,提供复用的状态逻辑代码,并返回
  4. 在该组件中,渲染参数组件,同时将状态通过porps传递给参数组件
import React, { useState, useEffect } from 'react'
import reactDOM from 'react-dom'

/* 创建高阶组件 */
const withMouse = WrappedComponent => {
	const Mouse = props => {
		const [x, setX] = useState(0)
		const [y, setY] = useState(0)
		const handleMouseMove = e => {
			setX(e.clientX)
			setY(e.clientY)
		}
		useEffect(() => {
			window.addEventListener('mousemove', handleMouseMove)
			return () => {
				window.removeEventListener('mousemove', handleMouseMove)
			}
		})
		return <WrappedComponent {...{ x, y }} {...props} />
	}
	/* 给被高阶组件渲染出来的组件命名displayname */
	const getDisplayName = WrappedComponent => {
		return WrappedComponent.displayName || WrappedComponent.name || 'component'
	}
	Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
	return Mouse
}
/* 使用高阶组件 */
const Position = props => {
	const { x, y, a } = props
	return (
		<div>
			<h3>a={a}</h3>
			<p>
				当前鼠标的位置为 x: {x}, y: {y}
			</p>
		</div>
	)
}
const MousePosition = withMouse(Position)
reactDOM.render(<MousePosition a="1" />, document.getElementById('root'))

问题:使用高阶组件后会得到名称相同的组件,比如本例中,通过高阶组件渲染出来的都为<Mouse />

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

解决:设置displayName,便于调试时区分不同的组件,用于设置调试信息(React Developer Tools信息)

const getDisplayName = WrappedComponent => {
	return WrappedComponent.displayName || WrappedComponent.name || 'component'
}
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`

问题:props丢失

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

解决:渲染WrappedComponent时,将props也一起传给被包裹的组件

七、React原理

1. setState的说明

setState是异步更新数据的

注意:使用该语法时,后面的setState()不要依赖于前面的setState(),因为两次拿到的值都是变化前的值

每个操作可以多次调用setState,只会触发一次页面重新渲染

错误示范:

const [count, setCount] = useState(1)
const handleClick = () = > {
  setCount(count + 1) // 异步完成之后count变为2
  console.log(count) // 1
  setCount(count + 1) // 异步完成之后count变为2
}

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。setState(state => {})

const [count, setCount] = useState(1)
const handleClick = () = > {
  setCount(count => {
		return count + 1
	})
	setCount(count => {
		console.log('拿到最新的count: ', count) // 2
		return count + 1
	})
	console.log('count: ', count)
}

2. JSX语法的转化过程

JSX仅仅是createElement()方法的语法糖

JSX语法被@babel/preset-react插件编译为createElement()方法

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

const element = <h1 className="greeting">Hello JSX!</h1>
// 等价于
const element = React.createElement('h1', {className: 'greeting'}, 'Hello JSX!')

3. 组件更新机制

setState()的两个作用:1. 修改state,2. 更新组件(UI

过程:父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件,函数组件useEffetct在不含依赖的情况下执行顺序为子到父)

4. 组件性能优化

  1. 减轻state:只存储跟组件渲染相关的数据
  2. 避免不必要的重新渲染,父组件更新会引起子组件更新(导致子组件没有变化时也会重新渲染),函数组件useEffect添加依赖可以减少不必要的渲染

5. 虚拟DOMDiff算法

React更新视图的思想是:只要state变化就重新渲染视图

虚拟DOM配合Diff算法实现部分更新

虚拟DOM本质上就是一个JS对象,用来描述希望在页面上看到的内容

执行过程:

  1. 初次渲染时,React会根据初试state(Model)(状态和JSX),创建一个虚拟DOM对象(树)
  2. 根据虚拟DOM生成真正的DOM,渲染到页面中
  3. 当数据变化后,重新根据新的数据,创建新的虚拟DOM对象(树)
  4. 与上一次得到的虚拟DOM对象,使用Diff算法对比,得到需要更新的内容
  5. 最终,React只将变化的内容更新(patch)到DOM中,重新渲染到页面

八、React路由基础

1. React路由介绍

SPA单页面应用,带来的用户体验更好,对服务器压力更小,为了有效的使用单个页面来管理原来多页面的功能,前端路由应运而生。

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

2. 路由的基本使用 (6.x)

使用步骤:react-router-dom@6.0.2

  1. 安装:yarn add react-router-dom

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

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

  4. 使用Link组件作为导航菜单(入口)

  5. 使用Route组件配置路由规则和要展示的组件(出口)

import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'

const First = () => <p>第一个页面的内容</p>

const App = () => {
	return (
		<Router>
			<div>
				<h1>React路由基础</h1>
				{/* 路由入口 */}
				<Link to="/first">页面一</Link>
				{/* 路由出口 */}
				<Routes>
					<Route path="/first" element={<First />} />
				</Routes>
			</div>
		</Router>
	)
}

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

3. 路由的执行过程

  1. 点击Link组件(a标签),修改了浏览器地址栏中的URL
  2. React路由监听到地址栏URL的变化
  3. React路由内部遍历所有Route组件,使用路由规则(path)与element进行匹配
  4. 当路由规则(path)能够匹配地址栏中的pathname时,就展示该Route组件的内容

4. 编程式导航

props.history.push('/')

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

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

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

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

5. 默认路由

默认路由:表示进入页面就会匹配的路由,即<Route path="/" element={<Home />} />

6. 匹配模式

模糊匹配模式(默认):只要pathnamepath开头就会被匹配,比如/login/a可以匹配出/ /log

精确匹配:Route添加exact属性即可

posted @ 2021-12-06 10:27  carla-cn  阅读(49)  评论(0)    收藏  举报