React 高级功能详解:代码分割、Context 与高阶组件
December 19, 2024 (1y ago)
官方骨灰教程(三),这里主要是介绍了react中的高级功能,属于进阶内容
打包配置
简单的说明
正如大多数的前端框架一样,react也是需要打包的。在react上并没有vue cli /ng cli那么方便的东西,不过正是因为如此。react才是高度可扩展的,我们可以使用任意的打包工具,来配置我们自己需要的打包结构
当然了react官方也是有推荐的cli 比如 Create React App,Next.js,Gatsby,他们只需要写一个配置文件就好了,不需要再做其它的事情,如果你需要自己原生构建一个打包配置也是可以的。手动去配置就完了,我推荐使用webpack
使用create-react-app + webpack构件一个脚手架
代码分割优化秘密
当我们的app越来越大,如果都打包成一个bundle无疑是一个灾难,因此我们可以使用,代码分割来优化
动态引入
看起来很NB,其实非常的简单,就是一个简单的import( )就好了
- 使用前
import { add } form './math'
console.log( add(16.4) )- 使用后
import( './math' ).then( math => {
console.log( add(16.4) )
} )
// 注意阿 这种语法和webpack是有关的,你应该先了解,webpack 相关的一些东西
// 如果你使用了creat-react-app cli 就可以自动的使用了,他们给你配置好了,- 动手干一波
// 这里使用creat-react-app + webpack + balble
// 设计模式使用高阶组件的方式
1. 安装两个包
cnpm i react-loadable babel-plugin-syntax-dynamic-import -D
2. 配置bable .babelrc
{
"presets": [
"react"
],
"plugins": [
"syntax-dynamic-import"
]
}
3. 如果你使用了eslint可以这样来玩 .eslintrc
module.exports = {
//...若干配置
parser: "babel-eslint"
}
4. 封装一个高阶组件 loadable.jsx
import React from 'react'
import Loadable from 'react-loadable'
const Loading = () => {
return <div>loading...</div>
}
export default (Loader) => {
const LoadableComponent = Loadable({
loader: Loader,
loading: Loading
})
return class LoadableHOC extends React.Component {
render () {
return <LoadableComponent></LoadableComponent>
}
}
}
5.这是一个需要动态加载的组件 Import.jsx
import React from 'react'
class Import extends React.Component {
render () {
return <div>import...</div>
}
}
export default Import
6. 这是一个父容器 test.jsx
import React from 'react'
import loadable from '@/utils/loadable'
const Import = loadable(() => import('@/components/Import'))
class Test extends React.Component {
render () {
return (<div>
<Import></Import>
</div>)
}
}
export default Test
6. mian中试一下
import React from 'react'
import ReactDom from 'react-dom'
import Test from '@/components/Test'
ReactDom.render(<Test />, document.getElementById('app')React.lazy
React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
🔍 注意:React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库。它有一个很棒的服务端渲染打包指南。
- 使用前
import OtherComponent from './OtherComponent';- 使用后
const OtherComponent = React.lazy(() => import('./OtherComponent'));
// 此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。
注意这个lazy 接受有个promise函数,并且要求resolve一个defalut export的react组件
- 指示加载器
这个是什么东西?这东西实际上就是一个 lazy的时候 用于loading的东西
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}- 注意:如果是服务端渲染建议使用这个库 Loadable Components 配合lazy 后者nextjs
模块加载异常怎么办?异常捕获边界
有时候,模块会加载异常,这个时候你需要这样来配 ,实际上也是一个HOC设计模式的最佳践行
import React, { Suspense } from 'react';
import MyErrorBoundary from './ErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}命名导出
这里有一个坑就是 lazy只能支持默认导出 default export 如果是使用name export需要使用一个中间,模块来处理
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));基于路由的代码分割
我们可以使用react-router 集合lazy进行留有的懒加载
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
Context
说明时候使用它
context 就是一个不断的往下穿透传递的prors ,如同vue中的provid
API
- creatContext
const MyContext = React.createContext(defaultValue);
// 创建一个content 具有默认值value 注意阿 如果你传递了undefined 给value 就有问题,你的default value不会生效
// 这个是双向绑定的,而且 shouldComponentUpdate 不能阻止 子组件的重新渲染,一旦context改变所有的消费者组件都将重新渲染- class.contextType
传统语法
const MyContext = React.createContext(defaultValue);
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
便捷语法
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。- context.consumer
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
// 如果是多级嵌套的之只拿里这个context最近的Provider提供的值,如果没有就是default value- 下面这个api是对dev tools工具友好的
// context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中示例
需求:我们要做一个主题可定制化的组件,要求就是会用context
// theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
theme: themes.dark,// 默认值
toggleTheme: () => {},
});
// themed-button.js
import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;
// 从content中获取一个函数 theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
return (
<ThemeContext.Consumer>
{
({theme, toggleTheme}) => (
<button onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)
}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
// app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
import ThemeTogglerButton from './theme-toggler-button';
// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
Change Theme
</ThemedButton>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
}
render() {
// 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,
// 而外部的组件使用默认的 theme 值
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
<ThemeTogglerButton />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);- 消费多个值
这东西不推荐,如果要消费多值,可以写到一个组件的渲染函数中去 calss.contextType那种
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');
// 用户登录 context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// 提供初始 context 值的 App 组件
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 一个组件可能会消费多个 context
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}注意如何避免重新渲染的发生
答案简单,就是把state提到父组件去
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}Ref有什么用?怎么用?
这东西可以使你直接操作组件或者dom
直接换发到DOM上
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>;
注意
不推荐使用Ref
非常简单的功能Fragments
这东西主要是解决这个问题
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
如果是使用Fragments
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
// 如果你需要加key 你可以这样
class Columns extends React.Component {
render() {
return (
<React.Fragment key={yourKey}>
<td>Hello</td>
<td>World</td>
<React.Fragment/>
);
}
}
逻辑复用和UI复用(封装)
高阶组件还是相对比较简单的东西,我们来这篇文档就够了
prop的几种常见的设计模式
render-props 它可以把特定行为或功能封装成一个组件,提供给其他组件使用让其他组件拥有这样的能力
- 要实现这样的方式设计方式,首先我们需要使用 拿到公用的组件的状态state,在使用组件时,添加一个值为函数的prop,通过函数参数来获取需要被提取出来公共的状态

- 如何渲染任意的ui呢?也非常的简单,有了数据直接渲染就好了

class Mouse extends React.Component {
// 鼠标位置状态
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount(){
window.addEventListener('mousemove',this.handleMouseMove)
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
render(){
// 向外界提供当前子组件里面的数据,重点,使用这个方法的返回值,就能渲染任意结构的ui了!秒啊,而且也为逻辑的操作都是内部实现的!完全解耦
return this.props.render(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
App
<Mouse render={mouse => {
return <p>X{mouse.x}Y{mouse.y}</p>
}}/>
</div>
)
}
}
ReactDOM.render(<App />,document.getElementById('root'))- 优化注意事项
- 我们一般使用children来代替redner属性名
- 推荐把props添加上校验规则
- 性能角度 ,组件销毁的时候要解绑事件



class Mouse extends React.Component {
// 鼠标位置状态
state = {
x: 0,
y: 0
}
// 监听鼠标移动事件
componentDidMount(){
window.addEventListener('mousemove',this.handleMouseMove)
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
},
componentWillUnmount(){
window.removeEvenListener("mousemove",this.handleMouserMove)
}
render(){
// 向外界提供当前子组件里面的数据,重点,使用这个方法的返回值,就能渲染任意结构的ui了!秒啊,而且也为逻辑的操作都是内部实现的!完全解耦
return this.props.children(this.state)
}
}
class App extends React.Component {
render() {
return (
<div>
App
<Mouse children={mouse => {
return <p>X{mouse.x}Y{mouse.y}</p>
}}/>
</div>
)
}
}
ReactDOM.render(<App />,document.getElementById('root'))高阶的组件封装
HOC高阶组件实际上是一个工具,实现状态逻辑复用,采取包装模式
高阶组件(HOC、Higher-Order Component) 是一个函数,接收要包装的组件,返回增强后的组件
核心技术
- <font style="color:rgba(0, 0, 0, 0.75);">接收要包装的组件,返回增强后的组件</font>
- 
- 高级组件内部创建了一个类组件
- 
使用说明
- 创建一个函数,名称约定以with开头
- 指定函数参数,参数应该以大写字母开头
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面
包装函数
// 定义一个函数,在函数内部创建一个相应类组件
function withMouse(WrappedComponent) {
// 该组件提供复用状态逻辑
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
// 事件的处理函数
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 当组件挂载的时候进行事件绑定
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 当组件移除时候解绑事件
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
// 在render函数里面返回传递过来的组件,把当前组件的状态设置进去
return <WrappedComponent {...this.state} />
}
}
return Mouse
}加强组件
function Position(props) {
return (
<p> // 这里的prop实际上就是我们的高阶函数里的state
X:{props.x}
Y:{props.y}
</p>
)
}
// 把position 组件来进行包装
let MousePosition = withMouse(Position)
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
高阶组件
<MousePosition></MousePosition>
</div>
)
}
}优化和改进
使用高阶组件存在如下的问题,
- 存在两个同名称的组件,没法区分(在dev-tools中)

为什么出现这个问题?
// 在render函数里面返回传递过来的组件,把当前组件的状态设置进去
return <WrappedComponent {...this.state} />
}
}
return Mouse // 你看都返回了Mouse解决方案,设置名字

这里的name就是组件的名称,注意啊,这个getDispalyName是不写在外面的!写在index中
import React from 'react'
import ReactDOM from 'react-dom'
/*
高阶组件
*/
import img from './images/cat.png'
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 控制鼠标状态的逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
return <WrappedComponent {...this.state} />
}
}
// 设置displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
return Mouse
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => (
<p>
鼠标当前位置:(x: {props.x}, y: {props.y})
</p>
)
// 猫捉老鼠的组件:
const Cat = props => (
<img
src={img}
alt=""
style={{
position: 'absolute',
top: props.y - 64,
left: props.x - 64
}}
/>
)
// 获取增强后的组件:
const MousePosition = withMouse(Position)
// 调用高阶组件来增强猫捉老鼠的组件:
const MouseCat = withMouse(Cat)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
{/* 渲染增强后的组件 */}
<MousePosition />
<MouseCat />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
- 我们还有问题,就是prop传参的是也是一个问题
- 问题:如果没有传递props,会导致props丢失问题
- 解决方式: 渲染WrappedComponent时,将state和props一起传递给组件

import React from 'react'
import ReactDOM from 'react-dom'
/*
高阶组件
*/
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 控制鼠标状态的逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
console.log('Mouse:', this.props)
return <WrappedComponent {...this.state} {...this.props} />
}
}
// 设置displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
return Mouse
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => {
console.log('Position:', props)
return (
<p>
鼠标当前位置:(x: {props.x}, y: {props.y})
</p>
)
}
// 获取增强后的组件:
const MousePosition = withMouse(Position)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
<MousePosition a="1" />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
总结
- 组件通讯是构建React应用必不可少的一环
- props的灵活性让组件更加强大
- 状态提升是React组件的常用模式
- 组件生命周期有助于理解组件的运行过程
- 钩子函数让开发者可以在特定的时机执行某些功能
- render props 模式和高阶组件都可以实现组件状态逻辑的复用
- 组件极简模型: (state,props) => UI
关于部分的性能优化问题
我们可以通过一个特殊的生命周期函数,解决render多次非必要渲染所带来的性能问题,
shoulCompentUpadta(){}
如果这个函数返回的是fasle表示不会调用render函数进行页面的重新渲染,这个函数有两个参数
上一个的prop,最新的prop,我们只需要对比两个是不是一样就好了,不一样就渲染,一样就不渲染
目前这个东西,在React的更高版本中,好像是不能用了,于是还有另外的技术手段来解决这个问题