作用:跨组件传递数据
使用步骤:
1 调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
const {Provider,Consumer}=React.createContext
2 使用Provider组件作为父节点
3 设置value属性,表示要传递的数据
4 调用Consumer组件接收数据
{data = >data参数表示接收到的数据--{data}}
总结:
1 如果两个组件是嵌套多层可以使用Context实现组件通信
2 Context提供了两个组件:Provider和Consumer
3 Provider组件:用来提供数据
4 Consumer组件:用来消费数据
children属性:表示组件的子节点,当组件标签有子节点时,props就会有该属性
children属性与普通的props一样,值可以是任意值
function Hello(props){
return (组件的子节点:{props.children})
}
我是子节点
props校验:允许在创建数组的时候,就指定props的类型、格式等
作用:捕获使用组件时应为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤:
1 安装包prop-types(yarn add prop-types/npm i props-types)
2 导入prop-types包
3 使用组件名.propTypes={}来给组件的props添加校验规则
4 校验规则通过PropTypes对象来指定
import PropTypes from 'prop-types'
function App(props){
return (hello,{props.colors}
)
}
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:PropType.string,
fontSize:PropTypes.number
})
作用:给props设置默认值,在未传入props时生效
function App(props){
return (此次展示props的默认值:{props.pageSize})
}
// 设置默认值
App.defaultProps = {
pageSize: 123
}
// 不传入pageSize属性
意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
钩子函数的作用:为开发人员在不同阶段操作组件提供了时机
只有类组件才有生命周期
执行时机:组件创建时(页面加载时)
执行顺序:constructor()-->render()-->componentDidMount
钩子函数 触发时机 作用 constructor 创建组件时,最先执行 初始化state、为事件处理程序绑定this render 每次组件渲染都会触发 渲染UI(注意:不能调用setState()) componentDidMount 组件挂载(完成DOM渲染)后 发送网络请求、DOM操作
执行时机:1.setState() 2.forceUpdate() 3.组件接收到新的props
说明:以上三者任意一种变化,组件就会重新渲染
执行顺序:render()--->componentDidUpdate()
钩子函数 触发时机 作用 render 每次组件渲染都会触发 渲染UI(与挂载阶段是同一个render) componentDidUpdate 组件更新(完成DOM渲染)后 发送网络请求、DOM操作,注意:如果要setState()必须放在一个if条件中
执行时机:组件从页面中消失
钩子函数:componentWillUnmount
触发时机:组件卸载(从页面中消失)
作用:执行清理工作(比如:清理定时器等)
思考:如果两个组件中的部分功能相似或相同,该如何处理?
处理方式:复用相似的功能(联想函数封装)
复用什么?1.state 2.操作state的方法(组件状态逻辑)
两种方式:1.render props模式 2.高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
思路:将要复用的state和操作state的方法封装到一个组件中
问题一:如何拿到该组件中复用的state?
在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
问题2:如何渲染任意的UI?
使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)
( 鼠标当前位置{mouse.x},{mouse.y}
)} />
1 创建Mouse组件,在组件中提供复用的状态逻辑代码(1.状态 2.操作状态的方法)
2 将要复用的状态作为props.render(state)方法的参数,暴露到组件外部
3 使用props.render()的返回值作为要渲染的内容
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
// 创建Mouse组件
class Mouse extends React.Component {
// 鼠标位置state
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 (
render props模式
{
return (鼠标位置:{mouse.x},{mouse.y}
)
}}>
// Mouse组件的复用
{
return ()
}}
/>
);
}
}
root.render(
)
Mouse组件负责:封装复用的状态逻辑代码(1.状态 2.操作状态的方法)
状态:鼠标坐标(x,y)
操作状态的方法:鼠标移动事件
传入的render prop负责:使用复用的状态来渲染UI结构
注意:并不是该模式叫render props就必须使用名为render的prop,实际上可以使用任意名称的prop
把prop是一个函数并且告诉组件要渲染什么内容的技术叫做:render props模式
推荐:使用children代替render属性
{({x,y})=>{鼠标的位置是:{x},{y}
}
组件内部:
this.props.children(this.state)
目的:实现状态逻辑复用
采用包装(装饰)模式
高阶组件(HOC,Higher-OrderComponent)是一个函数,接收要包装的组件,返回增强后的组件
高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给
被包装组件WrappedComponent
const EnhancedComponent = withHOC(WrappedComponent)
// 高阶组件内部创建的类组件
class Mouse extends React.Component{
render(){
return
}
}
1 创建一个函数,名称约定以with开头
2 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
3 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
4 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
5 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
// 创建高阶组件
function withMouse(WrappedComponent){
// 该组件提供复用的状态逻辑
class Mouse extends React.Component{}
return Mouse
}
// Mouse组件的render方法中:
return
// 创建组件
const MousePosition = withMouse(Position)
// 渲染组件
使用高阶组件存在的问题:得到的两个组件名称相同
原因:默认情况,React使用组件名称作为displayName
解决方式:为高阶组件设置displayName便于调试时区分不同的组件
displayName作用:用于设置调试信息(React Developer Tools信息)
设置displayName:Mouse.displayName=`WithMouse${getDisplayName(WrappedComponent)}`
问题:props丢失
原因:高阶组件没有往下传递props
解决方式:渲染WrappedComponent时,将state和this.props一起传递给组件
传递方式:
1 组件通讯是构建React应用必不可少的一环
2 props的灵活性让组件更加强大
3 状态提升是React组件的采用模式
4 组件生命周期有助于理解组件的运行过程
5 钩子函数让开发者可以在特定的时机执行某些功能
6 render props模式和高阶组件都可以实现组件状态逻辑复用
7 组件极简模型:(state,props) => UI
setState()是异步更新数据的
注意:使用该语法时,后面的setState()不要依赖于前面的setState()
可以多次调用setState(),只会触发一次重新渲染
this.state = {count:1}
this.setState({
count:this.state.count+1
})
console.log(this.state.count)
推荐:使用setState((state,props)=>{})语法
参数state:表示最新的state
参数props:表示最新的props
this.setState((state,props)=>{
return count:state.count + 1
})
console.log(this.state.count)
场景:在状态更新(页面完成重新渲染)后立即执行某个操作
语法:setState(updater,[,callback])
this.setState(
(state,props)=>{},
()=>{console.log('这个回调函数会在状态更新后立即执行')}
)
this.setState(
(state,props)=>{},
()=>{document.title='更新state后的标题'+this.state.count}
)
JSX仅仅是createElement()方法的语法糖(简化语法)
JSX语法被@babel/preset-react插件编译为createElement()方法
React元素:是一个对象,用来描述你希望在屏幕上看到的内容
setState()的两个作用:修改state、更新组件(UI)
过程:父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)
减轻state:只存储跟组件渲染相关的数据
注意:不用做渲染的数据不要放在state中
对于这种需要在多个方法中用到的数据,应该放在this中
class Hello extends Component{
componentDidMount(){
// timerId存储到this中,而不是state中
this.timerId = setInterval(()=>{},2000)
}
componentWillUnmount(){
clearInterval(this.timerId)
}
render(){...}
}
组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
问题:子组件没有任何变化时也会重新渲染
如何避免不必要的重新渲染呢?
解决方式:使用钩子函数shouldComponentUpdate(nextProps,nextState)
作用:通过返回值决定该组件是否重新渲染,返回true表示重新渲染,false表示不重新渲染
触发时机:更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate->render)
class Hello extends Component{
shouldComponentUpdate(){
// 根据条件,决定是否重新渲染组件
return false
}
render(){...}
}
纯组件:PureComponent与React.Component功能相似
区别:PureComponent内部自动实现了shouldComponentUpdate钩子,不需要手动比较
原理:纯组件内部通过分别对比前后两次props和state的值,来决定是否重新渲染组件
class Hello extends React.PureComponent{
render(){
return (纯组件)
}
}
说明:纯组件内部的对比是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,og})
// PureComponent内部做法:
最新的state.obj===上一次的state.obj // true,不重新渲染组件
注意:state或props中属性值为引用类型,应该创建新数据,不要直接修改原数据,示例:
// 正确创建新数据
const newObj={...state.obj,number:2}
setState({obj:newObj})
// 正确创建新数据
// 不要用数组的push/unshift等直接修改当前数组的方法
// 而应该用concat或slice等这些返回新数组的方法
this.setState({list:[...this.state.list,{新数据}]})
React更新视图的思想是:只要state变化就重新渲染视图
特点:思路非常清晰
问题:组件中有一个DOM元素需要更新时,也得把整个组件的内容重新渲染到页面中?不是
理想状态:部分更新,只更新变化的地方
问题:React是如何做到部分更新的?虚拟DOM配合Diff算法
虚拟DOM:本质上就是一个JS对象,用来描述你希望在屏幕上看到的内容(UI)
1 初次渲染时,React会根据初始state(Model),创建一个虚拟DOM对象
2 根据虚拟DOM生成真正的DOM,渲染到页面中
3 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象
4 与上一次得到的虚拟DOM对象,使用Diff算法对比(找不同),得到需要更新的内容
5 最终,React只将变化的内容更新(patch)到DOM中,重新渲染到页面
组件render()调用后,根据状态和JSX结构生成虚拟DOM对象
示例中,只更新p元素的文本节点内容
{
type:'div',
props:{
children:[
{type:'h1',props:{children:'随机数'}},
{type:'p',props:{children:0}}
]
}
}
// ...省略其他结构
{type:'p',props:{children:2}}
1 工作角度:应用第一,原理第二
2 原理有助于更好地理解React的自身运行机制
3 setState()异步更新数据
4 父组件更新导致子组件更新,纯组件提升性能
5 思路清晰简单为前提,虚拟DOM和Diff保效率
6 虚拟DOM->state+JSX
7 虚拟DOM的真正价值从来都不是性能