欢迎访问:【React Native】从React开始——高级概念
1. 前言
在博客【React Native】从React开始——核心概念中了解了React
的一些基础用法,并了解到其实和之前学习的微信小程序开发十分相像。感兴趣微信小程序开发的同学可以去同步了解学习下。在【React Native】从React开始——React Router 基础一篇中学习了在React
中路由的使用,通过路由,我们的单页面应用看起来也更像是一个完整的前端页面。但是其实当用户点击设定好的链接的时候,因为实际上并不会再向服务器发起页面请求,而是直接根据代码逻辑进行DOM
的渲染,所以这里加载会更加流畅。
当然,仅有前面两篇博客的只是储备其实还是略微不够的,因为在React
中还有很多高级的用法。在官网中的高级指引部分内容,也就是这篇博客学习的核心。感兴趣可以直接查看官网的文档。
2. 内容
2.1 Context
Context
提供了一个无需为每层组件手动添加props
,就能在组件树间进行数据传递的方法。
在一个典型的 React
应用中,数据是通过 props
属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的。Context
提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
。
【注】:本篇博客的示例代码在ReactLearn/src_11.04 v0.3/的基础上进行修改。比如这里修改NoMatch.js
文件为:
// NoMatch.js
import './App.css'
import UserInfoContext from './Config'
import React from 'react'
class NoMatch extends React.Component {
render() {
// 使用为this.context
console.log(this.context)
return (
<div>
<h3>
{this.context} No match !
</h3>
</div>
)
}
}
// 设置到当前的context
NoMatch.contextType = UserInfoContext
export default NoMatch
新建一个Config.js
文件,简单的用来测试:
// Config.js
import React from "react"
// 直接外部定义一个Context
const UserInfoContext = React.createContext("404")
export default UserInfoContext;
最终我们随便输入一个链接,就可以看见效果为:

这里只是简单的使用,因为在官网中也提到了:
Context
主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
所以对于后面的Consumer
、displayName
等这里不再介绍。
2.2 错误边界
错误边界是一种 React
组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript
错误,并且,它会渲染出备用 UI
,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
注意
错误边界无法捕获以下场景中产生的错误:
1.事件处理
2.异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
3.服务端渲染
4.它自身抛出来的错误(并非它的子组件)
如果一个 class
组件中定义了 static getDerivedStateFromError()
或 componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,用 static getDerivedStateFromError()
渲染备用 UI
,使用 componentDidCatch()
打印错误信息。
copy
一下ErrorBoundary
到本地项目的ErrorBoundary.js
文件中,定义为:
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
console.log(error, errorInfo)
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>
}
return this.props.children
}
}
export default ErrorBoundary
然后,我们人为的制造一个bug
在NoMatch.js
文件中,比如:
import './App.css'
import UserInfoContext from './Config'
import React from 'react'
class NoMatch extends React.Component {
render() {
// 制造一个bug代码
console.log(undefined = 1)
return (
<div>
<h3>
{this.context} No match !
</h3>
</div>
)
}
}
// 设置到当前的context
NoMatch.contextType = UserInfoContext
export default NoMatch
然后在index.js
文件中,为默认匹配设置一个错误边界:
<Route path="*">
<ErrorBoundary>
<NoMatch />
</ErrorBoundary>
</Route>
为了看见效果,我们需要将项目打包到发布环境中。因为在开发环境最终会转到一个错误详情页面。比如下面的:

但,这里我们只需要看出现的线上效果。故而我们这里将项目简单打包,使用命令:
npm run build // 代码会被编译到build文件夹
npm install -g serve // 安装一个服务器
serve -s build // 运行

再次测试,可以看到页面效果为:

2.2.1 错误边界应该放置在哪?
错误边界的粒度由你来决定,可以将其包装在最顶层的路由组件并为用户展示一个 “Something went wrong
” 的错误信息,就像服务端框架经常处理崩溃一样。你也可以将单独的部件包装在错误边界以保护应用其他部分不崩溃。
2.2.2 关于 try/catch ?
try / catch
很棒但它仅能用于命令式代码:
try {
showButton();
} catch (error) {
// ...
}
然而,React
组件是声明式的并且具体指出 什么 需要被渲染。错误边界保留了 React
的声明性质,其行为符合你的预期。例如,即使一个错误发生在 componentDidUpdate
方法中,并且由某一个深层组件树的 setState
引起,其仍然能够冒泡到最近的错误边界。
2.3 Refs 转发
关于转发,官网-Refs 转发的介绍如为:Ref
转发是一项将 ref
自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。也就是说其实学习这块内容是很有必要的,因为在日常开发中,通常都需要定义一些可重用的组件库。
Ref
转发是一个可选特性,其允许某些组件接收 ref
,并将其向下传递(换句话说,“转发”它)给子组件。比如在文档中给出的示例:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.createRef
创建了一个React ref
并将其赋值给ref
变量。 - 我们通过指定
ref
为JSX
属性,将其向下传递给<FancyButton ref={ref}>
。 React
传递ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。- 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为JSX
属性。 - 当
ref
挂载完成,ref.current
将指向<button> DOM
节点。
注意:
- 第二个参数
ref
只在使用React.forwardRef
定义组件时存在。常规函数和class
组件不接收ref
参数,且props
中也不存在ref
。 Ref
转发不仅限于DOM
组件,你也可以转发refs
到class
组件实例中。
比如下面的一个简单案例/examples/RefsLearn.js
:
// examples/RefsLearn.js
import React from 'react'
class RefsLearn extends React.Component {
constructor(props) {
super(props)
// 创建React ref对象
this.myRef = React.createRef()
}
handleClick (event){
// 获取到input的自定义属性值
let dataId = event.target.getAttribute("data-id");
let obj = this.myRef.current;
if(dataId === '1'){
obj.focus();
}else if(dataId === '2'){
obj.disabled = true;
}else if(dataId === '3'){
obj.value = "";
}else{
// eslint-disable-next-line no-throw-literal
throw "Error!";
}
}
render() {
return (
<div>
<!--render中的子控件设置ref,那么就可以用这个ref来得到这个子控件对象-->
<input type="input" ref={this.myRef}/>
<ul>
<li><button onClick={this.handleClick.bind(this)} data-id={1}>获取焦点</button></li>
<li><button onClick={this.handleClick.bind(this)} data-id={2}>禁用按钮</button></li>
<li><button onClick={this.handleClick.bind(this)} data-id={3}>清空文本</button></li>
</ul>
</div>
)
}
}
export default RefsLearn
在上面的案例中,可以可以实现预定义的三种效果,截图:

通过上面的案例,可以体会到:
render
中的子控件设置ref
,那么就可以用这个ref
来得到这个子控件对象,这个对象为ref.current
。当然,操作过程需要一些JavaScript
的知识,可以参考w3school网站。
2.4 Fragments
通常我们在render
中返回的是一个div
包起来的一坨控件,比如上面的例子:
<div>
<input type="input" ref={this.myRef}/>
<ul>
...
</ul>
</div>
但是有时候,在render
中使用div
来包起来的时候,会导致不再是我们想要的HTML
结构。比如下面的案例:
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
// 结果:
<table>
<tr>
<!--这里不是我们所希望的结果-->
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
显然我们不需要td
外层的div
元素。所以在React
中提供了Fragments
来解决这个问题。可以在render
的时候使用:
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
或者使用更加简洁的语法:
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
来解决这个问题。
2.5 高阶组件(HOC)
高阶组件(HigherOrderComponent
,HOC
)是 React
中用于复用组件逻辑的一种高级技巧。HOC
自身不是 React API
的一部分,它是一种基于 React
的组合特性而形成的设计模式。
具体而言,高阶组件是参数为组件,返回值为新组件的函数。也就是说,高阶组件其实就是一个纯函数,它会接受一个组件作为参数,然后返回一个新的组件。
在官网中给了一个较为复杂的案例来说明这个问题,这里我用一个简单的案例来阐述。比如这里定义Human
的类,都具有学习的功能,简单定义为:
// Human.js
import React from "react";
class Human extends React.Component{
constructor(props){
super(props)
this.state = {
// 用来接收用户通知信息
message: '随便看看'
}
}
render() {
return (
<div>
<h1>{ this.state.message }</h1><br/>
</div>
);
}
}
export default Human;
比如张三和李四来了,张三喜欢看文学方面的书籍,李四喜欢看计算机相关的书籍。那么对于张三李四,我们就可以这样来写。
// Human.js
import React from 'react'
class Human extends React.Component {
constructor(props) {
super(props)
this.state = {
message: this.props.message === undefined ? '随便看看' : this.props.message
}
}
render() {
return (
<div>
<h1>{this.state.message}</h1>
</div>
)
}
}
export const withLearning = function (WrappedComponent, info) {
return class extends React.Component {
constructor(props) {
super(props)
this.state = {
// 用来接收用户通知信息
message: info,
}
}
render() {
//传递属性message
return <WrappedComponent message={this.state.message}/>
}
}
}
export default Human
那么在index.js
文件中进行定义为:
<Route path="/Human_v1" component={ Human }></Route>
<Route path="/Human_v2" component={ withLearning(Human, "文学") }></Route>
<Route path="/Human_v3" component={ withLearning(Human, "计算机") }></Route>
当我们访问对应的链接的时候,就可以看见对应的不同显示效果。这里不再截图。至于关于HOC的其余注意事项,这里不再复述。
2.6 虚拟DOM
UI
更新需要昂贵的 DOM
操作,而 React
内部使用几种巧妙的技术以便最小化 DOM
操作次数。为了了解什么是虚拟DOM
,首先看看传统Web
加载处理的流程。
在传统的
Web
应用中,我们往往会把数据的变化实时地更新到用户界面中,于是每次数据的微小变动都会引起DOM
树的重新渲染。虚拟
DOM
的目的是将所有操作累加起来,统计计算出所有的变化后,统一更新一次DOM
。
当Node
节点的更新,虚拟DOM
会比较两棵DOM
树的区别,保证最小化的DOM
操作。比如在官网-协调部分提到的Diffing
算法:
- 当对比两颗树时,
React
首先比较两棵树的根节点; - 当根节点为不同类型的元素时,
React
会拆卸原有的树并且建立起新的树; - 当比对两个相同类型的
React
元素时,React
会保留DOM
节点,仅比对及更新有改变的属性; - 在处理完当前节点之后,
React
继续对子节点进行递归; - 在默认条件下,当递归
DOM
节点的子元素时,React
会同时遍历两个子元素的列表;当产生差异时,生成一个mutation
。 - 为了解决匹配过程的性能问题,
React
支持key
属性。当子元素拥有key
时,React
使用key
来匹配原有树上的子元素以及最新树上的子元素。这个key
不需要全局唯一,但在列表中需要保持唯一。 Key
应该具有稳定,可预测,以及列表内唯一的特质。不稳定的key
(比如通过Math.random()
生成的)会导致许多组件实例和DOM
节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。
重新渲染表示在所有组件内调用 render
方法,这不代表 React
会卸载或装载它们。React
只会基于以上提到的规则来决定如何进行差异的合并。
3. 后记
对于React的部分了解的还只是皮毛部分。因为最近还需要写一篇小论文,需要看论文、做实验验证想法,并完成小论文。所以至于在官网提到的静态类型检查、严格模式以及非受控组件等内容,这里就忽略了,之后再补充吧。