组件化开发(React二) 组件化开发: 如果把一个页面的所有处理逻辑放在一起,处理起来会很复杂,因此将一个页面拆分为不同的功能块
一个完整的页面可以划分为很多个组件
每个组件都用于实现页面的一个功能
每一个组件又可以细分
组件本身又可以在很多地方复用
React组件:
类组件与函数组件
根据内部是否有状态需要维护:无状态组件、有状态组件(this.state,不考虑hooks的情况下函数组件是无状态组件,而类组件不定义this.state也可以看做无状态组件)
根据组件的不同职责:展示型组件、容器型组件
组件之间有重叠,但都是为了数据逻辑和UI展示的分离
函数组件、无状态组件、展示型组件主要关注UI的展示
类组件、有状态组件、容器型组件主要关注数据逻辑
类组件与函数组件 类组件 定义要求: 首字母大写、继承自React.Component、实现render函数
constructor可选,在其中初始化数据
this.state中维护组件内部的数据
render是类组件唯一必须实现的方法
函数组件(没有hooks的函数组件)
没有生命周期、会被挂载、没有生命周期函数
this关键字不能指向组件实例,因为没有组件实例
没有内部状态state
1 2 3 4 5 6 7 function App ( ){ return ( <div > Hello World</div > ) } const root = ReactDOM .createRoot (document .querySelector ('#root' ))root.render (<App /> )
生命周期 生命周期: 从创建到销毁的整个过程,了解生命周期在最合适的地方完成想要的功能
React的生命周期、生命周期函数
装载阶段:组件第一次在DOM树种被渲染的过程——装载完成:componentDidMount
更新阶段:组件状态发生变化,重新更新渲染的过程——更新完成:componentDidUpdate
卸载阶段:组件从DOM树种被移除的过程——即将移除:componentWillUnmount
各函数任务及时刻 constructor:
不初始化state或不进行方法绑定,则可以不实现构造函数
作用一:给this.state赋值对象来初始化state
作用二:为事件绑定this——调用super()
componentDidMount:
组件挂载到DOM Tree后立马调用
作用一:进行依赖于DOM的操作
作用二:发送网络请求的最好位置
作用三:添加订阅
componentDidUpdate:
组件更新后立即调用,首次渲染不会执行这个方法
作用一:组件更新后,此处进行DOM相关操作
作用二:当更新前后props发送了变化,此处发送网络请求
componentWIllUnmount:
组件卸载及销毁之前直接调用
作用一:进行必要的清理操作
作用二:清除timer,取消网络请求或取消上面创建的订阅
不常用生命周期函数
getDerivedStateFromProps:state 的值在任何 时候都依赖于 props时使用;该方法返回一个对象 来更新state
getSnapshotBeforeUpdate:在React更新DOM 之前回调的一个函数,可以获取DOM更新前的一 些信息(比如说滚动位置)
shouldComponentUpdate:该生命周期函数很 常用,但是我们等待讲性能优化时再来详细讲解
组件之间的通信 父组件传值到子组件
父组件通过 属性=值 的形式来传递给子组件数据
子组件通过props参数获取父组件传过来的值
默认值设置: ChildComponent.defaultProps
参数类型设置: ChildComponent.propTypes isRequired代表是否必须
父组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;class App extends React.Component { constructor ( ){ super () this .state = { message : "I am your father!" , info :"You are my child!" } } render ( ){ let {message,info} = this .state return ( <div > <h2 > {message}</h2 > <ChildComponent info = {info}/ > </div > ) } } export default App
子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { Component } from 'react' import { PropTypes } from 'prop-types' export class ChildComponent extends Component { render ( ) { return ([ <div key ="1" > {this.props.info}</div > , <div key ="2" > {this.props.info2}</div > ]) } } ChildComponent .propTypes = { info : PropTypes .string .isRequired } ChildComponent .defaultProps = { info :"Tell me who are you!" , info2 :"You are my Dad!" } export default ChildComponent
子组件传值到父组件
通过props给子组件传递一个回调函数,在子组件中调用这个函数
父组件代码:
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 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;class App extends React.Component { constructor ( ){ super () this .state = { message : "I am your father!" , info :"You are my child!" } } dataFromChild = function (info ){ this .setState ({ message :info }) } render ( ){ let {message,info} = this .state return ( <div > <h2 > {message}</h2 > <ChildComponent info = {info} dataFromChild = {(message) => this.dataFromChild(message)}/> </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 import React , { Component } from 'react' import { PropTypes } from 'prop-types' export class ChildComponent extends Component { sendDataToFather = function ( ){ this .props .dataFromChild (555 ) } render ( ) { return ([ <div key ="1" onClick = {() => {this.sendDataToFather()}}>{this.props.info}</div > , <div key ="2" > {this.props.info2}</div > ]) } } ChildComponent .propTypes = { info : PropTypes .string .isRequired } ChildComponent .defaultProps = { info :"Tell me who are you!" , info2 :"You are my Dad!" } export default ChildComponent
注:各个地方函数的使用要注意,是否需要this,一般传递过去的函数都是箭头函数,调用的函数一般为function
1 2 <ChildComponent info = {info} dataFromChild = {(message )=> this .dataFromChild (message)}/> <ChildComponent info = {info} dataFromChild = {this.dataFromChild}/ >
如果直接传递,那么函数的调用由React确定就不知道this是啥
传递过去箭头函数,那么我们不关心什么时候调用这个箭头函数,而箭头函数包含的是隐式绑定的this,所以调用的函数需要function,要不然找不到this
React中实现插槽 为了使组件具有更好的通用性,组件的内容不能限制为固定的div、span等,因此父组件传元素到子组件中,使得元素类型可以多变
通过组件的children子元素实现(父像子传元素) 子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React , { Component } from 'react' export class ChildComponent extends Component { render ( ) { const {children} = this .props return ( <div > <div > {children[0]} </div > <div > {children[1]} </div > <div > {children[2]} </div > </div > ) } } export default ChildComponent
父组件代码1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;class App extends React.Component { constructor ( ){ super () } render ( ){ return ( <ChildComponent > <button > 按钮</button > <h2 > 标题</h2 > <span > span</span > </ChildComponent > ) } } export default App
结果:
更改父组件的子元素
1 2 3 4 5 <ChildComponent > <span > 按钮</span > <i > 标题</i > <button > 按钮</button > </ChildComponent >
结果:
通过props实现插槽(父向子传元素)
上面描述过通过props传值到子元素,props可以传的值不仅仅有JavaScript对象,React对象也可以作为参数传过去
可以避免通过children获取时索引不能精准的获取传入的元素
父组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;class App extends React.Component { constructor ( ){ super () } render ( ){ const spanJSX = <span > 按钮</span > const iJSX = <i > 标题</i > const buttonJSX = <button > 按钮</button > return ( <ChildComponent spanJSX ={spanJSX} iJSX = {iJSX} buttonJSX ={buttonJSX}/ > ) } } export default App
子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , { Component } from 'react' export class ChildComponent extends Component { render ( ) { const {spanJSX,iJSX,buttonJSX} = this .props return ( <div > <div > {spanJSX}</div > <div > {iJSX}</div > <div > {buttonJSX}</div > </div > ) } } export default ChildComponent
结果:
作用域插槽
上面两种插槽方法子组件接受到的都是有父组件写死的元素,但某些时候在子组件中需要根据状态更改组件的内容
所以需要实现元素类型父组件决定,数据或状态子组件决定
类似于这种由父组件子组件共同协作完成的,通常使用传递函数实现
父组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;class App extends React.Component { constructor ( ){ super () } render ( ){ return ( <ChildComponent itemtyPE = {item => <button > {item}</button > }/> ) } } export default App
子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 import React , { Component } from 'react' export class ChildComponent extends Component { render ( ) { const {itemtyPE} = this .props const message = "哈哈哈" return ( <div > {itemtyPE(message)} </div > ) } } export default ChildComponent
非直接父子组件的通信 常见方法:
通过props属性自上而下传递
但是对于某些数据需要在多个组件中共享(如地区、用户信息等等)
如果在顶层App定义这些信息,一层层传递下去,对于一些中间层不需要这些数据的组件来说,是一种冗余的操作
Context方法:
context提供了一种在组件之间共享值的方法,而不必显示的通过树的逐层传递props
context的目的是共享对于组件树来说是“全局的数据”,如当前用户、主题
注册Context 由于后续会在多个组件中用到这个Context1,所以将Context1的注册放到一个js文件中
1 2 3 4 5 import React from "react" ;const Context1 = React .createContext ()export default Context1
Context值的设置 父组件设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;import Context1 from "./context/Context1" ;class App extends React.Component { constructor ( ){ super () } render ( ){ return ( <Context1.Provider value ={{color: "red ",size:18 }}> <ChildComponent /> </Context1.Provider > ) } } export default App
Context值的获取 后代类组件Context值的获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { Component } from 'react' import Context1 from '../context/Context1' import GrandsonComponent from './GrandsonComponent' export class ChildComponent extends Component { constructor ( ){ super () } render ( ) { return ( <div > <h2 style ={{color:this.context.color}} > 子组件</h2 > <GrandsonComponent /> </div > ) } } ChildComponent .contextType = Context1 export default ChildComponent
后代函数组件Context值获取:
由于函数组件中没有this,所以不能够使用上面类组件获取值的方法,使用Context.Consumer获取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Context1 from "../context/Context1" export function GrandsonFunComponent ( ){ return ( <Context1.Consumer > { value=>{ return <h2 style ={{color:value.color}} > 函数孙子组件</h2 > } } </Context1.Consumer > ) } export default GrandsonFunComponent
多个Context的注册与获取 注册第二个Context:
1 2 3 4 5 import React from "react" ;const Context2 = React .createContext ()export default Context2
单组件设置多Context:
父组件采用Context嵌套的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;import Context1 from "./context/Context1" ;import Context2 from "./context/Context2" ;class App extends React.Component { constructor ( ){ super () } render ( ){ return ( <Context2.Provider value ={{color: "blue "}}> <Context1.Provider value ={{color: "red ",size:18 }}> <ChildComponent /> </Context1.Provider > </Context2.Provider > ) } } export default App
后代组件获取多Context:
通过设置contextType与Context.Consumer组合获取多个Context
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 import React , { Component } from 'react' import Context1 from '../context/Context1' import Context2 from '../context/Context2' import GrandsonComponent from './GrandsonComponent' import GrandsonFunComponent from './GrandsonFunComponent' export class ChildComponent extends Component { constructor ( ){ super () } render ( ) { return ( <div > <h2 style ={{color:this.context.color}} > 子组件</h2 > <Context2.Consumer > { value=>{ return <h3 style ={{color:value.color}} > 子组件2</h3 > } } </Context2.Consumer > <GrandsonComponent /> <GrandsonFunComponent /> </div > ) } } ChildComponent .contextType = Context1 export default ChildComponent
Context默认值的使用 使用了Context但不是Context.Provider的后代元素
Context的注册(带默认值):
1 2 3 4 5 import React from "react" ;const Context1 = React .createContext ({color :"green" })export default Context1
Context的设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;import Context1 from "./context/Context1" ;import Context2 from "./context/Context2" ;class App extends React.Component { constructor ( ){ super () } render ( ){ return ( <div > <Context2.Provider value ={{color: "blue "}}> <Context1.Provider value ={{color: "red ",size:18 }}> <ChildComponent /> </Context1.Provider > </Context2.Provider > <ChildComponent /> </div > ) } } export default App
Context的获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React , { Component } from 'react' import Context1 from '../context/Context1' import Context2 from '../context/Context2' import GrandsonComponent from './GrandsonComponent' import GrandsonFunComponent from './GrandsonFunComponent' export class ChildComponent extends Component { constructor ( ){ super () } render ( ) { return ( <div > <h2 style ={{color:this.context.color}} > 子组件</h2 > <GrandsonComponent /> <GrandsonFunComponent /> </div > ) } } ChildComponent .contextType = Context1 export default ChildComponent
任意组件之间的通信——事件总线(EventBus) 事件总线实现
setState的细节原理 为什么要setState: React没有数据劫持,直接更改this.state时,React不知道数据已经发生了变化,需要通过this.setState()来告知React数据发生了变化,需要刷新页面
setState的异步/同步 一般情况下,setSTate是异步的
设置为异步的原因:
可以显著提升性能,如果每次调用setState都进行一次更新,那么render会频繁的调用,界面重新渲染,这样效率很低(可以联系到回流-重绘),因此设置为异步获取到数据的多次更新,之后批量更新
如果同步更新了state,但还没执行render函数,那么state与子组件的props不能保持同步,可能出现若干问题
setState批量更新方法
获取到一系列setState的更新值,按先后顺序把旧的state与新的state合并(添加新的属性、更新改变了的属性),在最后将合并好的state赋值给this.state并刷新到页面上(SCU优化)
注意: 先合并再赋值给this.state
异步结果的获取: 既然是异步,那么我们便不知道更新啥时候结束,但有些时候又想在更新结束后进行一定的操作,这个操作应该在哪里进行?
方法一:setState的回调函数
setState接受两个参数:新的state+回调函数
回调执行顺序:render->componentDidUpdate(因此也可以在这个生命周期函数中对更新结束后的值操作)->setState回调
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 import React from "react" ;class App extends React.Component { constructor ( ){ super () this .state ={ counter :3 } } buttonClick ( ){ this .setState ({ counter :this .state .counter + 1 },()=> { console .log ("setState回调" ,this .state ) }) this .setState ({ counter :this .state .counter + 1 },()=> { console .log ("setState回调" ,this .state ) }) this .setState ({ counter :this .state .counter + 1 },()=> { console .log ("setState回调" ,this .state ) }) } componentDidUpdate ( ){ console .log ("componentDidUpdate" ) } render ( ){ console .log ("render" ) const { counter } = this .state return ( <div > <h2 > {counter}</h2 > <button onClick ={() => {this.buttonClick()}}>更新state</button > </div > ) } } export default App
注:
多次setState合并,一次render,一次componentDidUpdate,多次回调
这里多次一个提交了3次+1,但是最后结果只加了1,是因为this.state会在多次提交的state与原始state合并结束后再赋值给this.state,也就是这里获取到的this.state每次都是初始值3,相当于提交了3次 counter=4
this.setState的第一个参数也可以是一个函数:
返回值是要更改的值
参数是已经合并后的state而不是this.state
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 43 44 45 46 47 48 49 50 import React from "react" ;class App extends React.Component { constructor ( ){ super () this .state ={ counter :3 } } buttonClick ( ){ setTimeout (()=> { console .log ("setTimeOut宏任务" ) },0 ) new Promise ((resolve )=> { resolve ("" ) }).then ((res )=> { console .log ("promise微任务" ) }) this .setState ((state )=> { console .log ("setState" ,state) return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) this .setState ((state )=> { return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) this .setState ((state )=> { return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) } componentDidUpdate ( ){ console .log ("componentDidUpdate" ) } render ( ){ console .log ("render" ) const { counter } = this .state return ( <div > <h2 > {counter}</h2 > <button onClick ={() => {this.buttonClick()}}>更新state</button > </div > ) } } export default App
执行顺序为:this.setState第一个参数函数->render->componentDidUpdate->this回调
注:
多次setState合并,一次render,一次componentDidUpdate,多次回调
setState函数执行、render、componentDidUpdate执行都是在微任务后,宏任务前,因此前面的函数应该都是放在微任务队列中
提交3次+1,最后得到的结果也和预期相同,因为将3次setState的第一个参数函数放入微任务队列中,在原来的state基础上进行更改,然后合并,得到新的state,在调用下一个微任务setState时,用前面合并好的state作为参数,再进行更改与合并操作
虽然setState中是在微任务中分3次调用,但setState的回调是在完成componentDidUpdate再放入微队列中,此时this.state已经更改为最后值,所以获取到的都是最后结果
回调的好处:
在数据更改后处理一些state的逻辑
回调函数会把之前的state和props传入进来
由于setState是一个异步函数,所以对更新后值的处理要放入回调中
方法二:组件的生命周期函数componentDidUpdate 上面已讲
setState的同步
React18之前,在组件生命周期和React合成事件中,setState是异步
React18之前,在setTimeout或者原生dom事件中,setState是同步
注:
由React去决定运行时机的大多是异步
由setTimeout/或者原生Dom的响应函数中调用这种用户编写决定运行时机的一般是同步
React18之后,setState默认都是异步的
React18之后获取同步的setState 使用特殊的flushSync操作
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 43 44 45 46 47 48 49 50 51 52 53 54 import React from "react" ;import {flushSync} from "react-dom" class App extends React.Component { constructor ( ){ super () this .state ={ counter :3 } } buttonClick ( ){ setTimeout (()=> { console .log ("setTimeOut宏任务" ) },0 ) new Promise ((resolve )=> { resolve ("" ) }).then ((res )=> { console .log ("promise微任务" ) }) flushSync (()=> { this .setState ((state )=> { console .log ("setState" ,state) return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) }) this .setState ((state )=> { return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) this .setState ((state )=> { return {counter :state.counter +1 } },()=> { console .log ("setState回调" ,this .state ) }) } componentDidUpdate ( ){ console .log ("componentDidUpdate" ) } render ( ){ console .log ("render" ) const { counter } = this .state return ( <div > <h2 > {counter}</h2 > <button onClick ={() => {this.buttonClick()}}>更新state</button > </div > ) } } export default App
可以看到使用flushSync中的setState在微任务执行之前就已经运行,说明flushSync确实将setState更改为同步方式运行
React的SCU优化 React更新机制 React渲染流程:
React更新流程:
React在props或state发生改变时,会调用React的render方法,会创建一颗新树,React会基于新树与旧树的不同去判断如何有效的更新UI
如果两棵树进行完全比较更新,算法的时间复杂度为O(n^3),n为树中元素的数量,开销太大,React的更新性能会变得低效
React对diff算法的优化:
同层节点之间相互比较,不会跨节点比较
不同类型的节点,产生不同的树结构
开发中,通过key来指定哪些节点在不同的渲染下保持稳定
keys的优化:
在最后位置插入数据,有无key意义不大
在前面插入数据,没有key,所有的li都需要更改,有key,后续的li只需要移动
key的注意事项:
key应该是唯一的
key不能使用随机数,每次render的随机数不同
不能使用index作为key,index在操作后也可能会改变,要使用一个不会随render改变的值
shouldComponentUpdate优化(即SCU优化)
在上图中,如果App中值发生了改变,那么App的render函数会重新执行,而在App的执行过程中,会执行其所有子组件的render函数,都进行diff算法,性能很低
子组件调用render函数应该有一个前提——依赖的数据state/props发生改变,此时才调用render函数
shouldComponentUpdate函数
参数:nextProps(修改后的pros)、nextState(修改之后的state)
返回值:true(调用render方法)、false(不调用render方法)、默认返回true(只要state改变就会调用render方法)
在值发生变化时,就可以在shouldComponentUpdate监听自己的state和props是否发生变化从而觉得是否进行render
类组件PureComponent 继承自PureComponent自动实现了SCU优化,不需要自己去编写shouldComponentUpdate函数
如果使用Component,只要调用setState就会执行render函数,而PureComponent会帮我们实现比较setState后state是否改变,如果改变才执行render
不可变数据的力量 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 43 import React from "react" ;import {flushSync} from "react-dom" class App extends React.Component { constructor ( ){ super () this .state ={ books :[ {name :"aaa" ,count :1 }, {name :"bbb" ,count :1 }, {name :"ccc" ,count :1 }, {name :"ddd" ,count :1 } ] } } addNewBook ( ){ const newBook = {name :"eee" ,count :1 } this .setState ({ }) } render ( ){ console .log ("render" ) const { books } = this .state return ( <div > <ul > {books.map((item,index)=>{ return( <li key ={index} > <span > name:{item.name}-count:{item.count}</span > <button > +1</button > </li > ) })} </ul > <button onClick ={e => this.addNewBook()}>添加新书</button > </div > ) } } export default App
上述在setState中没进行任何操作,但render依然会执行,而改成PureComponent时不会执行
PureComponent中render执行要求:
PureComponent在比较时底层会使用shallowEqual方法,shallowEqual是一种浅层比较,如果比较的两个对象是同一个,则不会调用render,即使this.state发生了改变,也不会调用render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 constructor ( ){ super () this .state ={ books :[ {name :"aaa" ,count :1 }, {name :"bbb" ,count :1 }, {name :"ccc" ,count :1 }, {name :"ddd" ,count :1 } ] } } addNewBook ( ){ const newBook = {name :"eee" ,count :1 } this .state .books .push (newBook) const books = this .state .books this .setState ({ books :books }) }
如果要想在这种情况下调用render,可以利用books创建一个新的对象,用这个对象去更改state,在这种情况下,即使未更改this.state的,也会调用render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 constructor ( ){ super () this .state ={ books :[ {name :"aaa" ,count :1 }, {name :"bbb" ,count :1 }, {name :"ccc" ,count :1 }, {name :"ddd" ,count :1 } ] } } addNewBook ( ){ const books = [...this .state .books ] this .setState ({ books :books }) }
函数组件memo 函数由于不能继承于PureComponent,可以使用memo替代,即用memo包裹原来的函数组件
获取原生DOM或获取组件 在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:
管理焦点,文本选择或媒体播放;
触发强制动画;
集成第三方 DOM 库;
我们可以通过refs获取DOM;
通过refs获取DOM的方式:
传入字符串: 使用时通过 this.refs.传入的字符串 格式获取对应的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React from "react" ;class App extends React.PureComponent { constructor ( ){ super () } addNewBook ( ){ console .log (this .refs .button ) } render ( ){ return ( <div > <button onClick ={e => this.addNewBook()} ref="button">查看DOM</button > </div > ) } } export default App
传入一个对象: 通过React.createRef()方式创建一个对象,使用使用对象的current属性来访问DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .titleRef = React .createRef () } addNewBook ( ){ console .log (this .titleRef .current ) } render ( ){ return ( <div > <button onClick ={e => this.addNewBook()} ref={this.titleRef}>查看DOM</button > </div > ) } } export default App
传入一个函数: 该函数会在DOM被挂载时回调,回调函数会传入一个元素对象,我们可以访问并保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .titleRef = React .createRef () } render ( ){ return ( <div > <button ref ={(e) => {console.log(e)}}>查看DOM</button > </div > ) } } export default App
通过refs获取组件的方式: 针对类组件:
上面的ref同样可以加在子组件上,获取得到子组件的实例,就可以在父组件中获取子组件对子组件的函数、变量进行操作
针对函数组件:
函数组件由于没有实例,是不可能获取到它的
但是在某些情况,想获取得到函数实例中的某个DOM元素,可以使用foreardRef
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 import React from "react" ;import { forwardRef } from "react" ;const HelloWorld = forwardRef (function (props, ref ){ return ( <div > <h1 ref ={ref} > Hello World</h1 > </div > ) }) class App extends React.PureComponent { constructor ( ){ super () this .HelloWorldRef = React .createRef () } render ( ){ return ( <div > <HelloWorld ref = {this.HelloWorldRef}/ > <button onClick ={() => {console.log(this.HelloWorldRef.current)}}>查看DOM</button > </div > ) } } export default App
受控组件与非受控组件
HTML中,表单元素(input、textarea、select)通常会自己维护state并根据用户的输入进行更新
React中,可变状态通常保存在组件的state属性中,并且只能通过setState来更新
在JSX中,如果给表单元素设置了value属性,显示this.state.value的值,那么表单元素就不会自己维护state,其state来源于React的state,因此当设置了value属性后,input元素输入后输入框里的东西不会变化,相当于未输入
此时只能给input元素添加onChange(e)监听,从而在监听函数中使用输入的值在setState中更改value的值,使得页面刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React from "react" ;import { forwardRef } from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .state ={ username :"oww" } } inputChange (e ){ this .setState ({username :e.target .value }) } render ( ){ const {username} = this .state return ( <div > <input type ="text" name ="username" value ={username} onChange ={(e) => {this.inputChange(e)}}/> </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 import React from "react" ;import { forwardRef } from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .state ={ username :"oww" , password :"" } } inputChange (e ){ this .setState ({[e.target .name ]:e.target .value }) } render ( ){ const {username,password} = this .state return ( <div > <input type ="text" value ={username} name ="username" onChange ={(e) => {this.inputChange(e)}}/> <input type ="text" value ={password} name ="password" onChange ={(e) => {this.inputChange(e)}}/> </div > ) } } export default App
checkbox表单多选监听 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 import React from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .state ={ hobbies :[ {id :"sing" ,text :"唱" ,checked :false }, {id :"jump" ,text :"跳" ,checked :false }, {id :"rap" ,text :"rap" ,checked :false } ] } } inputChange (e,index ){ const hobbies = [...this .state .hobbies ] hobbies[index].checked = e.target .checked this .setState ({ hobbies :hobbies }) } render ( ){ const {hobbies} = this .state return ( <div > <h2 > 爱好列表</h2 > {hobbies.map((item, index)=>{ return ( <label htmlFor ={item.id} key ={item.id} > <input type ="checkbox" id = {item.id} checked ={item.checked} onChange ={(e) => this.inputChange(e,index)}/>{item.text} </label > ) })} </div > ) } } export default App
select多选 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 import React from "react" ;class App extends React.PureComponent { constructor ( ){ super () this .state ={ fruits :[ {id :"apple" ,text :"苹果" }, {id :"banana" ,text :"香蕉" }, {id :"orange" ,text :"橘子" } ], checked :["apple" ] } } selectChange (e ){ let checked = Array .from (e.target .selectedOptions ).map (item => item.value ) this .setState ({ checked :checked }) } render ( ){ const {fruits,checked} = this .state return ( <div > <h2 > 爱好列表</h2 > <select multiple value ={checked} onChange ={e => this.selectChange(e)}> {fruits.map(item=>{ return <option value ={item.id} key ={item.id} > {item.text}</option > })} </select > </div > ) } } export default App
高阶组件 高阶函数: 满足下列一个条件
高阶组件(Higher-Order Components、HOC):
高阶组件的参数为组件,返回值为新组件的函数
高阶组件本身不是一个组件,而是一个函数
高阶组件不是React的API,而是基于React组合特性而形成的一种设计模式
高阶组件应用——props的增强 HOC代码: 高阶函数包裹代码
例如本代码可以将用户信息封装传给子组件,不需要像上面的父向子传值,在子组件中写其他代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { PureComponent } from "react" function enhanceProps (WrapperCpn, otherProps ){ class UserComponent extends PureComponent { constructor ( ){ super () this .state = { userinfo :{ username :"ouwenwu" , age :22 } } } render ( ){ return <WrapperCpn {...this.state.userinfo } {...otherProps }/> } } return UserComponent } export default enhanceProps
父组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;import enhanceProps from "./HOC/HOC" ;class App extends React.PureComponent { constructor ( ){ super () } render ( ){ const EnhancePropsCpn = enhanceProps (ChildComponent ,{color :"blue" }) return ( <div > <EnhancePropsCpn /> </div > ) } } export default App
子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , { Component } from 'react' export class ChildComponent extends Component { constructor ( ){ super () } render ( ) { return ( <div > <h2 style ={{color:this.props.color}} > username:{this.props.username}</h2 > </div > ) } } export default ChildComponent
结果:
高阶组件应用——Context的共享 HOC代码: HOC可以返回类组件也可以返回函数组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { PureComponent } from "react" import Context1 from "../context/Context1" function enhanceProps (WrapperCpn ){ return props => { return ( <Context1.Consumer > { value=>{ return <WrapperCpn {...value } {...props }/> } } </Context1.Consumer > ) } } export default enhanceProps
父组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from "react" ;import ChildComponent from "./component/ChildComponent" ;import Context1 from "./context/Context1" ;import enhanceProps from "./HOC/HOC" ;class App extends React.PureComponent { constructor ( ){ super () } render ( ){ const EnhancePropsCpn = enhanceProps (ChildComponent ) return ( <div > <Context1.Provider value ={{color: "red "}}> <EnhancePropsCpn /> </Context1.Provider > </div > ) } } export default App
子组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , { Component } from 'react' export class ChildComponent extends Component { constructor ( ){ super () } render ( ) { return ( <div > <h2 style ={{color:this.props.color}} > username:</h2 > </div > ) } } export default ChildComponent
结果:
注:可以看出对Context不再需要在每一个子组件中写之前提到的获取Context的方法,只需要将所有需要该Context的子组件包裹在高阶组件之中
高阶组件应用——渲染判断定权 与上面的主要差距为,会对传入的参数进行判断来决定返回哪个子组件
高阶组件应用——生命周期劫持 在高阶组件中劫持子组件的生命周期,在自己的生命周期中完成逻辑
总结 高阶组件的主要作用是提高代码的复用性。提高组件之间的复用
缺陷:
HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将产生非常多的嵌套
HOC可以劫持props,在不遵从约定的情况下可能造成冲突