一、组件的定义
1、组件最好写在独立的js文件中
2、组件名必须以大写字母开头,以此区分组件和普通的react元素
3、在当前js文件中定义并渲染了该组件,若只需要在该页面使用组件,则组件无需export。 如果将组件抽离成了独立的js文件,则需要export。(在其他页面需导入该组件后使用
4、如果组件的返回值为null,表示不渲染任何内容
1. 函数组件
函数组件必须有返回值,表示该组件的结构
import React from 'react'
import ReactDOM from 'react-dom'
// function Hello() {
// console.log(this) //undefined
/*this本质上是指向它的调用者, this是在函数运行时才绑定, JS里普通函数都是window调用的, 所以指向window, 经由babel编译后开启了严格模式 this是undefined。*/
// return (
// <div>函数组件</div>
// )
// }
const Hello = () => <div>使用箭头函数创建组件</div>
函数组件里没有this,且没有生成实例对象,不存在使用this.state获取状态。
当使用
<组件名/>或者执行了ReactDOM.render()将组件渲染到页面之后:1、React解析组件标签,找到了该组件
2、发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,呈现在页面中。
2. 类组件
使用ES6语法的class创建的组件
- 类组件应该继承React.Component父类,从而可以使用父类中提供的方法或者属性
- 类组件必须提供 render 方法
- render方法中必须要有return返回值,一般返回JSX结构表示该组件的结构
创建class类,继承React.Component,在里面提供render方法,在return里面返回渲染内容
import React from 'react'
import ReactDOM from 'react-dom'
class Hello extends React.Component {
render() {
console.log('render中的this:',this)
return (
<div>创建类组件</div>
)
}
}
render方法是放在Hello这个类的原型对象上,供实例使用。
render()里的this指向问题:指向Hello这个类的实例对象,即组件实例对象。
这些常用的属性,是从React.Component这个类继承而来。
当使用
<组件名/>或者执行了ReactDOM.render()将组件渲染到页面之后:1、React解析组件标签,找到了该组件
2、发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
3、将render返回的虚拟DOM转为真实DOM,呈现在页面中。
3. 抽离成单独的JS文件
每个组件作为一个独立的个体,一般都会放到一个单独的JS文件中
- 创建Hello.js
- 在Hello.js 中导入React
- 创建组件
- 在Hello.js中导出
import React from 'react'
export default class Hello extends React.Component {
render(){
return (
<div></div>
)
}
}
import React from 'react'
// 创建组件
class Hello extends React.Component {
render() {
return (
<div></div>
)
}
}
// 导出组件
export default Hello
使用该组件:
- 在index.js中导入Hello组件
- 用组件名作为标签名,渲染到页面
import React from 'react'
import ReactDOM from 'react-dom'
// 导入Hello组件
import Hello from './Hello'
// 渲染组件
class Index extends React.Component {
render(){
return (
<Hello></Hello>
)
}
}
二、有状态组件和无状态组件
函数组件又叫做 无状态组件,类组件又叫做 有状态组件
- 状态(state) 即数据
- 函数组件没有自己的状态,只负责数据展示(除非使用
react hooks) - 类组件有自己的状态,负责更新UI,让页面动起来
1. 类组件中 state的使用
- 状态(state)是组件内部的私有数据,只能在组件内部使用(和props区分,props是从外界传入组件的数据)
- state的值是对象,在对象里可以放多个数据
- state是在组件实例对象上的其中一个属性,所以通过
this.state来访问 - 第二种初始化方式-原理:在类中可以直接写赋值语句,所以不需要传实参的有固定值的属性,可以直接写(无需在constructor里写
this.属性 = 固定值)
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
/* constructor(props) {
//子类有继承行为且写了构造器,必须写super
super(props)
// 第一种初始化方式
this.state = {
count: 0
}
} */
// 第二种初始化方式:使用 ES6里面的简化语法初始化 state(推荐)
state = {
count: 10
}
render() {
// const {count} = this.state
return (
<div>
<h1>计数器:{ this.state.count }</h1>
</div>
)
}
}
2. 类组件中使用 setState() 修改状态
- 语法:
this.setState({要修改的数据}),只会更新对应属性的值,不会影响state中其他属性的值 - 注意:不要直接修改state中的值,这是错误的
- setState() 作用:1.修改 state 2.更新UI(指当状态变了后,最新的状态值会自动渲染到页面中,因为React底层会有监听,一旦调用了setState导致了数据的变化,就会重新调用一次render方法,重新渲染当前组件)
- 在这个过程中,constructor只在生成组件实例的时候调用了1次;render()调用了n+1次,第一次和每次使用setState都会调用
- 思想:数据驱动视图(数据先改变,驱动页面进行更新)
import React from 'react'
import ReactDOM from 'react-dom'
export default class App extends React.Component {
// 初始化state
state = {
count:1
}
render(){
return (
<div>
<div>计数器 :{this.state.count}</div>
<button onClick={() => {
this.setState({
count: this.state.count+1
})
}}>+1</button>
</div>
)
}
}
三、组件间通讯
1. 通过props通讯
- 组件是封闭的,要接受外部数据得通过props来实现
- props的作用:接收传递给组件的数据
- 传递数据:在页面使用组件时,给组件添加自定义属性。props是一个对象,可以往其中传入多个属性
<Hello name="rose" age={19} /> - 接收(获取)数据:函数组件通过 参数 props接收数据,类组件通过 this.props接收数据
(1)函数组件
const Hello = props => {
console.log(props)
return (
<div>
<h1>props:{props.name}</h1> //rose
</div>
)
}
(2)类组件
class Hello extends React.Component {
render() {
const { age } = this.props
// console.log(this.props)
return (
<div>
<h1>props: {age}</h1> //19
</div>
)
}
}
(3)特点
- 可以给组件传递任意类型的数据
- props是只读属性,不能对值进行修改
- 注意:使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props,其他的地方是可以拿到的(比如在render里)
(4)举例:
使用该组件时,传入值
//可以给组件传递任意类型的数据
<Hello
name="rose"
//传递非字符串数据 需要加{}
age={19}
colors={['red', 'green', 'blue']}
fn={() => console.log('这是一个函数')}
tag={<p>这是一个p标签</p>} //把JSX作为tag属性的值传递给组件
/>
在组件中,处理接收到的数据
import React from 'react'
import ReactDOM from 'react-dom'
// 类组件:
class Hello extends React.Component {
// 推荐使用 props 作为 constructor 的参数,这样在 constructor 里可以直接用 props,或者用 this.props也可以
constructor(props) {
super(props)
console.log('在构造函数中,可以使用props,也可以使用this.props:',props)
}
this.props.fn() //调用从外界传入本组件的函数
// 错误演示!!!不能修改props的值
// this.props.name = 'tom'
render() {
console.log('render里只能用this.props:', this.props)
return (
<div>
<h1>props:{this.props.age}</h1>
</div>
)
}
}
(5)批量传递props
组件:
class Hello extends React.Component {
render() {
const { age, name } = this.props
return (
<div>
<h1>{age}</h1>
<h1>{age}</h1>
</div>
)
}
}
使用组件:
const p = { age:'19', name:'一粒沙' }
<Hello {...p}/>
注意:...运算符不能用于展开对象,只是由于react+babel的存在,让...p可以用于给标签传递属性
组件间用props通讯的三种情形
(1)父(类组件) —> 子(函数组件)
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 父组件
class Parent extends React.Component {
// 1. 父组件提供要传递的数据
state = {
lastName: '王'
}
render() {
return (
<div className="parent">
// 2. 使用子组件时,通过给子组件标签添加属性的方式,将数据传入子组件
<Child name={this.state.lastName} />
</div>
)
}
}
// 子组件
const Child = props => {
// 3. 子组件中通过props接收并使用从父组件传过来的数据
console.log('子组件接收到的所有数据的集合:', props)
return (
<div className="child">
<p>子组件使用从父组件传来的数据:{props.name}</p>
</div>
)
}
(2)子(类组件) —> 父(类组件)
-
父组件提供回调函数,用来接收数据(谁要接收数据谁就提供回调函数)
-
子组件调用,将要传递的数据作为回调函数的实参。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 父组件
class Parent extends React.Component {
state = {
parentMsg: ''
}
// 1、父组件提供回调函数,形参取名data,用来接收数据
getChildMsg = data => {
console.log('接收到子组件中传递过来的数据:', data)
// 4、使用从子组件中传递过来的数据:把父组件的数据parentMsg进行更新
this.setState({
parentMsg: data
})
}
render() {
return (
<div className="parent">
父组件:{this.state.parentMsg} //渲染从子组件传过来的数据
// 2、 使用子组件时,将该函数作为属性的值,传递给子组件
<Child getMsg={this.getChildMsg} />
</div>
)
}
}
// 子组件
class Child extends React.Component {
state = {
msg: '111'
} //要传递给父组件的数据
handleClick = () => {
// 3、子组件通过props调用父组件中传递过来的回调函数(将子组件的数据作为实参,传递给父组件)
this.props.getMsg(this.state.msg)
}
render() {
return (
<div className="child">
<button onClick={this.handleClick}>给父组件传递数据</button>
</div>
)
}
}
此时回调函数是用箭头函数(推荐使用)写的,箭头函数不会绑定this,所以会向外一层去寻找,外层是render方法,在render方法里面的this指向的是当前实例对象,所以调用回调函数时的this 是指向组件实例的。
(3)兄弟组件间传值
状态提升:将共享状态(二者间需要通讯的数据)提升到最近的公共父组件中,由公共父组件管理这个状态
子组件B把数据传递给子组件A:
import React from 'react'
import ReactDOM from 'react-dom'
// 父组件
class Counter extends React.Component {
// 1. 公共父组件提供要共享的状态
state = {
count: 0
}
// 2. 公共父组件提供操作共享状态的方法
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
// 把父组件的数据 通过给子组件添加属性 传给ChildA
<ChildA count={this.state.count} />
// ChildB通过回调函数,修改被共享的状态
<ChildB onIncrement={this.onIncrement} />
</div>
)
}
}
// 3. 要通讯的子组件A只需通过props接收状态(父传子),子组件B通过props调用修改状态的方法(子传父)
const ChildA = props => {
//接收数据
return <h1>计数器:{props.count}</h1>
}
const ChildB = props => {
//调用传入本组件的函数onIncrement
return <button onClick={() => props.onIncrement()}>+1</button>
}
props进阶
(1)children属性
- children属性: 表示组件标签的子节点,使用某组件时,该组件名包裹着的 就是它的子节点。当组件标签有子节点时,props就会自动有该属性
- children属性与普通的props一样,值可以是任意值(如 文本、react元素、组件、甚至是函数)
children为:函数子节点
使用该组件时:
<App>
{
() => console.log('这是一个函数子节点')
}
</App>
定义组件时:
import React from 'react'
import ReactDOM from 'react-dom'
const App = props => {
console.log(props)
props.children() //调用了本组件的函数子节点
return (
<div>
<h1>组件标签的子节点:</h1>
{/* {props.children} 子节点如果是函数,不能直接打印,只能调用*/}
</div>
)
}
children为:jsx(如p标签)或组件(如button组件)
使用该组件时:
const Test = () => <button>我是button组件</button>
<App>
{/* <p>我是子节点,是一个p标签</p> */}
<Test />
</App>
定义组件时:
import React from 'react'
import ReactDOM from 'react-dom'
const App = props => {
console.log(props)
return (
<div>
<h1>组件标签的子节点:</h1>
{props.children}
</div>
)
}
children为:文本节点“我是子节点”
使用该组件时:
<App>我是子节点</App>
(2)props校验
允许在定义组件的时候,指定props的类型、格式等
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,
就是说组件调用者可能不知道组件封装着需要什么样的数据。如果传入的数据不对,可能会导致报错
开启props校验的作用:使用组件时如果传递的数据不对,会报明确的错误提示
使用步骤
- 安装包 prop-types (
yarn add prop-types | npm i props-types) - 导入prop-types 包
- 通过propTypes对象,给组件的props添加校验规则
组件名.propTypes={}
举例:
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
const App = props => {
const arr = props.colors
const lis = arr.map((item, index) => <li key={index}>{item}</li>)
return <ul>{lis}</ul>
}
// 添加props校验
App.propTypes = {
//约定该属性为array类型
//如果类型不对,则报出明确错误,便于分析错误原因
colors: PropTypes.array
}
使用组件时,传入数据:
<App colors={['red', 'blue']} />
常见的校验规则
- 常用的类型:
array、bool、func、number、object、string - React元素类型:
element - 必填项:
isRequired - 特定结构的对象:
shape({})
举例:
添加props校验:
属性 a 的类型: 数值(number)
属性 fn 的类型: 函数(func)并且为必填项
属性 tag 的类型: React元素(element)
属性 filter 的类型: 对象({area: '上海', price: 1999})
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
const App = props => {
return (
<div>
<h1>props校验:</h1>
</div>
)
}
App.propTypes = {
a: PropTypes.number,
//在类型后加上了.isRequired,表示该类型必选
fn: PropTypes.func.isRequired,
tag: PropTypes.element,
//特定结构的对象
filter: PropTypes.shape({
area: PropTypes.string,
price: PropTypes.number
})
}
使用组件时,函数是必传项:
<App fn={() => {}} />
(3)props的默认值
- 场景举例:分页组件 -> 每页显示条数
- 作用:在自己未传入props时生效
import React from 'react'
import ReactDOM from 'react-dom'
const App = props => {
console.log(props)
return (
<div>
<h1>此处展示props的默认值:{props.pageSize}</h1>
</div>
)
}
//设置默认值
App.defaultProps = {
pageSize: 10
}
使用组件时:
//未传入props,以默认值为准
// <App />
//传入了props,以传入值为准
<App pageSize={20} />
2. 使用Context通讯
如果两个组件相隔层级比较多(嵌套多层,比如爷爷传递数据给孙子),使用Context实现组件间通讯更方便(如果用props,需要一层层传递和接收,较为繁琐)
作用: 跨组件传递数据
函数组件
类组件
-
调用
React.createContext()创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件const { Provider, Consumer } = React.createContext() -
使用Provider组件作为父节点(一般在最外层设置)
-
设置value属性,表示要传递的数据
<Provider value="pink">
<div className="app">
<Node />
</div>
</Provider>
- 调用Consumer组件接收数据:
- 哪一层想要接收数据,就用Consumer进行包裹。
- 其中的回调函数的参数data就是接收到的传递过来的值pink,回调函数的返回值是要渲染的JSX结构
举例:
组件间的嵌套关系:App—>Node—>SubNode—>Child
需求:通过APP组件把数据传递给Child组件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
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>
)
}
四、refs的使用
组件内的标签可以定义ref属性来标识自己
字符串形式的ref (已废弃):
其值"input1",就是实例对象的ref属性值中的一个key,该key的value就是当前标识了ref为"input1"的这个节点
回调函数形式的ref(较早版本的react):
其参数就是当前标识了ref的这个节点
class Demo extends React.Component{
showData = ()=>{
// console.log('箭头函数的this继承外界作用域,即组件对象实例',this)
//字符串形式的ref(已废弃)
// const { input1 } = this.refs
// console.log('拿到的是虚拟DOM转成真实DOM之后的节点',input1)
// alert(input1.value)
//2、从实例自身上取input2
const { input2 } = this
alert(input2.value)
}
render(){
return(
<div>
<input ref="input1" type="text" placeholder="字符串形式的ref"/>
<button onClick={this.showData}>提示输入的数据</button>
{/* 1、参数c拿到的就是当前标识了ref的这个节点,把该节点放在了组件实例自身上,取名叫input2*/}
<input ref={c => this.input2 = c} type="text" placeholder="回调函数形式的ref"/>
</div>
)
}
}
PS:在JSX里的注释方式,先写{},表明其中的是js表达式,然后再/* */
关于回调refs的说明:官方文档
class Demo extends React.Component{ state = {isHot:false} showInfo = ()=>{ const {input1} = this alert(input1.value) } changeWeather = ()=>{ //获取原来的状态 const {isHot} = this.state //更新状态 this.setState({isHot:!isHot}) } saveInput = (c)=>{ this.input1 = c; console.log('参数c:',c); } render(){ const {isHot} = this.state return( <div> <h2>今天天气很{isHot ? '炎热':'凉爽'}</h2> {/* 用内联方式写回调函数:*/} {/*<input ref={(c)=>{this.input1 = c;console.log('参数c:',c);}} type="text"/><br/><br/>*/} {/*将 ref 的回调函数定义成 class 的绑定函数的方式*/} <input ref={this.saveInput} type="text"/><br/><br/> <button onClick={this.showInfo}>提示输入的数据</button> <button onClick={this.changeWeather}>更新状态</button> </div> ) } }如果
ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数 DOM 元素。所以:页面初次render:
参数c:<input type="text">后续更新状态时调用render:
参数c:null 参数c:<input type="text">解决方式:将 ref 的回调函数定义成 class 的绑定函数的方式
ref={this.saveInput}把函数放在了类实例对象的自身上无关紧要,写成内联也可以。
使用createRef()创建的ref属性:React 16.3 版本引入
调用React.createRef(),返回一个容器,其中的current属性用于存储被ref标识的节点
class Demo extends React.Component{
//给Demo类的实例对象上追加一个属性a,值为1
a = 1
//给Demo类的实例对象追加属性,值为一个容器。
myRef = React.createRef()
//React.createRef()创建的容器,只能专用(存一个节点)
myRef2 = React.createRef()
showData = ()=>{
console.log("容器myRef:",this.myRef) // {current:input} current是固定属性
alert(this.myRef.current.value);
}
showData2 = ()=>{
alert(this.myRef2.current.value);
}
render(){
return(
<div>
{/*把当前ref所标识的input节点,存储到了myRef这个容器里*/}
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点击按钮提示数据</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}