React元素渲染
React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的 UI。
更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()。
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
注意:
在实践中,大多数 React 应用只会调用一次 ReactDOM.render()。在下一个章节,我们将学习如何将这些代码封装到有状态组件中
React 只更新它需要更新的部分
按照上面的例子来说,页面上发生变化的只是h2标签里面的时间,别的dom元素是不变的(可以右键检查,进行查看)
React组件
- 组件是React的一等公民,使用React就是在用组件
- 组件表示页面中的部分功能
- 组合多个组件实现完整的页面功能
- 特点:可复用、独立、可组合
组件的创建方式
函数创建组件
- 函数组件:使用JS的函数创建组件
- 约定1:函数名称必须以大写字母开头
- 约定2:函数组件必须有返回值,表示该组件的结构
- 如果返回值为null,表示不渲染任何内容
示例demo
编写函数组件
function Hello() {
return (
<div>这是第一个函数组件</div>
)
}
利用ReactDOM.render()进行渲染
ReactDOM.render(<Hello />,document.getElementById('root'))
类组件(★★★)
- 使用ES6语法的class创建的组件
- 约定1:类名称也必须要大写字母开头
- 约定2:类组件应该继承React.Component父类,从而可以使用父类中提供的方法或者属性
- 约定3:类组件必须提供 render 方法
- 约定4:render方法中必须要有return返回值
示例demo
创建class类,继承React.Component,在里面提供render方法,在return里面返回内容
class Hello extends React.Component{
render(){
return (
<div>这是第一个类组件</div>
)
}
}
通过ReactDOM进行渲染
ReactDOM.render(<Hello />,document.getElementById('root'))
抽离成单独的JS文件(★★★)
- 思考:项目中组件多了之后,该如何组织这些组件?
- 选择一:将所有的组件放在同一个JS文件中
- 选择二:将每个组件放到单独的JS文件中
- 组件作为一个独立的个体,一般都会放到一个单独的JS文件中
示例demo
- 创建Hello.js
- 在Hello.js 中导入React,创建组件,在Hello.js中导出
import React from 'react'
export default class extends React.Component {
render(){
return (
<div>单独抽离出来的 Hello</div>
)
}
}
- 在index.js中导入Hello组件,渲染到页面
import Hello from './js/Hello'
ReactDOM.render(<Hello />,document.getElementById('root'))
//可以使用这样的方式传递实参const element =<Comment name="h" age="12" arr={[1,2,3,1]} props={obj}></Comment>
组合组件
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。
例如,我们可以创建一个可以多次渲染 Welcome 组件的 App 组件:
function Welcome(props) {
//组件内部通过props接收所有传递进来的参数,
//例如这个props接收的就是{name: "zs"} 这是一个对象
return <h1>Hello, {props.name}</h1>;
}
//可以使用这样的方式传递实参const element =<Comment name="h" age="12" arr={[1,2,3,1]} props={obj}></Comment>
function App() {
return (
<div>
<Welcome name="Sara" />
//可以通过这样的方式进行向组件内传值
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
通常来说,每个新的 React 应用程序的顶层组件都是 App 组件。但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像 Button 这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。
提取组件
将组件拆分为更小的组件。
例如,参考如下 Comment 组件:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
该组件用于描述一个社交媒体网站上的评论功能,它接收 author(对象),text (字符串)以及 date(日期)作为 props。
该组件由于嵌套的关系,变得难以维护,且很难复用它的各个部分。因此,让我们从中提取一些组件出来。
首先,我们将提取 Avatar 组件:
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar 不需知道它在 Comment 组件内部是如何渲染的。因此,我们给它的 props 起了一个更通用的名字:user,而不是 author。
我们建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。
我们现在针对 Comment 做些微小调整:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
接下来,我们将提取 UserInfo 组件,该组件在用户名旁渲染 Avatar 组件:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
进一步简化 Comment 组件:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
最初看上去,提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(Button,Panel,Avatar),或者组件本身就足够复杂(App,FeedStory,Comment),那么它就是一个可提取出独立组件的候选项。
Props 的只读性
组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。来看下这个 sum 函数:
function sum(a, b) {
return a + b;
}
这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
相反,下面这个函数则不是纯函数,因为它更改了自己的入参:
function withdraw(account, amount) {
account.total -= amount;
}
React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
(自己试验了一下:你传进去的如果是简单数据类型的话,你更改就会报错。但是,如果你传进去的是复杂数据类型的话,你传进去就会对数据发生改变)
当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在下一章节中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
props进阶
基本使用
- 组件时封闭的,要接受外部数据应该通过props来实现
- props的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
‘例如:
ReactDOM.render(<Hello name="zs"></Hello>,document.getElementById('root'))
- 接收数据:函数组件通过 参数 props接收数据,类组件通过 this.props接收数据
function Hello(props){
console.log(props);
return (<div>接收到的数据:{props}</div>)
}
- 函数组件获取
class Hello extends React.Component{
render(){
return (<div>类接收组件的值:{this.props.name}</div>)
}
}
特点
- 可以给组件传递任意类型的数据
- props是只读属性,不能对值进行修改
- 注意:使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props,其他的地方是可以拿到的
组件通讯的三种方式(★★★)
父组件传递数据给子组件
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为state中的数据
- 子组件中通过props接收父组件中传递的数据
// 父组件
class Father extends React.Component{
state={
familyName:'王',
momey:80000
}
render(){
return (<div>传递数据给子组件:<Child name={this.state}></Child></div>)
}
}
// 子组件
class Child extends React.Component{
render(){
console.log(this.props);
return (<h2>接收父组件的数据:{this.props.name.momey}</h2>)
}
}
// 组件传值:给组件标签添加属性(函数和类的方式都是一样的)
ReactDOM.render(<Father></Father>,document.getElementById('root'))
注意:无论父组件传来的是什么样地值,子组件都以一个name的对象进行接收
子组件传递数据给父组件
- 利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
- 父组件提供一个回调函数,用来接收数据
- 将该函数作为属性的值,传递给子组件
- 子组件通过props调用回调函数
// 1.父组件定义一个回调函数进行值的接收
// 2.父组件在嵌套的子组件内进行一个属性传值,把当前的回调函数传入子组件
// 3.子组件接收回调函数,并把要传递的数据作为参数传递给回调函数
// 父组件
class Father extends React.Component{
getChildData=(msg)=>{
console.log('接收子组件的数据',msg);
}
render(){
return (<div>传递数据给子组件:<Child getMsg={this.getChildData}></Child></div>)
}
}
// 类子组件
// class Child extends React.Component{
// state={ChildData:'孩子的数据'}
// handleClick=()=>{
// return this.props.getMsg(this.state.ChildData)
// }
// render(){
// console.log(this.props);
// return (<button onClick={this.handleClick}>接收父组件的数据:</button>)
// }
// }
// 函数子组件
function Child(props){
var state={ChildData:'孩子的数据'}
props.getMsg(state.ChildData)
return (<h1>接收父组件的数据:</h1>)
}
// 组件传值:给组件标签添加属性(函数和类的方式都是一样的)
ReactDOM.render(<Father></Father>,document.getElementById('root'))
兄弟组件传递
- 将共享状态(数据)提升到最近的公共父组件中,由公共父组件管理这个状态
- 这个称为状态提升
- 公共父组件职责:1. 提供共享状态 2.提供操作共享状态的方法
- 要通讯的子组件只需要通过props接收状态或操作状态的方法
示例demo
- 定义布局结构,一个Counter里面包含两个子组件,一个是计数器的提示,一个是按钮
class Counter extends React.Component {
// 提供共享的状态
state = {
count: 0
}
// 提供共享方法
onIncrement = (res) => {
// 只要第二个子组件调用了这个函数,就会执行里面代码
this.setState({
count: this.state.count + res
})
}
render() {
return (<div>
{/* 把状态提供给第一个子组件 */}
<Child1 count={this.state.count}/>
{/* 把共享方法提供给第二个子组件 */}
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
}
class Child1 extends React.Component {
render() {
return (
<h1>计数器:{this.props.count}</h1>
)
}
}
class Child2 extends React.Component {
handleClick = () => {
// 这里一旦调用,就会执行父组件里面 onIncrement函数
this.props.onIncrement(2)
}
render() {
return (
<button onClick={this.handleClick}>+</button>
)
}
}
ReactDOM.render(<Counter></Counter>,document.getElementById('root'))
Context(★★★)
如果出现层级比较多的情况下(例如:爷爷传递数据给孙子),我们会使用Context来进行传递
作用: 跨组件传递数据
使用步骤
-
调用 React.createContext() 创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件
-
使用Provider 组件作为父节点
-
设置value属性,表示要传递的数据
-
哪一层想要接收数据,就用Consumer进行包裹,在里面回调函数中的参数就是传递过来的值
小结
- 如果两个组件相隔层级比较多,可以使用Context实现组件通讯
- Context提供了两个组件:Provider 和 Consumer
- Provider组件: 用来提供数据
- Consumer组件: 用来消费数据
props进阶
children属性
- children属性: 表示组件标签的子节点,当组件标签有子节点时,props就会有该属性
- children属性与普通的props一样,值可以使任意值(文本、react元素、组件、甚至是函数)
props校验(★★★)
-
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,简单来说就是组件调用者可能不知道组件封装着需要什么样的数据
-
如果传入的数据不对,可能会导致报错
-
关键问题:组件的使用者不知道需要传递什么样的数据
-
props校验:允许在创建组件的时候,指定props的类型、格式等
-
作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤
- 安装包 prop-types (yarn add prop-types | npm i props-types)
- 导入prop-types 包
import PropTypes from 'prop-types';
- 使用组件名.propTypes={} 来给组件的props添加校验规则
- 校验规则通过PropTypes对象来指定
-当校验不通过的时候,不影响渲染,但是后台会出警告
import propTypes from 'prop-types';
import React from 'react'
//propTypes大写P会报错,不知道为什么,但是换成小写就好了
function APP(Props){
console.log(Props);
console.log(typeof Props.color);
return (
<h1>hello ,{Props.color}</h1>
)
}
APP.propTypes={
color:propTypes.array
}
function Hello(){
return(
<APP color={[1,2,3,4,3]}></APP>
// {[1,2,3,4,3]}
)
}
ReactDOM.render(
<Hello></Hello>,document.getElementById('root')
)
常见的约束规则
- 创建的类型: array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象: shape({})
- 更多的约束规则
props的默认值
-
场景:分页组件 -> 每页显示条数
-
挂载
-
顺序:constructor=>render=>componentDidMount(在render执行后立即执行)
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
constructor()//一般可以放一些初始化的操作
constructor(props)
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。
通常,在 React 中,构造函数仅用于以下两种情况:
通过给 this.state 赋值对象来初始化内部 state。
为事件处理函数绑定实例
在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state:
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值,你应使用 this.setState() 替代。
要避免在构造函数中引入任何副作用或订阅。如遇到此场景,请将对应的操作放置在 componentDidMount 中。
注意
避免将 props 的值复制给 state!这是一个常见的错误:
constructor(props) {
super(props);
// 不要这样做
this.state = { color: props.color };
}
如此做毫无必要(你可以直接使用 this.props.color),同时还产生了 bug(更新 prop 中的 color 时,并不会影响 state)。
只有在你刻意忽略 prop 更新的情况下使用。此时,应将 prop 重命名为 initialColor 或 defaultColor。必要时,你可以修改它的 key,以强制“重置”其内部 state。
请参阅关于避免派生状态的博文,以了解出现 state 依赖 props 的情况该如何处理。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date:new Date()};//直接在构造函数中为 this.state 赋值初始 state:
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(<Clock></Clock>,document.getElementById('root'))
static getDerivedStateFromProps()
render()
每次渲染就会触发,不论是开始时候渲染,还是中间渲染页面(注意:不能再调用setState()。因为只要渲染内容render就会触发,setState()既能更新状态又能更新UI,一更新UI,render就又会触发)
componentDidMount()//会在组件挂载后(插入 DOM 树中)立即调用
更新
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate(prevProps, prevState, snapshot)//会在更新后会被立即调用。首次渲染不会执行此方法。(相比较于render,晚于render
卸载
当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount()//会在组件卸载及销毁之前直接调用
错误处理
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
static getDerivedStateFromError()
componentDidCatch()
React事件处理(★★★)
事件绑定
- React事件绑定语法与DOM事件语法相似
- 语法:on+事件名称=事件处理函数,比如 onClick = function(){}
- 注意:React事件采用驼峰命名法
示例demo
export default class extends React.Component {
clickHandle(e){
console.log('点了')
}
render(){
return (
<div><button onClick = {this.clickHandle}>点我点我点我</button></div>
)
}
}
小结
- 在React中绑定事件与原生很类似
- 需要注意点在于,在React绑定事件需要遵循驼峰命名法
- 类组件与函数组件绑定事件是差不多的,只是在类组件中绑定事件函数的时候需要用到this,代表指向当前的类的引用,在函数中不需要调用this
事件对象
- 可以通过事件处理函数的参数获取到事件对象
- React中的事件对象叫做:合成事件
- 合成事件:兼容所有浏览器,无需担心跨浏览器兼容问题
- 除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()和 preventDefault()
- 如果你想获取到原生事件对象,可以通过 nativeEvent 属性来进行获取
示例demo
export default class extends React.Component {
clickHandle(e){
// 获取原生事件对象
console.log(e.nativeEvent)
}
render(){
return (
<div><button onClick = {this.clickHandle}>点我点我点我</button></div>
)
}
}
支持的事件(有兴趣的课下去研究)
- Clipboard Events 剪切板事件
- 事件名 :onCopy onCut onPaste
- 属性 :DOMDataTransfer clipboardData
- compositionEvent 复合事件
- 事件名: onCompositionEnd onCompositionStart onCompositionUpdate
- 属性: string data
- Keyboard Events 键盘事件
- 事件名:onKeyDown onKeyPress onKeyUp
- 属性: 例如 number keyCode 太多就不一一列举
- Focus Events 焦点事件 (这些焦点事件在 React DOM 上的所有元素都有效,不只是表单元素)
- 事件名: onFocus onBlur
- 属性: DOMEventTarget relatedTarget
- Form Events 表单事件
- 事件名: onChange onInput onInvalid onSubmit
- Mouse Events 鼠标事件
- 事件名:
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit
onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave
onMouseMove onMouseOut onMouseOver onMouseUp
- 事件名:
- Pointer Events 指针事件
- 事件名:
onPointerDown onPointerMove onPointerUp onPointerCancel onGotPointerCapture
onLostPointerCapture onPointerEnter onPointerLeave onPointerOver onPointerOut
- 事件名:
- Selection Events 选择事件
- 事件名:onSelect
- Touch Events 触摸事件
- 事件名:onTouchCancel onTouchEnd onTouchMove onTouchStart
- UI Events UI 事件
- 事件名: onScroll
- Wheel Events 滚轮事件
- 事件名:onWheel
- 属性:
number deltaMode
number deltaX
number deltaY
number deltaZ
- Media Events 媒体事件
- 事件名:
onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted
onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay
onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend
onTimeUpdate onVolumeChange onWaiting
- 事件名:
- Image Events 图像事件
- 事件名:onLoad onError
- Animation Events 动画事件
- 事件名:onAnimationStart onAnimationEnd onAnimationIteration
- Transition Events 过渡事件
- 事件名:onTransitionEnd
- Other Events 其他事件
- 事件名: onToggle
有状态组件和无状态组件
- 函数组件又叫做 无状态组件,类组件又叫做 有状态组件
- 状态(state) 即数据
- 函数组件没有自己的状态,只负责数据展示
- 类组件有自己的状态,负责更新UI,让页面动起来
State和SetState(★★★)
state基本使用
- 状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
- state的值是对象,表示一个组件中可以有多个数据
- 通过this.state来获取状态
示例demo
export default class extends React.Component {
constructor(){
super()
// 第一种初始化方式
this.state = {
count : 0
}
}
// 第二种初始化方式
state = {
count:1
}
render(){
return (
<div>计数器 :{this.state.count}</div>
)
}
}
setState() 修改状态
- 状态是可变的
- 语法:this.setState({要修改的数据})
- 注意:不要直接修改state中的值,这是错误的
- setState() 作用:1.修改 state 2.更新UI
- 思想:数据驱动视图
示例demo
export default class extends React.Component {
// 第二种初始化方式
state = {
count:1
}
render(){
return (
<div>
<div>计数器 :{this.state.count}</div>
<button onClick={() => {
this.setState({
count: this.state.count+1
})
}}>+1</button>
</div>
)
}
}
小结
- 修改state里面的值我们需要通过 this.setState() 来进行修改
- React底层会有监听,一旦我们调用了setState导致了数据的变化,就会重新调用一次render方法,重新渲染当前组件