脚手架
传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构
认识
对于现在比较流行的三大框架都有属于自己的脚手架:作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好
-
Vue的脚手架:@vue/cli -
Angular的脚手架:@angular/cli -
React的脚手架:create-react-app
目前这些脚手架都是使用node编写的,并且都是基于webpack的
创建React项目
-
先安装脚手架依赖
node,无论是windows还是Mac OS,都可以通过node官网nodejs.org/en/download… 直接下载 -
create-react-app 项目名称,项目名称不能包含大写字母 -
创建完后
cd 项目,然后npm start运行项目 -
目录结构如下:
webpack
React脚手架默认是基于Webpack来开发的,但是并没有在目录结构中看到任何webpack相关的内容?原因是React脚手架将webpack相关的配置隐藏起来了(从Vue CLI3开始,也进行了隐藏),
-
如果希望看到
webpack的配置信息,应该怎么来做呢? -
可以使用
npm run eject命令执行一个package.json文件中的一个脚本:"eject": "react-scripts eject" -
这个命令执行是不可逆的,执行过程中会给提示,后面会学习其他修改
webpack配置的方法 -
删除不需要的文件,将
src下的所有文件都删除,将public文件下除favicon.ico和index.html之外的文件都删除掉 -
再在
src目录下,创建一个index.js文件,因为这是webpack打包的入口,就可以在index.js中开始编写React代码
组件化
组件化是一种分而治之的思想,组件化是React的核心思想,组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用,任何的应用都会被抽象成一颗组件树
-
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
-
但如果将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
React的组件化
React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件
-
根据组件的定义方式,可以分为:函数组件(
Functional Component)和类组件(Class Component) -
根据组件内部是否有状态需要维护,可以分成:无状态组件(
Stateless Component)和有状态组件(Stateful Component) -
根据组件的不同职责,可以分成:展示型组件(
Presentational Component)和容器型组件(Container Component)
这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离
-
函数组件、无状态组件、展示型组件主要关注
UI的展示 -
类组件、有状态组件、容器型组件主要关注数据逻辑
-
当然还有很多组件的其他概念:比如异步组件、高阶组件等
类组件
-
类组件的定义有如下要求:
-
组件的名称是大写字符开头(无论类组件还是函数组件)
-
类组件需要继承自
React.Component -
类组件必须实现
render函数
-
-
使用
class定义一个组件:-
constructor是可选的,通常在constructor中初始化一些数据 -
this.state中维护的就是组件内部的数据 -
render()方法是class组件中唯一必须实现的方法
-
-
render函数的返回值,当render被调用时,它会检查this.props和this.state的变化并返回以下类型之一-
React元素 通常通过JSX创建,例如<div />会被React渲染为DOM节点,<MyComponent />会被React渲染为自定义组件,<div />和<MyComponent />均为React元素 -
数组或
fragments:使得render方法可以返回多个元素 -
Portals:可以渲染子节点到不同的DOM子树中 -
字符串或数值类型:它们在
DOM中会被渲染为文本节点 -
布尔类型或
null:什么都不渲染
-
import React, { Component } from 'react'
export class ComClass extends Component {
render() {
return (
<div>类组件</div>
)
}
}
export default ComClass
函数组件
函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容
- 函数组件有自己的特点,后面学习
Hooks时,会针对函数式组件进行更多的学习-
没有生命周期,也会被更新并挂载,但是没有生命周期函数
-
this关键字不能指向组件实例(因为没有组件实例) -
没有内部状态(
state)
-
- 定义一个函数组件
import React from 'react' // rfc export default function ComFunc() { return ( <div>函数组件</div> ) }
生命周期
事物从创建到销毁的整个过程称之为生命周期,React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
-
生命周期和生命周期函数的关系:谈
React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的-
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
比如装载阶段(
Mount),组件第一次在DOM树中被渲染的过程比如更新过程(
Update),组件状态发生变化,重新更新渲染的过程比如卸载过程(
Unmount),组件从DOM树中被移除的过程 -
React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的某些函数进行回调,这些函数就是生命周期函数比如实现
componentDidMount函数:组件已经挂载到DOM上时,就会回调比如实现
componentDidUpdate函数:组件已经发生了更新时,就会回调比如实现
componentWillUnmount函数:组件即将被移除时,就会回调
-
-
生命周期函数解析
-
construct:如果不初始化state或不进行方法绑定,则不需要为React组件实现构造函数constructor中通常只做两件事情:通过给this.state赋值对象来初始化内部的state并为事件绑定实例(this) -
componentDidMount:会在组件挂载后(插入DOM树中)立即调用依赖于
DOM的操作可以在这里进行在此处发送网络请求就最好的地方(官方建议)
可以在此处添加一些订阅(会在
componentWillUnmount取消订阅) -
componentDidUpdate:会在更新后会被立即调用,首次渲染不会执行此方法当组件更新后,可以在此处对
DOM进行操作如果你对更新前后的
props进行了比较,可以选择在此处进行网络请求(当 props 未发生变化时,则不会执行网络请求) -
componentWillUnmount:会在组件卸载及销毁之前直接调用在此方法中执行必要的清理操作,例如清除
timer、取消网络请求、清除在componentDidMount()中创建的订阅等 -
getDerivedStateFromProps:state的值在任何时候都依赖于props时使用,该方法返回一个对象来更新state -
getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置) -
shouldComponentUpdate:该生命周期函数很常用,但是等讲性能优化时再来详细讲解 -
更详细的生命周期相关的内容,可以参考官网: zh-hans.reactjs.org/docs/reactc…
import React, { Component } from 'react' export class ComLife extends Component { constructor() { console.log('ComLife constructor'); super() this.state = { message: '组件生命周期', } } componentDidMount() { // 请求在此处 console.log('ComLife componentDidMount', this); } componentDidUpdate(prevProps, prevState,snapshot) { console.log('ComLife componentDidUpdate', snapshot) } componentWillUnmount() { console.log('ComLife componentWillUnmount') } // 不常用生命周期 getSnapshotBeforeUpdate(prevProps, prevState, snapshot) { // 组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。 console.log('ComLife getSnapshotBeforeUpdate', prevProps, prevState, snapshot); return 'snapshotValue' } shouldComponentUpdate() { // 返回true render会更新反之不会 console.log('ComLife shouldComponentUpdate不会执行render'); return false } renderUpdate() { this.setState({ message: 'ComLife组件更新了' }) } render() { console.log('ComLife render') return ( <div> <span>{this.state.message}</span> <button onClick={e=> this.renderUpdate()}>ComLife更新</button> </div> ) } } export default ComLife -
父子组件通信
在一个React项目中,组件之间的通信是非常重要的环节,父组件在展示子组件,可能会传递一些数据给子组件:
-
父组件通过
属性=值的形式来传递给子组件数据 -
子组件通过
props参数获取父组件传递过来的数据 -
案例实现如下:
// ComTab import React, { Component } from 'react' import ComTabSon from './ComTabSon' export class ComTab extends Component { constructor() { super() this.state = { tabList: [ {name: '流行', type: 'liuxing'}, {name: '新款', type: 'xinkuan'}, {name: '精选', type: 'jingxuan'}, ], currentIndex: 0 } } sonItemClick(currentIndex) { this.setState({ currentIndex }) } getItemSlot(item) { if(item.type === 'liuxing') { return <i>流行</i> }else if(item.type === 'xinkuan') { return <strong>新款</strong> }else { return <button>精选</button> } } render() { const {currentIndex, tabList} = this.state return ( <div> <h3>{this.props.contextColor ? this.props.contextColor : 'ComTab-父子通信案例'}</h3> <ComTabSon tabList={tabList} currentIndex={currentIndex} itemClick={index=>this.sonItemClick(index)} itemSlot={item=>this.getItemSlot(item)} /> <h2>{tabList[currentIndex].name}</h2> </div> ) } } export default ComTab // ComTabSon import React, { Component } from 'react' import '../style/comTabSon.css' export class ComTabSon extends Component { itemClick(mi) { this.props.itemClick(mi) } render() { console.log('ComTabSon',this.props); return ( <div className='tab'> { this.props.tabList.map((m,mi)=> { return ( <div key={m.type} className={`tab_item ${this.props.currentIndex === mi ? 'active' : ''}`} onClick={e=> this.itemClick(mi)} > {/* <span className={this.props.currentIndex === mi ? 'active' : ''}>{m.name}</span> */} {this.props.itemSlot(m)} </div> ) }) } </div> ) } } export default ComTabSon
插槽(slot)
在开发中抽取了一个组件,但是为了让这个组件具备更强的通用性,不能将组件中的内容限制为固定的div、span等等这些元素,React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
-
组件的
children子元素:每个组件都可以获取到props.children,它包含组件的开始标签和结束标签之间的内容,有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生 -
props属性传递React元素;通过具体的属性名,可以让我们在传入和获取时更加的精准 -
代码如下:
// ComSlot
import React, { Component } from 'react'
import ComSlotSon from './ComSlotSon'
export class ComSlot extends Component {
render() {
const btn = <button>按钮2</button>
return (
<div>
<ComSlotSon
leftSlot={btn}
centerSlot={<strong>嘻嘻</strong>}
rightSlot={<i>哒哒</i>}
>
<button>按钮1</button>
<i>哈哈</i>
<strong>啦啦</strong>
</ComSlotSon>
</div>
)
}
}
export default ComSlot
// ComSlotSon
import React, { Component } from 'react'
import '../style/comSlotSon.css'
export class ComSlotSon extends Component {
leftSlotClick(left) {
console.log(left);
}
render() {
console.log('ComSlotSon',this.props);
return
(<div>
<h3>ComSlot-插槽使用children</h3>
<div className='slot'>
{/* 只有一个元素时这样取 */}
{/* <div>{this.props.children}</div> */}
<div>{this.props.children[0]}</div>
<div>{this.props.children[1]}</div>
<div>{this.props.children[2]}</div>
</div>
<h3>ComSlot-插槽使用prop传值</h3>
<div className='slot'>
<div>{this.props.leftSlot}</div>
<div>{this.props.centerSlot}</div>
<div>{this.props.rightSlot}</div>
</div>
</div>
)
}
}
export default ComSlotSon
Context应用-组件数据通信
引入
-
在开发中,比较常见的数据传递方式是通过
props属性自上而下(由父到子)进行传递 -
但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、
UI主题、用户登录状态、用户信息等) -
如果我们在顶层的
App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的 操作 -
React提供了一个API:Context,一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props -
Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
API
-
React.createContext:- 创建一个需要共享的
Context对象 - 如果一个组件订阅了
Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的context值 defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
- 创建一个需要共享的
-
Context.Provider:-
每个
Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化 -
Provider接收一个value属性,传递给消费组件 -
一个
Provider可以和多个消费组件有对应关系 -
多个
Provider也可以嵌套使用,里层的会覆盖外层的数据 -
当
Provider的value值发生变化时,它内部的所有消费组件都会重新渲染
-
-
Class.contextType:-
挂载在
class上的contextType属性会被重赋值为一个由React.createContext()创建的Context对象 -
这能让你使用
this.context来消费最近Context上的那个值 -
你可以在任何生命周期中访问到它,包括
render函数中
-
-
Context.Consumer:-
React组件也可以订阅到context变更,这能让你在函数式组件 中完成订阅context -
这里需要函数作为子元素(
function as child)这种做法 -
这个函数接收当前的
context值,返回一个React节点 -
什么时候使用
Context.Consumer呢?当使用
value的组件是一个函数式组件时当组件中需要使用多个
Context时
-
-
API练习代码如下:// themeContext.js import { createContext } from "react"; // import React from "react"; // const ThemeContext = React.createContext("dark"); const ThemeContext = createContext(); export default ThemeContext; // userContext.js import { createContext } from "react"; // import React from "react"; // const UserContext = React.createContext({ // name: "context", // phone: "13323065577", // }); const UserContext = createContext({ name: "context", phone: "13323065577", }); export default UserContext; // ComContext.jsx import React, { Component } from 'react' import ComText from './ComText' import ThemeContext from '../utils/themeContext' import UserContext from '../utils/userContext' export class ComContext extends Component { constructor() { super() this.state = { contextColor: 'ComContext页面传过来的值颜色' } } render() { console.log(UserContext) return ( <div> <h3>ComContext-非父子数据共享</h3> <ThemeContext.Provider value='light'> {/* 使用UserContext的默认value值 */} <UserContext.Provider value={UserContext._currentValue}> {/* ComTextSon里面使用了传入的值 */} <ComText /> </UserContext.Provider> </ThemeContext.Provider> </div> ) } } export default ComContext // import React, { Component } from 'react' import ComTextSon from './ComTextSon' export class ComText extends Component { render() { const btn = <button>按钮2</button> return ( <div> <ComTextSon leftSlot={btn} centerSlot={<strong>嘻嘻</strong>} rightSlot={<i>哒哒</i>} > <button>按钮1</button> <i>哈哈</i> <strong>啦啦</strong> </ComTextSon> </div> ) } } export default ComText // ComTextSon.jsx import React, { Component } from 'react' import '../style/ComTextSon.css' import ThemeContext from '../utils/themeContext'; import UserContext from '../utils/userContext'; export class ComTextSon extends Component { leftSlotClick(left) { console.log(left); } render() { console.log('ComTextSon',this.props); return this.context ? (<div> {/* 一个Context时类组件可以使用这个,多个使用Consumer */} <h3>ComContext-ThemeContext页面传过来的值{this.context}</h3> <UserContext.Consumer> {value => { return <i>使用Consumer: {value.name}</i> }} </UserContext.Consumer> </div>) : (<div> <h3>ComTextSon-没有context</h3> </div> ) } } // 一个Context时类组件可以使用这个 ComTextSon.contextType = ThemeContext export default ComTextSon
setState
-
开发中不能直接通过修改
state的值来让界面发生更新 -
因为修改了
state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化 -
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化 -
必须通过
setState来告知React数据已经发生了变化 -
在组件中没有实现
setState的方法,为什么可以调用呢?因为setState方法是从Component中继承过来的
异步更新
-
setState的更新是异步的?-
在下面的练习代码中可以看到在
changeMsg方法中打印更新之后的message还是旧的值 -
可见
setState是异步的操作,并不能在执行完setState之后立马拿到最新的state的结果
-
-
为什么
setState设计为异步呢?-
setState设计为异步其实之前在GitHub上也有很多的讨论 -
React核心成员(Redux的作者)Dan Abramov也有回复,有兴趣可以参考一下github.com/facebook/re… -
简单的总结一下就是
setState设计为异步,可以显著的提升性能并保证内部一致性 -
如果每次调用
setState都进行一次更新,意味着render函数会被频繁调用,界面重新渲染效率是很低的 -
最好的办法应该是获取到多个更新,之后进行批量更新
-
如果同步更新了
state,但是还没有执行render函数,那么state和props不能保持同步 -
state和props不能保持一致性,会在开发中产生很多的问题
-
-
那么如何可以获取到更新后的值呢?
-
s
etState的回调中获取:setState接受两个参数,第二个参数是一个回调函数,会在更新后会执行:setState(partialState, callback) -
在生命周期函数中获取:
componentDidUpdate(prevProps, prevState,snapshot) {console.log(this.state.message);}
-
一定是异步吗?
-
React18之前- 在组件生命周期或
React合成事件中,setState是异步
- 在
setTimeout或者原生dom事件中,setState是同步
- 在组件生命周期或
-
React18之后-
在React18之后,默认所有的操作都被放到了批处理中(异步处理)
-
如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作
-
练习代码如下
import React, { Component } from 'react'
export class ComState extends Component {
constructor() {
super()
this.state = {
message: 'ComState异步'
}
}
changeMsg() {
console.log(this.state.message, 'changeMsg-setState之前');
// 基础用法
// this.setState({
// message: '点击了查看异步信息'
// })
/*
下面log打印的还是老的message的值不是修改后的,在react18之前setTimeou,
promise和原生Dom事件是同步的,18之后默认都是异步的 。
为什么设计成异步的?redux作者在issue中这样回答的:
1. 批量更新是有益的,如果每次更新都调用render函数,频繁调用效率低
2. 保证内部一致性,如果state设为同步会立即刷新但还没有执行render函数,那么state和props不能保持一致
*/
// console.log(this.state.message, 'setState之后');
// 以下setState另外用法,可以拿到更新后的值
//方法一:
/* this.setState({
message: '点击了查看异步信息按钮更新了'
},()=> {
console.log('回调里是最新的值',this.state.message);
}) */
// 方法二:
this.setState((state, props)=> {
// 1.编写一些对新的state处理逻辑
// 2.可以获取之前的state和props值
console.log(this.state.message, this.props)
return {
message: '点击了查看异步信息按钮更新了'
}
})
console.log('还是老的值', this.state.message);
}
// 方法三:
componentDidUpdate(prevProps, prevState) {
console.log('ComState-componentDidUpdate',prevState, prevProps, this.state.message);
}
render() {
console.log('render')
return (
<div>
<h3>ComState-setState详细使用</h3>
<i>{this.state.message}</i>
<button onClick={e=>this.changeMsg()}>查看异步信息</button>
</div>
)
}
}
export default ComState
render更新
当 props 或 state 发生变化时,React 会自动调用组件的 render 方法来重新渲染组件。这是 React 的核心机制之一,用于确保 UI 与数据保持同步
-
props变化:当父组件传递的props发生变化时,子组件会重新渲染 -
state变化:当组件内部的state发生变化时(例如通过setState或useState更新状态),组件会重新渲染 -
下面看下渲染的机制和如何进行性能优化
机制
-
生成 Virtual DOM:
render方法会根据当前的props和state生成新的Virtual DOM -
与旧 Virtual DOM 对比:
React会通过diff算法对比新旧Virtual DOM,找出需要更新的部分-
同层节点之间相互比较,不会跨节点比较,
tag不同直接删掉重建 -
开发中可以通过
key来指定哪些节点在不同的渲染下保持稳定 -
key应该是唯一的,key不要使用随机数和index作为key -
在最后位置插入数据,有无
key意义并不大 -
在前面插入数据:
-
在没有
key的情况下,所有的li都需要进行修改 -
当子元素拥有
key时,React使用key来匹配原有树上的子元素以及最新树上的子元素
-
-
-
更新真实 DOM:只有实际发生变化的部分才会更新到真实
DOM,而不是整个页面重新渲染
优化
-
组件重新渲染时,默认情况下会递归触发所有子组件的重新渲染
-
上面情况显然是没有必要的,会造成比较低的性能,理想应该只有依赖的数据(
state、 props)发生改变时,再调用自己的render方法 -
事实上
React也考虑到了一点,React通过以下机制优化性能:-
Virtual DOM 和
diff算法:React 会对比新旧 Virtual DOM,只有实际发生变化的部分才会更新到真实 DOM -
浅比较(
Shallow Comparison) :如果子组件的
props和state没有变化,React会跳过子组件的渲染对于函数组件,可以使用
React.memo来避免不必要的重新渲染对于类组件,可以使用
shouldComponentUpdate或继承PureComponent来优化
-
shallowEqual
shallowEqual 是 React 中用于浅比较两个对象或数组是否相等的方法,它通常用于优化组件的渲染性能,比如在 React.memo、PureComponent 或自定义 shouldComponentUpdate 中
-
shallowEqual会比较两个对象的顶层属性是否相等 -
如果属性是基本类型(如
string、number、boolean),直接比较值 -
如果属性是引用类型(如
object、array),比较引用地址(是否指向同一个对象) -
它不会递归比较嵌套对象或数组的内容,所有尽量将
props和state扁平化 -
以下是
shallowEqual的简化实现逻辑:function shallowEqual(objA, objB) { // 如果两个对象是同一个引用,直接返回 true if (objA === objB) return true; // 如果其中一个不是对象或为 null,返回 false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); // 如果属性数量不同,返回 false if (keysA.length !== keysB.length) return false; // 遍历所有属性,比较值 for (let i = 0; i < keysA.length; i++) { const key = keysA[i]; if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) { return false; } } return true; }
shouldComponentUpdate
React提供了一个生命周期方法 shouldComponentUpdate(简称为SCU),这个方法接受参数,并且需要有返回值,也可以在此方法中使用shallowEqual方法比较
- 该方法有两个参数:
-
nextProps修改之后,最新的props属性 -
nextState修改之后,最新的state属性
-
- 该方法返回值是一个
boolean类型:-
返回值为
true,那么就需要调用render方法 -
返回值为
false,那么就不需要调用render方法 -
默认返回的是
true,也就是只要state发生改变,就会调用render方法
-
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
render() {
return <div>{this.props.value}</div>;
}
}
PureComponent和memo
-
针对类组件可以将
class继承自PureComponent -
针对函数式组件组件可以使用一个高阶组件
memo -
代码如下:
import React, { PureComponent, memo } from "react"; const ComRenderFunc = memo((props) => { return <div>ComRendeFunc-memo</div>; }); export class ComRender extends PureComponent { /* 只要更改state或者props的值都会再次执行render函数, 我们可以对这个进行优化,可以在生命周期shouldComponentUpdate中, 进行判return false则不会更新render,但数据多时就会比较麻烦, react提供了PureComponent和memo在项目中尽量使用这两个, 他们的原理:也是进行了判断如果我们写了shouldComponentUpdate, react会执行它,没有写就判断是不是使用了PureComponent,使用了react 就会把isPureComponent设为true,会执行shallowEqual进行判断 */ render() { return ( <div> <h3>ComRender-对render的优化</h3> <ComRenderFunc /> </div> ); } } export default ComRender;
使用ref
在React的开发模式中,通常情况下不需要也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作,这时可以通过refs获取DOM,目前有三种方式ref=XXX:
-
传入字符串:使用时通过
this.refs传入的字符串格式获取对应的元素 -
传入一个对象:对象是通过
React.createRef()方式创建出来的,使用时获取到创建的对象其中有一个current属性就是对应的元素 -
传入一个函数:该函数会在
DOM被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存,使用时直接拿到之前保存的元素对象即可
ref 的值根据节点的类型而有所不同:
-
当
ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性 -
当
ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性 -
不能在函数组件上使用
ref属性,因为他们没有实例,可以通过React.forwardRef,后面也会学习hooks中如何使用ref
练习代码如下:
import React, { PureComponent, createRef, forwardRef, memo } from "react";
const ComRefFun = memo(
forwardRef((props, ref) => {
return (
// 函数组件必须使用forwardRef,想让父组件获取那个元素就在那个元素上加ref
<div ref={ref}>
<span>ComRefFun-函数组件</span>
<i>哒哒哒哒</i>
</div>
);
})
);
export class ComRef extends PureComponent {
constructor() {
super();
this.funel = createRef(); // 2
this.iel = createRef(); // 2
this.sel = null; // 3
}
getRefClassDom() {
console.log(this.refs.pel); // 第一种
console.log(this.iel.current); // 第二种(推荐)如果是个组件可以执行组件的方法
console.log(this.sel); // 第三种
}
getRefFunDom() {
console.log(this.funel.current);
}
render() {
return (
<div>
<h3>ComRef</h3>
<p ref="pel">第一种:哈哈哈哈</p>
<i ref={this.iel}>第二种:嘿嘿嘿嘿</i>
<strong ref={(el) => (this.sel = el)}>第三种:嘻嘻嘻嘻</strong>
<button onClick={(e) => this.getRefClassDom()}>获取类组件dom</button>
<ComRefFun ref={this.funel} />
<button onClick={(e) => this.getRefFunDom()}>获取函数组件dom</button>
</div>
);
}
}
export default ComRef;
表单组件
在React中HTML表单的处理方式和普通的DOM元素不太一样,React推荐大多数情况下使用受控组件来处理表单数据:
-
一个受控组件中,表单数据是由
React组件来管理的,只能使用setState()更新 -
另一种替代方案是使用非受控组件,这时表单数据将交由
DOM节点来处理,不受setState()的控制
受控组件
适合需要精确控制表单数据、实时验证或复杂逻辑的场景
-
特点:
-
状态由React管理:组件的值通过
state或props来控制 -
数据流单向:组件的值由
React的state或props决定,用户输入时会触发事件处理函数更新状态 -
实时同步:组件的值与
React的状态始终保持一致
-
-
执行情况:
-
实现方式:
-
在组件的
state中定义表单元素的值 -
将
state的值绑定到表单元素的value属性 -
通过
onChange事件监听用户输入,更新state
import React, { useState } from "react"; function ControlledInput() { const [value, setValue] = useState(""); const handleChange = (event) => { setValue(event.target.value); // 更新state }; return ( <div> <input type="text" value={value} onChange={handleChange} /> <p>输入的内容: {value}</p> </div> ); } export default ControlledInput; -
非受控组件
适合简单表单、性能敏感或与第三方库集成的场景
- 特点:
-
状态由DOM管理:组件的值由
DOM节点自身维护,而不是通过React的state或props -
数据流双向:组件的值可以通过
ref从DOM节点中获取 -
不实时同步:组件的值与
React的状态没有直接关联,只有在需要时(如提交表单时)才从DOM中获取值
-
- 实现方式:
-
使用
ref来访问DOM节点,在需要时(如表单提交时),通过ref获取表单元素的值 -
在非受控组件中通常使用
defaultValue来设置默认值
-
练习代码
import React, { PureComponent, createRef } from 'react'
export class ComForm extends PureComponent {
constructor() {
super()
this.state = {
username: 'shine',
password: '1313',
isAgree: false,
selectHobby: 'dance',
selectHobbys: ['dance'],
hobby: [],
hobbies: [
{ value: "sing", text: "唱", isChecked: false },
{ value: "dance", text: "跳", isChecked: false },
{ value: "rap", text: "rap", isChecked: false }
],
fei: '哒哒哒哒'
}
this.feiRef = createRef()
}
formSubmit(e) {
// 阻止默认提交行为
e.preventDefault()
// 拿到数据可以做提交动作
console.log('获取非受控值', this.feiRef.current.value);
console.log(this.state);
}
changeValue(e) {
this.setState({
[e.target.name]: e.target.value || !e.target.checked
})
}
changeHobbyValue(e,i) {
let list = [...this.state.hobbies]
list[i].isChecked = e.target.checked
let value = list.filter(f=>f.isChecked).map(m=>m.value)
this.setState({
hobbies: list,
hobby: value
})
}
changeSelectValue(e) {
// 额外补充: Array.from(可迭代对象)
// Array.from(arguments)
// e.target.selectedOptions是HTMLCollection转为数组
let list = Array.from(e.target.selectedOptions, item => item.value)
this.setState({
selectHobbys: list
})
console.log(e.target.selectedOptions);
}
render() {
return (
<div>
<h3>ComForm-受控组件和非受控组件</h3>
<form onSubmit={e=> this.formSubmit(e)}>
<label htmlFor="username">
用户名:<input id='username' name='username' type="text" value={this.state.username} onChange={e=>this.changeValue(e)} />
</label>
<label htmlFor="password">
密码:<input id='password' name='password' type="password" value={this.state.password} onChange={e=>this.changeValue(e)} />
</label>
{/* checkbox单选 */}
<label htmlFor="isAgree">
<input id='isAgree' name='isAgree' type="checkbox" checked={this.state.agree} onChange={e=> this.changeValue(e)} />
同意协议
</label>
{/* checkbox多选 */}
<div>
{
this.state.hobbies.map((m,mi)=> {
return (
<label htmlFor={m.value} key={m.value}>
<input
id={m.value}
type="checkbox"
checked={m.isChecked}
onChange={e=> this.changeHobbyValue(e, mi)}
/>
{m.text}
</label>
)
})
}
</div>
{/* select单选 */}
<select name="selectHobby" id="selectHobby" value={this.state.selectHobby} onChange={e=>this.changeValue(e)}>
{
this.state.hobbies.map((m,mi)=> {
return <option key={m.value} value={m.value}>{m.text}</option>
})
}
</select>
{/* select多选选 */}
<select name="selectHobbys" id="selectHobbys" multiple value={this.state.selectHobbys} onChange={e=>this.changeSelectValue(e)}>
{
this.state.hobbies.map(m=> {
return <option key={m.value} value={m.value}>{m.text}</option>
})
}
</select>
{/* 非受控组件 */}
<label htmlFor="fei">
<input id='fet' type="text" defaultValue={this.state.fei} ref={this.feiRef} />
</label>
<button type='submit'>提交</button>
</form>
</div>
)
}
}
export default ComForm
高阶组件
相信都知道也用过高阶函数,高阶组件和它非常相似,高阶函数至少满足以下条件之一:接受一个或多个函数作为输入或者输出一个函数,JavaScript中比较常见高阶函数的filter、map、reduce都是高阶函数
-
高阶组件的英文是
Higher-Order Components,简称为HOC -
官方的定义:高阶组件是参数为组件,返回值为新组件的函数
-
高阶组件本身不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件
-
不要修改原组件:高阶组件应该通过组合的方式增强组件功能,而不是直接修改原组件
-
传递
props:确保将高阶组件接收到的props传递给被包裹的组件 -
显示名称:为高阶组件设置显示名称(
displayName),以便在调试时更容易识别 -
案例代码:
// propsHoc.js import React, { PureComponent } from "react"; export default function propsHoc(OriginCom) { // 接收组件 class PropHoc extends PureComponent { constructor(props) { super(props); this.state = { userInfo: { name: "propsHoc", phone: "17538138619", }, }; } render() { console.log(this.props); return <OriginCom {...this.props} {...this.state.userInfo} />; } } // 返回组件 return PropHoc; } // authHoc.js import React from "react"; function authHoc(OriginCom) { // return class NewOriginCom extends PureComponent { // render() { // return <OriginCom />; // } // }; // 这里省略了组件名字,参考函数组件返回时可以不写组件名字,类组件也可以 // return class extends PureComponent { // render() { // return <OriginCom />; // } // }; return (props) => { // 函数组件返回 let token = window.localStorage.getItem("token"); return token ? <OriginCom {...props} /> : <h3>请先登录。。。</h3>; }; } export default authHoc; // lifeHoc.js import { PureComponent } from "react"; function lifeHoc(OriginCom) { return class extends PureComponent { UNSAFE_componentWillMount() { this.start = new Date().getTime(); } componentDidMount() { this.end = new Date().getTime(); let time = this.end - this.start; console.log(`当前${OriginCom.name}页面花费了${time}ms渲染完成!`); } render() { return <OriginCom {...this.props} />; } }; } export default lifeHoc; // ComHoc.jsx import React, { PureComponent } from 'react' import propsHoc from '../utils/propsHoc' import authHoc from '../utils/authHoc' import lifeHoc from '../utils/lifeHoc' const Son1 = propsHoc(function Son1(props) { // propsHoc传入函数组件得到组件,则Son1为组件,render直接引入 console.log('propsHo增加的props值', props); return ( <div> <h4>1. propsHoc: 不改原有代码的情况下添加新的props</h4> <i>原来prop值:{props.type}</i> <i>通过propsHoc加的props:{props.name}-{props.phone}</i> </div> ) }) const Son2 = authHoc(function Son2(props) { return ( <div> <strong>已登录,尽情玩吧</strong> </div> ) }) const Son3 = lifeHoc(function Son2(props) { return ( <div> <h3>组件渲染耗时</h3> <ul> <li>数据列表1</li> <li>数据列表2</li> <li>数据列表3</li> <li>数据列表4</li> <li>数据列表5</li> <li>数据列表6</li> <li>数据列表7</li> <li>数据列表8</li> <li>数据列表9</li> <li>数据列表10</li> </ul> </div> ) }) export class ComHoc extends PureComponent { render() { return ( <div> <h3>ComHoc-高阶组件-接收组件返回组件</h3> {/* 回忆高阶函数:接收一个或者多个函数,返回一个函数 */} {/* props增强-不改原有代码的情况下添加新的props, 下面传入的type可以在propsHoc拿到 */} <Son1 type='ComHocFunSon1' /> {/* 登录鉴权案例 */} <Son2 /> {/* 组件渲染耗时 */} <Son3 /> </div> ) } } export default ComHoc
Portals的使用
在React中,默认情况下,子组件会作为父组件的子元素渲染到DOM中。然而使用Portals时,React允许你将子组件渲染到其父组件DOM层级之外的地方:
-
ReactDOM.createPortal(child, container) -
第一个参数(
child)是任何你希望渲染渲染的React子元素 -
第二个参数(
container)是一个你希望将该React元素渲染到的DOM节点 -
这种技术特别适用于需要在页面中脱离父组件结构显示的情况,比如模态框(
Modal)、工具提示(Tooltip)、下拉菜单(Dropdown)等 -
练习代码如下:
import React, { PureComponent } from 'react' import { createPortal } from 'react-dom' class ComPop extends PureComponent { render() { return createPortal( // 以插槽形式传过来的元素都会挂载到pop div上面 this.props.children, document.getElementById('pop') ) } } export class ComPortals extends PureComponent { render() { return ( <div> { createPortal( // h3就会挂在pop父元素上面 <h3>ComPortals-将元素挂载到任何dom上</h3>, document.getElementById('pop') ) } {/* 将内容挂载到pop上组件封装 */} <ComPop> <h4>ComPop组件</h4> </ComPop> </div> ) } } export default ComPortals
fragment
通常在 React 中,如果你返回多个元素,React 会要求你将它们包裹在一个父元素中,但这个父元素会在真实的 DOM 中被渲染出来,可能会影响布局或结构。而 Fragment 解决了这个问题,它允许你将多个元素分组在一起而不添加额外的 DOM 节点
-
使用
<Fragment>...</Fragment>包裹元素,React还提供了一个简写的语法来使用Fragment,即使用空标签<>和</>来包裹元素 -
当在列表渲染中使用
Fragment时,可以为每个Fragment添加key属性,但使用简写时不能加key -
练习代码如下:
import React, { Fragment, PureComponent } from 'react' export class ComFragment extends PureComponent { constructor() { super() this.state = { list: ['啦啦啦','哈哈哈啊哈','哒哒哒哒','嘻嘻嘻嘻'] } } render() { return ( // 不会渲染元素 // <Fragment> // <h3> // ComFragment // </h3> // </Fragment> // 语法糖 // <> // <h3> // ComFragment // </h3> // <i>哈哈啊哈哈哈</i> // </> <div> { this.state.list.map(m=>{ return ( // 需要绑定key时,此时不能用语法糖 <Fragment key={m}> <i>{m}/</i> </Fragment> ) }) } </div> ) } } export default ComFragment
StrictMode
-
StrictMode是一个用于帮助开发者识别应用潜在问题的工具 -
它本身不会渲染任何可见的 UI 元素,只会在开发模式下激活额外的检查和警告
-
用于标识不安全的生命周期方法、意外的副作用和过时的API等问题
-
可以为应用程序的任何部分启用严格模式,严格模式检查的是什么?
-
帮助识别不安全的生命周期方法:React 会对使用了过时的生命周期方法的组件发出警告,提醒开发者使用新的方法
-
检查副作用:
StrictMode会在开发环境中对组件的生命周期进行额外检查,帮助开发者确保副作用操作(如订阅事件、定时器、数据请求等)在组件卸载时能够正确清理,防止潜在的内存泄漏或不一致的状态 -
警告过时的 API:当应用使用一些已经废弃的 API(例如
findDOMNode)时,StrictMode会出警告,提示开发者使用新的替代方法 -
检查不一致的渲染:
StrictMode会做一些额外的渲染和检测,确保组件的渲染是符合预期的,从而帮助开发者发现潜在的渲染问题
-
-
练习代码如下:
import React, { PureComponent, StrictMode } from 'react' export class ComStrictSon extends PureComponent { constructor() { super() console.log('111constructor'); // 严格模式会执行两次constructor,严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用,在生产环境中,是不会被调用两次的 } // componentDidMount() { // console.log(this.refs.son); // 严格模式会报错 // } // UNSAFE_componentWillMount(){ // 严格模式会报错 // console.log('UNSAFE_componentWillMount'); // } render() { return ( <div> <h3>ComStrictSon</h3> </div> ) } } export class ComStrict extends PureComponent { render() { return ( <div> <h3>ComStrictMode-严格模式</h3> {/* StrictMode不会渲染元素 */} <StrictMode> {/* <ComStrictSon ref='son' 严格模式会报错 /> */} <ComStrictSon /> </StrictMode> </div> ) } } export default ComStrict
app.jsx入口文件如下:
里面是组件化练习的所有组件引入
import React, { Component } from "react";
import ComClass from "./component/ComClass";
import ComFunc from "./component/ComFunc";
import ComLife from "./component/ComLife";
import ComFather from "./component/ComFather";
import ComTab from "./component/ComTab";
import ComSlot from "./component/ComSlot";
import ComContext from "./component/ComContext";
import ComEventBus from "./component/ComEventBus";
import ComState from "./component/ComState";
import ComRender from "./component/ComRender";
import ComRef from "./component/ComRef";
import ComForm from "./component/ComForm";
import ComHoc from "./component/ComHoc";
import ComPortals from "./component/ComPortals";
import ComFragment from "./component/ComFragment";
import ComStrict from "./component/ComStrict";
import ComTransite from "./component/ComTransite";
import ComCss from "./component/ComCss";
export class App extends Component {
render() {
return (
<div>
<h1>APP</h1>
<hr />
{/* 类组件和函数组件 */}
<ComClass />
<ComFunc />
<hr />
{/* 组件生命周期 */}
<ComLife />
<hr />
{/* 父子通信 */}
<ComFather />
<hr />
{/* 通信案例 */}
<ComTab />
<hr />
{/* 插槽使用 */}
<ComSlot />
<hr />
{/* 非父子数据共享 */}
<ComContext />
<hr />
{/* 非父子事件传值 */}
<ComEventBus />
<hr />
{/* setState详细使用 */}
<ComState />
<hr />
{/* render的优化 */}
<ComRender />
<hr />
{/* ref的使用 */}
<ComRef />
<hr />
{/* 受控和非受控组件 */}
<ComForm />
<hr />
{/* 高阶组件 */}
<ComHoc />
<hr />
{/* ComPortals */}
<ComPortals />
<hr />
{/* ComFragment */}
<ComFragment />
<hr />
{/* 严格模式 */}
<ComStrict />
<hr />
{/* 动画 */}
<ComTransite />
<hr />
{/* react中写css */}
<ComCss />
<hr />
</div>
);
}
}
export default App;