认识组件化
1.组件化思想
当人们面对复杂问题的处理方式:
- 将复杂的问题进行拆解, 拆分成很多个可以处理的小问题
- 再将其放在整体当中,你会发现大的问题也会迎刃而解
其实上面的思想就是分而治之的思想:
- 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石
- 而前端目前的模块化和组件化都是基于分而治之的思想
2.什么是组件化开发呢?
组件化也是类似的思想:
- 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
- 但如果我们将一个页面拆分成一个个小的功能模块, 每个功能完成自己这部分独立功能, 那么整个页面的管理和维护变得非常容易
我们需要通过组件化的思想来思考整个应用程序:
- 我们将一个完整的页面分成很多个组件
- 每个组件都用于实现页面的一个功能块
3.React的组件化
组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一颗组件树
组件化思想的应用:
- 尽可能的将页面拆分成一个个小的、可复用的组件
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强
4.React组件分类
React的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
这些概念有很多重叠,但是它们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要关注UI的展示
- 类组件、有状态组件、容器型组件主要关注数据逻辑
React 创建组件
1.类组件
类组件的定义由如下要求:
- 组件的名称是大写字符开头 (无论类组件还是函数组件)
- 类组件需要继承自:
React.Component
- 类组件必须实现
render
函数
使用
class
定义一个组件:constructor
是可选的,我们通常在constructor
中初始化一些数据this.state
中维护的就是我们组件内部的数据render()
方法是class
组件中唯一必须实现的方法
2.render函数的返回值
当render
函数被调用时, 它会检查this.props
和this.state
的变化并返回以下类型之一
React元素
- 通常通过
JSX
创建 - 例如:
<div/>
会被React
渲染为DOM
节点,\<MyComponent/>
会被React
渲染为自定义组件 - 无论是
<div/>
还是<MyComponent/>
均为React
元素
- 通常通过
- 数组或 fragments: 使得
render
方法可以返回多个元素 - Portals: 可以渲染子节点到不同的
DOM
子树中 - 字符串或数值类型: 他们在
DOM
中会被渲染为文本节点 - 布尔类型或null: 什么都不渲染
3.函数组件
函数组件是使用function
来进行定义的函数, 只是这个函数会返回和类组件中render
函数一样的内容
函数组件的特点 (后面会讲hooks, 就不一样了)
- 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
- 没有 this (组件实例)
- 没有内部状态 (state)
React 生命周期
1.认识生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期
React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
- 比如装载阶段(
Mount
),组件第一次在DOM树中被渲染的过程 - 比如更新阶段(
Update
),组件状态发生变化,重新更新渲染的过程 - 比如卸载过程(
Unmount
),组件从DOM树中被移除的过程
- 比如装载阶段(
React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的预定函数进行回调,这些函数就是生命周期函数
- 比如实现
componentDidMount
函数:组件已经挂载到DOM上时,就会回调 - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调 - 比如实现
componentWillUnmount
函数:组件卸载及销毁之前,就会回调
- 比如实现
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的
- (后面我们可以通过hooks来模拟一些生命周期的回调)
2.生命周期解析
- 我们先来学习一下最基础、最常用的生命周期函数:
3.生命周期函数应用场景
Constructor
- 如果不初始化
state
或不进行方法绑定,则不需要为React
组件实现构造函数 constructor
中通常只做两件事情:- 通过给
this.state
赋值对象来初始化内部 state - 为事件绑定this
- 通过给
componentDidMount
componentDidMount()
会在组件挂载后 ( 插入DOM树中 ) 立即调用componentDidMount()
中通常进行哪些操作?- 依赖于DOM的操作可以在这里进行
- 在此处发送网络请求就最好的地方 (官方建议)
- 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)
componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行- 当组件更新后,可以对此 DOM 进行操作
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求
(例如,当 props 未发生变化时,则不会执行网络请求)
componentWillUnmount
componentWillUnmount
会在组件卸载及销毁之前调用- 在此方法执行必要的清理操作
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅
3.不常用的生命周期函数
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数
getDerivedStateFromProps
state
的值在任何时候都依赖于props
时使用- 该方法返回一个对象来更新state
getSnapshotBeforeUpdate
- 在 React 更新 DOM 之前回调的一个函数
- 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)
React 组件的嵌套
1.认识组件的嵌套
组件之间存在嵌套关系:
- 在之前的案例中,我们只是创建了一个组件App
- 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护
- 所以组件化的核心思想应该是对组件进行拆分, 拆分成一个个小的组件
- 在将这些组件组合嵌套在一起, 最终形成我们的应用程序
上面的嵌套逻辑如下
- App组件是Header、Main、Footer组件的父组件
- Main组件是Banner、ProductList组件的父组件
2.认识组件间的通信
在开发过程中,我们会经常遇到需要组件之间相互进行通信
- 比如
App
可能使用了多个Header
组件,每个地方的Header
展示的内容不同,那么我们就需要使用者传递给Header
一些数据,让其进行展示 - 又比如我们在
Main
组件中一次请求了Banner
数据和ProductList
数据,那么就需要传递给它们来进行展示
- 比如
- 总之,在一个
React
项目中,组件之间通信是非常重要的环节
React 父子组件间通信
1.父传子组件-props
父组件在展示子组件, 可能会传递一些数据给子组件
- 父组件通过<font color='red'> 属性=值</font> 的形式给子组件传递数据
- 子组件通过 <font color='red'>props</font> 参数获取父组件传递过来的数据
<details>
<summary>函数组件传递Props</summary>
</details>
2.属性验证-propTypes
对于传递给子组件的数据, 有时候我们可能希望进行数据的类型校验, 特别是对于大型项目来说
- 当然,如果你项目中默认继承了
Flow
或者TypeScript
,那么直接就可以进行类型验证 - 但是,即使我们没有使用
Flow
或者TypeScript
,也可以通过prop-type
库来进行参数验证
- 当然,如果你项目中默认继承了
- 使用
propTypes
来进行对props
的验证, 首先: 导入prop-types
我们这里只做的 <font color='red'>props 类型的校验</font> 和 <font color='red'>props 的默认值</font> (更多的验证方式可以参考官网)
- 比如某个 props 属性是必须传递的使用:
propTypes.string.isRequired
- 比如某个 props 属性是必须传递的使用:
- 如果没有传递
props
, 我们希望有默认值使用:类名.defaultProps = {}
// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'
// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
// name属性是必传的
name: propTypes.string.isRequired,
age: propTypes.number,
height: propTypes.number,
}
// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
name: 'hali',
age: 21,
height: 1.77,
}
3.子组件传递父组件-函数传递
当子组件需要向父组件传递消息:
- 在
vue
中是通过自定义事件来完成的 - 在
React
中同样还是通过props
传递消息 只是让父组件给子组件传递一个回调函数(callback),在子组件调用这个函数
- 注意
this
绑定问题
- 注意
- 在
// 父组件
render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
{/* 子传父: 让子组件来调用父组件中的方法 */}
{/* 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 */}
<Counter increment={e => this.increment()} />
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1,
})
}
// 子组件
class Counter extends Component {
render() {// 调用父组件传递的函数
return <button onClick={this.props.increment}>+</button>
}
}
React 非父子组件通信
1.Context介绍
非父子组件数据的共享:
在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递
但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
React
提供了一个API
:Context
Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
2.Context使用
React.createContext
- 作用: 创建一个需要全局共享对象的数据 (需要跨组件间通信的数据)
- 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的
Provider
中读取到当前的context
值 defaultValue
是组件在顶层查找过程中没有找到对应的Provider
,那么就使用默认值
Context.Provider
- 介绍:每个 Context 对象都会返回一个
Provider React
组件,它允许消费组件订阅 context 的变化 - 传递value:
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
节点
3.组件通信图示
React 通信补充
1.React slot
slot
翻译为插槽: 在React
中没有插槽概念, 因为React
太灵活了, 那在React如何实现插槽功能呢?使用过
Vue
的朋友知道, 如果我们向一个组件中插入内容时, 可以在子组件中预留插槽, 内容由父组件决定
Children 实现插槽功能
父组件在使用子组件时, 将需要展示的内容用子组件包裹起来
子组件中通过:
props.children[0]
来取得父组件要展示的内容<details>
<summary>理解</summary>
<p>前面我们讲过:</p>
<li> render 函数中 return 的JSX 代码最终会转换成React.createElement('tabName', 'config', children)
</li>
<li>而第三个参数 children 就是: 咱们在元素中插入的子元素, 在React源码中将ChildrenArr
放到了props
属性中的children
中, 所以说子组件可以通过props.children
来接收到父组件插入的子元素</li>
</details>
props 实现具名插槽
在插入插槽内容时, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)
在React中可以通过 属性(props) 来指定你要插入的内容, 然后在子组件中使用 props 取出指定的JSX元素插入到指定位置
总结
Children
使用场景: 当只有一个默认内容时, 直接插入到子元素即可props
指定slot
使用场景: 当有多个插槽内容时, 使用props
形式传递
2.属性展开
如果你已经有了一个 props 对象,你可以使用展开运算符 ...
来在 JSX 中传递整个 props 对象
function Profile(props) {
return (
<div>
{/* 在 JSX 中传递整个 props 对象。以下两个组件是等价的 */}
<ProfileHeader nickname={props.nickname} level={props.level}/>
<ProfileHeader {...props}/>
</div>
)
}
events 事件总线
events
前面通过
Context
主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?- 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
- 在React中,我们可以依赖一个使用较多的库
events
来完成对应的操作
- 我们可以通过npm或者yarn来安装events:
yarn add events
events常用的API:
- 创建
EventEmitter
对象:eventBus
对象; - 发出事件:
eventBus.emit
("事件名称", 参数列表); - 监听事件:
eventBus.addListener
("事件名称", 监听函数); - 移除事件:
eventBus.removeListener
("事件名称", 监听函数);
- 创建
// 1.创建全局事件总线
const eventBus = new EventEmitter()
// 2.发射事件
emitHomeEvent() {
eventBus.emit('sayHello', 'hello home', 123)
}
// 3.监听事件
componentDidMount() {
eventBus.addListener('sayHello', this.handleSayHelloListener)
}
// 4.卸载事件
componentWillUnmount() {
eventBus.removeListener('sayHello', this.handleSayHelloListener)
}
refs
1.如何使用ref
React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;
- 如何创建
refs
来获取对应的DOM
呢?目前有三种方式: 方式一:传入字符串
- 使用时通过
this.refs.传入的字符串
格式获取对应的元素
- 使用时通过
方式二:传入一个对象
- 对象是通过
React.createRef()
方式创建出来的; - 使用时获取到创建的对象其中有一个
current
属性就是对应的元素
- 对象是通过
方式三:传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
- 使用时,直接拿到之前保存的元素对象即可
import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
super(props)
this.titleRef = createRef()
this.titleEl = null
}
render() {
return (
<div>
{/* <h2 ref=字符串/对象/函数方式> hello react</h2> */}
<h2 ref="titleRef">hello react</h2>
<h2 ref={this.titleRef}>hello react</h2>
<h2 ref={arg => (this.titleEl = arg)}>hello react</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
// 1.通过refs来操作DOM,有三种方式
// 方式一: 字符串
this.refs.titleRef.innerHTML = 'hello jean'
// 方式二: 对象
this.titleRef.current.innerHTML = 'hello JavaScript'
// 方式三: 函数
this.titleEl.innerHTML = 'hello TypeScript'
}
2.ref的类型
ref
的值根据节点的类型而有所不同:- 当
ref
属性用于HTML
元素时,构造函数中使用React.createRef()
创建的ref
接收底层DOM
元素作为其current
属性 - 当
ref
属性用于自定义class
组件时,ref
对象接收组件的挂载实例作为其current
属性 - 你不能在函数组件上使用 ref 属性,因为他们没有实例
constructor(props) {
this.counterRef = createRef()
}
render() {
return (
<div>
<Counter ref={this.counterRef} />
<button onClick={e => this.appIncrementCount()}>APP的按钮</button>
</div>
)
}
// 通过ref来获取类组件对象
appIncrementCount() {
// 调用子组件方法
this.counterRef.current.increment()
}
函数式组件是没有实例的,所以无法通过ref获取他们的实例:
但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
这个时候我们可以通过React.forwardRef
,后面我们也会学习hooks
中如何使用ref;
受控组件与非受控组件
1.认识受控组件
- 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的
state
比如下面的HTML表单元素:
- 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在React中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”;
2.受控组件基本演练
- 在 HTML 中,表单元素(如\<input>、 \<textarea> 和 \<select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新
- 我们将两者结合起来,使
React
的state
成为“唯一数据源”; - 渲染表单的
React
组件还控制着用户输入过程中表单发生的操作; - 被
React
以这种方式控制取值的表单输入元素就叫做“受控组件”;
- 我们将两者结合起来,使
- 由于在表单元素上设置了
value
属性,因此显示的值将始终为this.state.value
,这使得 React 的 state 成为唯一数据源。 - 由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新
3.受控组件的其他演练
textarea标签
- texteare标签和input比较相似:
select标签
- select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
处理多个输入
- 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
这里我们可以使用ES6的一个语法:计算属性名(Computed property names)
- 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
3.非受控组件(了解)
React推荐大多数情况下使用 受控组件 来处理表单数据:
- 一个受控组件中,表单数据是由 React 组件来管理的;
- 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据
- 我们来进行一个简单的演练:
- 使用ref来获取input元素;
- 在非受控组件中通常使用defaultValue来设置默认值
- <details>
<summary>了解即可</summary>
</details>
高阶组件
1.认识高阶函数
- 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数
高阶函数的维基百科定义(至少满足以下条件之一):
- 接受一个或多个函数作为输入
- 输出一个函数
JavaScript
中比较常见的filter、map、reduce
都是高阶函数那么什么是高阶组件呢?
- 高阶组件的英文是 Higher-Order Components,简称为 HOC
- 官方的定义: 高阶组件是参数为组件, 返回为新的组件
我们可以进行如下的解析:
- 首先, 高阶组件本身不是一个组件, 而是一个函数
- 其次, 这个函数的参数是一个组件, 返回值是一个组件
2.高阶组件的定义
高阶组件调用类似于:
- <details>
<summary>高阶函数的编写过程类似于这样</summary>
</details> 组件的名称问题:
- 在ES6中,类表达式中类名是可以省略的
- 组件的名称都可以通过
displayName
来修改
高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式
高阶组件在一些React第三方库中非常常见:
- 比如redux中的connect (后续会讲到)
- 比如react-router中的withRouter (后续会讲到)
3.高阶组件应用场景
应用一: props的增强
- 不修改原有代码的情况下,添加新的props
- 利用高阶组件来共享Context
应用二: 渲染判断鉴权
在开发中,我们可能遇到这样的场景:
- 某些页面是必须用户登录成功才能进行进入;
- 如果用户没有登录成功,那么直接跳转到登录页面;
- 这个时候,我们就可以使用高阶组件来完成鉴权操作
应用三: 生命周期劫持
- 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:
4.高阶组件的意义
- 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理
- 利用高阶组件可以完成多个组件中的共同功能
其实早期的React有提供组件之间的一种复用方式是
mixin
,目前已经不再建议使用:- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
当然,(高阶组件)HOC 也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
- HOC可以劫持
props
,在不遵守约定的情况下也可能造成冲突
Hooks
的出现,是开创性的,它解决了很多React之前的存在的问题- 比如this指向问题、比如hoc的嵌套复杂度问题等等
4.ref的转发(获取函数式组件DOM)
在前面我们学习
ref
时讲过,ref
不能应用于函数式组件:- 因为函数式组件没有实例,所以不能获取到对应的组件对象
但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?
- 方式一:直接传入ref属性 (错误的做法)
- 方式二:通过
forwardRef高阶函数
Portals
Portals的使用
- 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到
id
为root
的DOM
元素上的) Portal
提供了一种将子节点渲染到存在于父组件之外的 DOM 节点- 参数一:
child
是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment; - 参数二:
container
是一个DOM 元素 ReactDOM.createPortal(child, container)
- 参数一:
- 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
- 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的
Fragment
Fragment的使用
- 在之前的开发中,我们总是在一个组件中返回内容时包裹一个div根元素
我们希望可以不渲染这样根元素div应该如何操作呢?
- <details>
<summary>使用Fragment
</summary>
</details> Fragment
允许你将子列表分组, 无需向 DOM 天添加额外节点
- <details>
React还提供了
Fragment
的短语法:- 短语法使用:
<></>
- <details>
<summary>下拉查看</summary>
</details> - 注意: 如果我们需要在
Fragment
中添加属性或者key
, 那么就不能使用短语法
- 短语法使用:
StrictMode
StrictMode 介绍
StrictMode
是一个用来突出显示应用程序中潜在问题的工具- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
- 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建
可以为应用程序的任何部分启用严格模式:
- 不会对 Header 和 Footer 组件运行严格模式检查
- 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查
严格模式检查的是什么?
到底检测什么呢?
- 识别不安全的生命周期
- 使用过时的
ref API
使用废弃的
findDOMNode
方法- 在之前的React API中,可以通过
findDOMNode
来获取DOM,不过已经不推荐使用了
- 在之前的React API中,可以通过
检查意外的副作用
- 这个组件的
constructor
会被调用两次 - 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
- 在生产环境中,是不会被调用两次的
- 这个组件的
检测过时的
context API
- 早期的
Context
是通过static
属性声明Context
对象属性,通过getChildContext
返回Context
对象等方式来使用Context
的 - 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法
- 早期的
认识组件化
1.组件化思想
当人们面对复杂问题的处理方式:
- 将复杂的问题进行拆解, 拆分成很多个可以处理的小问题
- 再将其放在整体当中,你会发现大的问题也会迎刃而解
其实上面的思想就是分而治之的思想:
- 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石
- 而前端目前的模块化和组件化都是基于分而治之的思想
2.什么是组件化开发呢?
组件化也是类似的思想:
- 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
- 但如果我们将一个页面拆分成一个个小的功能模块, 每个功能完成自己这部分独立功能, 那么整个页面的管理和维护变得非常容易
我们需要通过组件化的思想来思考整个应用程序:
- 我们将一个完整的页面分成很多个组件
- 每个组件都用于实现页面的一个功能块
3.React的组件化
组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一颗组件树
组件化思想的应用:
- 尽可能的将页面拆分成一个个小的、可复用的组件
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强
4.React组件分类
React的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
这些概念有很多重叠,但是它们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要关注UI的展示
- 类组件、有状态组件、容器型组件主要关注数据逻辑
React 创建组件
1.类组件
类组件的定义由如下要求:
- 组件的名称是大写字符开头 (无论类组件还是函数组件)
- 类组件需要继承自:
React.Component
- 类组件必须实现
render
函数
使用
class
定义一个组件:constructor
是可选的,我们通常在constructor
中初始化一些数据this.state
中维护的就是我们组件内部的数据render()
方法是class
组件中唯一必须实现的方法
2.render函数的返回值
当render
函数被调用时, 它会检查this.props
和this.state
的变化并返回以下类型之一
React元素
- 通常通过
JSX
创建 - 例如:
<div/>
会被React
渲染为DOM
节点,<MyComponent/>
会被React
渲染为自定义组件 - 无论是
<div/>
还是<MyComponent/>
均为React
元素
- 通常通过
- 数组或 fragments: 使得
render
方法可以返回多个元素 - Portals: 可以渲染子节点到不同的
DOM
子树中 - 字符串或数值类型: 他们在
DOM
中会被渲染为文本节点 - 布尔类型或null: 什么都不渲染
3.函数组件
函数组件是使用function
来进行定义的函数, 只是这个函数会返回和类组件中render
函数一样的内容
函数组件的特点 (后面会讲hooks, 就不一样了)
- 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
- 没有 this (组件实例)
- 没有内部状态 (state)
React 生命周期
1.认识生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期
React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
- 比如装载阶段(
Mount
),组件第一次在DOM树中被渲染的过程 - 比如更新阶段(
Update
),组件状态发生变化,重新更新渲染的过程 - 比如卸载过程(
Unmount
),组件从DOM树中被移除的过程
- 比如装载阶段(
React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的预定函数进行回调,这些函数就是生命周期函数
- 比如实现
componentDidMount
函数:组件已经挂载到DOM上时,就会回调 - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调 - 比如实现
componentWillUnmount
函数:组件卸载及销毁之前,就会回调
- 比如实现
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的
- (后面我们可以通过hooks来模拟一些生命周期的回调)
2.生命周期解析
- 我们先来学习一下最基础、最常用的生命周期函数:
3.生命周期函数应用场景
Constructor
- 如果不初始化
state
或不进行方法绑定,则不需要为React
组件实现构造函数 constructor
中通常只做两件事情:- 通过给
this.state
赋值对象来初始化内部 state - 为事件绑定this
- 通过给
componentDidMount
componentDidMount()
会在组件挂载后 ( 插入DOM树中 ) 立即调用componentDidMount()
中通常进行哪些操作?- 依赖于DOM的操作可以在这里进行
- 在此处发送网络请求就最好的地方 (官方建议)
- 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)
componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行- 当组件更新后,可以对此 DOM 进行操作
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求 (例如,当 props 未发生变化时,则不会执行网络请求)
componentWillUnmount
componentWillUnmount
会在组件卸载及销毁之前调用- 在此方法执行必要的清理操作
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅
3.不常用的生命周期函数
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数
getDerivedStateFromProps
state
的值在任何时候都依赖于props
时使用- 该方法返回一个对象来更新state
getSnapshotBeforeUpdate
- 在 React 更新 DOM 之前回调的一个函数
- 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)
React 组件的嵌套
1.认识组件的嵌套
组件之间存在嵌套关系:
- 在之前的案例中,我们只是创建了一个组件App
- 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护
- 所以组件化的核心思想应该是对组件进行拆分, 拆分成一个个小的组件
- 在将这些组件组合嵌套在一起, 最终形成我们的应用程序
上面的嵌套逻辑如下
- App组件是Header、Main、Footer组件的父组件
- Main组件是Banner、ProductList组件的父组件
2.认识组件间的通信
在开发过程中,我们会经常遇到需要组件之间相互进行通信
- 比如
App
可能使用了多个Header
组件,每个地方的Header
展示的内容不同,那么我们就需要使用者传递给Header
一些数据,让其进行展示 - 又比如我们在
Main
组件中一次请求了Banner
数据和ProductList
数据,那么就需要传递给它们来进行展示
- 比如
- 总之,在一个
React
项目中,组件之间通信是非常重要的环节
React 父子组件间通信
1.父传子组件-props
父组件在展示子组件, 可能会传递一些数据给子组件
- 父组件通过 属性=值 的形式给子组件传递数据
- 子组件通过 props 参数获取父组件传递过来的数据
函数组件传递Props
2.属性验证-propTypes
对于传递给子组件的数据, 有时候我们可能希望进行数据的类型校验, 特别是对于大型项目来说
- 当然,如果你项目中默认继承了
Flow
或者TypeScript
,那么直接就可以进行类型验证 - 但是,即使我们没有使用
Flow
或者TypeScript
,也可以通过prop-type
库来进行参数验证
- 当然,如果你项目中默认继承了
- 使用
propTypes
来进行对props
的验证, 首先: 导入prop-types
我们这里只做的 props 类型的校验 和 props 的默认值 (更多的验证方式可以参考官网)
- 比如某个 props 属性是必须传递的使用:
propTypes.string.isRequired
- 比如某个 props 属性是必须传递的使用:
- 如果没有传递
props
, 我们希望有默认值使用:类名.defaultProps = {}
// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'
// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
// name属性是必传的
name: propTypes.string.isRequired,
age: propTypes.number,
height: propTypes.number,
}
// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
name: 'hali',
age: 21,
height: 1.77,
}
3.子组件传递父组件-函数传递
当子组件需要向父组件传递消息:
- 在
vue
中是通过自定义事件来完成的 - 在
React
中同样还是通过props
传递消息 只是让父组件给子组件传递一个回调函数(callback),在子组件调用这个函数
- 注意
this
绑定问题
- 注意
- 在
// 父组件
render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
{/ 子传父: 让子组件来调用父组件中的方法 /}
{/ 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 /}
<Counter increment={e => this.increment()} />
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1,
})
}
// 子组件
class Counter extends Component {
render() {// 调用父组件传递的函数
return <button onClick={this.props.increment}>+</button>
}
}
React 非父子组件通信
1.Context介绍
非父子组件数据的共享:
在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递
但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
React
提供了一个API
:Context
Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
2.Context使用
React.createContext
- 作用: 创建一个需要全局共享对象的数据 (需要跨组件间通信的数据)
- 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的
Provider
中读取到当前的context
值 defaultValue
是组件在顶层查找过程中没有找到对应的Provider
,那么就使用默认值
Context.Provider
- 介绍:每个 Context 对象都会返回一个
Provider React
组件,它允许消费组件订阅 context 的变化 - 传递value:
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
节点
3.组件通信图示
React 通信补充
1.React slot
slot
翻译为插槽: 在React
中没有插槽概念, 因为React
太灵活了, 那在React如何实现插槽功能呢?使用过
Vue
的朋友知道, 如果我们向一个组件中插入内容时, 可以在子组件中预留插槽, 内容由父组件决定
Children 实现插槽功能
父组件在使用子组件时, 将需要展示的内容用子组件包裹起来
子组件中通过:
props.children[0]
来取得父组件要展示的内容
理解
props 实现具名插槽
在插入插槽内容时, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)
在React中可以通过 属性(props) 来指定你要插入的内容, 然后在子组件中使用 props 取出指定的JSX元素插入到指定位置
总结
Children
使用场景: 当只有一个默认内容时, 直接插入到子元素即可props
指定slot
使用场景: 当有多个插槽内容时, 使用props
形式传递
2.属性展开
如果你已经有了一个 props 对象,你可以使用展开运算符 ...
来在 JSX 中传递整个 props 对象
function Profile(props) {
return (
<div>
{/ 在 JSX 中传递整个 props 对象。以下两个组件是等价的 /}
<ProfileHeader nickname={props.nickname} level={props.level}/>
<ProfileHeader {...props}/>
</div>
)
}
events 事件总线
events
前面通过
Context
主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?- 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
- 在React中,我们可以依赖一个使用较多的库
events
来完成对应的操作
- 我们可以通过npm或者yarn来安装events:
yarn add events
events常用的API:
- 创建
EventEmitter
对象:eventBus
对象; - 发出事件:
eventBus.emit
("事件名称", 参数列表); - 监听事件:
eventBus.addListener
("事件名称", 监听函数); - 移除事件:
eventBus.removeListener
("事件名称", 监听函数);
- 创建
// 1.创建全局事件总线
const eventBus = new EventEmitter()
// 2.发射事件
emitHomeEvent() {
eventBus.emit('sayHello', 'hello home', 123)
}
// 3.监听事件
componentDidMount() {
eventBus.addListener('sayHello', this.handleSayHelloListener)
}
// 4.卸载事件
componentWillUnmount() {
eventBus.removeListener('sayHello', this.handleSayHelloListener)
}
refs
1.如何使用ref
React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;
- 如何创建
refs
来获取对应的DOM
呢?目前有三种方式: 方式一:传入字符串
- 使用时通过
this.refs.传入的字符串
格式获取对应的元素
- 使用时通过
方式二:传入一个对象
- 对象是通过
React.createRef()
方式创建出来的; - 使用时获取到创建的对象其中有一个
current
属性就是对应的元素
- 对象是通过
方式三:传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
- 使用时,直接拿到之前保存的元素对象即可
import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
super(props)
this.titleRef = createRef()
this.titleEl = null
}
render() {
return (
<div>
{/ <h2 ref=字符串/对象/函数方式> hello react</h2> /}
<h2 ref="titleRef">hello react</h2>
<h2 ref={this.titleRef}>hello react</h2>
<h2 ref={arg => (this.titleEl = arg)}>hello react</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
// 1.通过refs来操作DOM,有三种方式
// 方式一: 字符串
this.refs.titleRef.innerHTML = 'hello jean'
// 方式二: 对象
this.titleRef.current.innerHTML = 'hello JavaScript'
// 方式三: 函数
this.titleEl.innerHTML = 'hello TypeScript'
}
2.ref的类型
ref
的值根据节点的类型而有所不同:- 当
ref
属性用于HTML
元素时,构造函数中使用React.createRef()
创建的ref
接收底层DOM
元素作为其current
属性 - 当
ref
属性用于自定义class
组件时,ref
对象接收组件的挂载实例作为其current
属性 - 你不能在函数组件上使用 ref 属性,因为他们没有实例
constructor(props) {
this.counterRef = createRef()
}
render() {
return (
<div>
<Counter ref={this.counterRef} />
<button onClick={e => this.appIncrementCount()}>APP的按钮</button>
</div>
)
}
// 通过ref来获取类组件对象
appIncrementCount() {
// 调用子组件方法
this.counterRef.current.increment()
}
函数式组件是没有实例的,所以无法通过ref获取他们的实例: 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素; 这个时候我们可以通过React.forwardRef
,后面我们也会学习hooks
中如何使用ref;
受控组件与非受控组件
1.认识受控组件
- 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的
state
比如下面的HTML表单元素:
- 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在React中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”;
2.受控组件基本演练
- 在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新
- 我们将两者结合起来,使
React
的state
成为“唯一数据源”; - 渲染表单的
React
组件还控制着用户输入过程中表单发生的操作; - 被
React
以这种方式控制取值的表单输入元素就叫做“受控组件”;
- 我们将两者结合起来,使
- 由于在表单元素上设置了
value
属性,因此显示的值将始终为this.state.value
,这使得 React 的 state 成为唯一数据源。 - 由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新
3.受控组件的其他演练
textarea标签
- texteare标签和input比较相似:
select标签
- select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
处理多个输入
- 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法: 这里我们可以使用ES6的一个语法:计算属性名(Computed property names)
3.非受控组件(了解)
React推荐大多数情况下使用 受控组件 来处理表单数据:
- 一个受控组件中,表单数据是由 React 组件来管理的;
- 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据
- 我们来进行一个简单的演练:
- 使用ref来获取input元素;
- 在非受控组件中通常使用defaultValue来设置默认值
了解即可
高阶组件
1.认识高阶函数
- 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数
高阶函数的维基百科定义(至少满足以下条件之一):
- 接受一个或多个函数作为输入
- 输出一个函数
JavaScript
中比较常见的filter、map、reduce
都是高阶函数那么什么是高阶组件呢?
- 高阶组件的英文是 Higher-Order Components,简称为 HOC
- 官方的定义: 高阶组件是参数为组件, 返回为新的组件
我们可以进行如下的解析:
- 首先, 高阶组件本身不是一个组件, 而是一个函数
- 其次, 这个函数的参数是一个组件, 返回值是一个组件
2.高阶组件的定义
高阶组件调用类似于:
高阶函数的编写过程类似于这样
组件的名称问题:
- 在ES6中,类表达式中类名是可以省略的
- 组件的名称都可以通过
displayName
来修改
高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式
高阶组件在一些React第三方库中非常常见:
- 比如redux中的connect (后续会讲到)
- 比如react-router中的withRouter (后续会讲到)
3.高阶组件应用场景
应用一: props的增强
- 不修改原有代码的情况下,添加新的props
- 利用高阶组件来共享Context
应用二: 渲染判断鉴权
在开发中,我们可能遇到这样的场景:
- 某些页面是必须用户登录成功才能进行进入;
- 如果用户没有登录成功,那么直接跳转到登录页面;
- 这个时候,我们就可以使用高阶组件来完成鉴权操作
应用三: 生命周期劫持
- 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:
4.高阶组件的意义
- 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理
- 利用高阶组件可以完成多个组件中的共同功能
其实早期的React有提供组件之间的一种复用方式是
mixin
,目前已经不再建议使用:- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
当然,(高阶组件)HOC 也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
- HOC可以劫持
props
,在不遵守约定的情况下也可能造成冲突
Hooks
的出现,是开创性的,它解决了很多React之前的存在的问题- 比如this指向问题、比如hoc的嵌套复杂度问题等等
4.ref的转发(获取函数式组件DOM)
在前面我们学习
ref
时讲过,ref
不能应用于函数式组件:- 因为函数式组件没有实例,所以不能获取到对应的组件对象
但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?
- 方式一:直接传入ref属性 (错误的做法)
- 方式二:通过
forwardRef高阶函数
Portals
Portals的使用
- 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到
id
为root
的DOM
元素上的) Portal
提供了一种将子节点渲染到存在于父组件之外的 DOM 节点- 参数一:
child
是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment; - 参数二:
container
是一个DOM 元素 ReactDOM.createPortal(child, container)
- 参数一:
- 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
- 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的
Fragment
Fragment的使用
- 在之前的开发中,我们总是在一个组件中返回内容时包裹一个div根元素
我们希望可以不渲染这样根元素div应该如何操作呢?
使用
Fragment
Fragment
允许你将子列表分组, 无需向 DOM 天添加额外节点
React还提供了
Fragment
的短语法:- 短语法使用:
<></>
下拉查看
- 注意: 如果我们需要在
Fragment
中添加属性或者key
, 那么就不能使用短语法
- 短语法使用:
StrictMode
StrictMode 介绍
StrictMode
是一个用来突出显示应用程序中潜在问题的工具- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
- 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行;_它们不会影响生产构建_
可以为应用程序的任何部分启用严格模式:
- 不会对 Header 和 Footer 组件运行严格模式检查
- 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查
严格模式检查的是什么?
到底检测什么呢?
- 识别不安全的生命周期
- 使用过时的
ref API
使用废弃的
findDOMNode
方法- 在之前的React API中,可以通过
findDOMNode
来获取DOM,不过已经不推荐使用了
- 在之前的React API中,可以通过
检查意外的副作用
- 这个组件的
constructor
会被调用两次 - 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
- 在生产环境中,是不会被调用两次的
- 这个组件的
检测过时的
context API
- 早期的
Context
是通过static
属性声明Context
对象属性,通过getChildContext
返回Context
对象等方式来使用Context
的 - 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法
- 早期的
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。