本文迁移自老博客。本篇文章系统总结了React
中的组件技术,并总结了一些小而美的案例如页面复用、权限控制、渲染劫持、异步组件、错误边界、埋点上报 等。
先粘一下本文所有案例代码
本文所有案例Demo效果 新窗口打开
React中的组件化技术
为什要讲编程思想?
- 我们每天都在写代码开发软件,如同一万个读者,有一万个哈姆雷特,同一个功能我们每个人的书写的代码都不一样。那么怎样优雅的书写代码可以降低软件的开发维护成本,这其中编程思想就非常重要。
- 组件化技术是编程思想中重要的一部分。
高阶函数在 《JavaScript权威教程》一书中的定义: “所谓的高阶函数就是操作函数的函数,它接收一个或多个函数作为输出,并返回一个新函数”
那么高阶函数有什么用处呢?我们先看一个示例
javascript// 假设有这样一种计算器,输入参数,输出一个对象(包含这个参数的计算情况)
// 这里只是就算参数的个数
function calculator(...nums) { // 计算参数之和
return {
arr: nums,
count: nums.length
}
}
console.log(calculator(1,2,3));
// { arr: [ 1, 2, 3 ], count: 3 }
// 现在有个需求要求扩展该函数,
// 除了计算参数个数,还要计算这些参数的求和与乘积
// 这就是一个高阶函数 增强calculator函数的功能
function enhancer(base, funcs) {
return (...args) => {
const state = base(...args);
funcs.reduce((a, b) => a(b(state)));
return state
}
}
function multi(result) {
result.multi = result.arr.reduce((a, b) => a*b);
return result;
}
function sum(result) {
result.sum = result.arr.reduce((a, b) => a+b);
return result;
}
const complexCalculator = enhancer(calculator, [multi, sum]);
console.log(complexCalculator(1,2,3));
// { arr: [ 1, 2, 3 ], count: 3, sum: 6, multi: 6 }
当然有人会说, 我直接写一个方法岂不是更通俗易懂?
javascriptfunction complexCalculator(...nums) { // 计算参数之和
return {
arr: nums,
count: nums.length,
sum: nums.reduce((a, b) => a+b),
multi: nums.reduce((a, b) => a+b),
}
}
console.log(complexCalculator(1,2,3));
的确是通俗易懂了,但这样的话complexCalculator函数就不具有扩展性和灵活性,当功能越来越多你就会发现前者的优势。
我们可以再发散的想一下,前者可以封装成一个库,提供给别人, 别人无需修改该库也能扩展它的功能,功能就具有了想象力,这就是高阶函数的魅力。
其实上述案例是我根据redux原理杜撰的,如果说这就是redux原理肯定有人喷我。redux原理肯定是比这种情况复杂一些的,但也不是太难。
我们在想一下,如果一个高阶函数B,它的返回值是C函数,C函数执行结果可以还是一个函数,and so on ,,, B作为参数传给另一个组件A,A组件中调用了B组件传入自身,生成C函数继续调用传入自身(A函数)的其他参数,用这种高阶函数的形式,已经实现了两个函数之间的通信,进而B函数扩展了A函数的能力。
这样是不是距离redux原理有更近一步了?
这里是知识提一个引子,说明高阶函数的想象力,具体redux的实现详见我的下一篇文章。 下面我们来看一下高阶组件
⾼阶组件是参数为组件,返回值为新组件的函数。
- 在react中奉行一切皆组件,为了提高组件复用率,可测试性,就要保证组件功能单一性;
- 但是若要满足复杂需求就要扩展功能单⼀的组件,在React里就有了HOC(Higher-Order Components)的概念。
- 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
使用HOC的优点
javascriptimport React, {Component} from "react";
const foo = Cmp => props => {
return (
<div className="border">
<Cmp {...props} />
</div>
);
};
const foo2 = Cmp => props => {
return (
<div className="greenBorder">
<Cmp {...props} />
</div>
);
};
class Child extends Component {
render() {
return <div>Child</div>;
}
}
const Foo = foo2(foo(foo(Child)));
export default class HocPage extends Component {
render() {
return (
<div>
<h3>HocPage</h3>
<Foo />
</div>
);
}
}
高阶组件(HOC)是 React 中⽤于复⽤组件逻辑的一种⾼级技巧。HOC ⾃身不是 React API 的⼀部分,它是⼀种基于 React 的组合特性而形成的设计模式。
装饰器的使用需要安装 @babel/plugin-proposal-decorators
并更新config-overrides.js
配置。
不要在 render 方法中使用 HOC
React 的 diff 算法(称为协调)使⽤组件标识来确定它是应该更新现有⼦树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新⼦树。 如果它们不相等,则完全卸载前一个子树。
jsxrender() {
// 每次调⽤ render 函数都会创建一个新的EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致⼦子树每次渲染都会进行卸载,和重新挂载的操作!
return <EnhancedComponent />;
}
这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有⼦组件的状态丢失。
使用组合的方式,将组件包装在容器上,依赖父子组件的生命周期关系来;
stateless
的函数组件class
组件props
jsxexport default function PropsProxy () {
const [radio, setRadio] = React.useState(2)
return <div>
<label htmlFor="radio1">列表页面A</label>
<input id="radio1" type="radio" value={1} checked={radio === 1}
onChange={() => setRadio(1)}/>
<label htmlFor="radio2">列表页面B</label>
<input id="radio2" type="radio" value={2} checked={radio === 2}
onChange={() => setRadio(2)}/>
{radio === 1 ? <PageA/> : <PageB/>}
</div>
}
const PageA = CommonPageList(
(props) => props.list.map(it => <p>--{it}</p>),
fetchListA
);
const PageB = CommonPageList(
(props) => props.list.map(it => <p>##{it}</p>),
fetchListB
);
function fetchListA() {
return new Promise(resolve => {
setTimeout(() => {
resolve([1,2,3]);
}, 1000)
})
}
function fetchListB() {
return new Promise(resolve => {
setTimeout(() => {
resolve(['A', 'B', 'C']);
}, 300)
})
}
function CommonPageList (WrappedComponent, fetchingMethod, defaultProps = {
loading: '加载中。。。',
emptyText: '没有数据'
}) {
return class extends React.Component {
state = {
list: [],
isRequesting: true,
}
async componentDidMount() {
const list = await fetchingMethod();
this.setState({
list,
isRequesting: false
});
}
render() {
if(this.state.isRequesting) {
return <p>{defaultProps.loading}</p>
}
if(!this.state.list.length) {
return <p>{defaultProps.emptyText}</p>
}
return <WrappedComponent
{...defaultProps}
{...this.props}
list={this.state.list}
/>
}
}
}
javascriptimport React from 'react';
// 属性代理 通过渲染劫持实现权限控制
const apiGetPermission = async () => new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 1000);
});
function PermissionControl(Comp) {
return function(props){
let [state, setData] = React.useState({
hasPermission: false,
isRequesting: true,
});
apiGetPermission().then(res => {
setData((state) => ({
...state,
hasPermission: res,
isRequesting: false,
}))
});
if(state.isRequesting) return <p>页面加载中...</p>
return state.hasPermission ? <Comp {...props}/> : <h2>您访问的页面不存在</h2>
}
}
export default PermissionControl(Comp);
function Comp(props) {
return <h2>个人中心-{props.children}</h2>
}
javascriptfunction AddBorder(Comp) {
return (props) => {
return <div style={{
border: '6px solid #ccc',
display: "inline-block"
}}>
<Comp {...props}/>
</div>
}
}
// @AddBorder // 装饰器写法
function Box(props) {
return <div style={{
backgroundColor: 'red',
color: 'white',
display: 'flex',
height: '200px',
width: '200px',
alignItems: 'center',
justifyContent: 'center',
}}>{props.children}</div>
}
// 属性代理 劫持渲染示例
export default function RenderHijack(props) {
// return <Box>this is box</Box>
const Comp = AddBorder(Box);
return <Comp>this is box<br/>{props.children}</Comp>
}
javascriptimport React from 'react';
// 属性代理之stateless
function WrapperInput(WrappedComponent) {
return class extends React.Component {
state = {
text: ''
}
constructor(props) {
super(props);
}
handleClick = (e) => {
this.setState({
text: e.target.value
})
}
render() {
const newProps = {
onChange: this.handleClick,
value: this.state.text,
}
return <div>
<WrappedComponent
{...newProps}
{...this.props}/>
<p>{this.state.text}</p>
</div>
}
}
}
export default function() {
const MyInput = (props) => {
return <input {...props}/>
}
const Comp = WrapperInput(MyInput)
return <>
<p>属性代理之stateless实现双向数据绑定</p>
<Comp type='text'></Comp>
</>;
}
使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法
jsxconst HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
jsximport React from 'react';
// class组件反向继承 实现性能上报
function PerformanceMonitoring (Comp) {
return class extends Comp {
state = {
start: Date.now(),
time: 0,
}
constructor(props) {
super(props);
}
componentDidMount() {
super.componentDidMount?.();
this.setState({
time: Date.now() - this.state.start
});
console.log('todo 性能上报')
}
render() {
return <>
{super.render()}
<h2>组件渲染耗时{this.state.time}ms</h2>
</>
}
}
}
class HeavyComp extends React.Component {
state = {}
constructor(props) {
super(props);
}
static getDerivedStateFromProps(props, state) {
const total = Fibonacci(props.count ?? 35);
return {
total
}
}
render() {
return <h2> Fibonacci (n) = {this.state.total}</h2>
}
}
export default PerformanceMonitoring(HeavyComp);
jsxclass HeavyComp extends React.Component {
state = {}
constructor(props) {
super(props);
}
static getDerivedStateFromProps(props, state) {
const total = Fibonacci(props.count ?? 35);
return {
total
}
}
render() {
return <h2> Fibonacci (n) = {this.state.total}</h2>
}
}
export default PerformanceMonitoring(HeavyComp);
属性代理和反向继承对比
随着项目的扩张,代码包也随之增长, 为避免因体积过大导致加载时间过程 React16.6中引入了React.lazy和React.Susperse两个API, 再配合动态import()语法就可以实现组件代码打包分割和异步加载。
jsx/**
* 异步组件
* 随着项目的扩张,代码包也随之增长,
* 为避免因体积过大导致加载时间过程
* React16.6中引入了React.lazy和React.Susperse两个API,
* 再配合动态import()语法就可以实现组件代码打包分割和异步加载。
*/
import React, {lazy, Suspense} from 'react';
import { useEffect } from 'react';
const About = lazy(() => import(/* webpackChunkName:'about' */ './About'));
// const About = lazy(
// () => new Promise(resolve => {
// setTimeout(() => {
// resolve(import(/* webpackChunkName:'about' */ './About'))
// }, 500);
// })
// );
export default function (){
const [hasLoad, setHasLoad] = React.useState(false);
return hasLoad ? <LazyComp/> :
<button onClick={
() => setHasLoad(true)
}>
加载异步组件
</button>
}
function LazyComp (){
return <div>
<Suspense fallback={<div>loading</div>}>
<About/>
</Suspense>
</div>
}
Suspense组件需要等待异步组件加载完成再渲染异步组件的内容。
类似vue中的插槽技术
React V 16中引入,部分UI的JS错误不会导致整个应用崩溃; 错误边界是一种 React 组件,错误边界在 渲染期间、生命周期方法和整个组件树的构造函数 中捕获错误,且会渲染出备用UI而不是崩溃的组件。
jsximport React from 'react';
export default function() {
return <div>
<ErrorBoundary>
<h2>APP1</h2>
<Child></Child>
</ErrorBoundary>
<ErrorBoundary>
<h2>APP2</h2>
<Child></Child>
</ErrorBoundary>
</div>
}
class Child extends React.Component {
state = {
count: 1
}
render() {
if(this.state.count === 3) {
throw new Error('I crashed!');
}
return <>
<p>{this.state.count}</p>
<button onClick={
() => this.setState({count: this.state.count + 1})
}>add</button>
</>
}
}
class ErrorBoundary extends React.Component {
state = {
hasError: false,
}
static getDerivedStateFromError(error) {
// 更新UI
return {
hasError: true,
}
}
componentDidMount(error, errorInfo) {
// todo 这里进行错误上报
// 你同样可以将错误日志上报给服务器
console.log(error, errorInfo);
}
render() {
if(this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
jsximport React from 'react';
export default function () {
return <ExposureDisappear className="container"
exposure={e => console.log('上报A页面曝光')}
disappear={e => console.log('上报A页面消失')}>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
<h2>AAA</h2>
</ExposureDisappear>
}
class ExposureDisappear extends React.Component {
constructor(props) {
super(props);
this.id = `${Date.now()}-${Number.parseInt(Math.random() * 1000)}`;
}
componentDidMount() {
this.el = document.getElementById(this.id);
this.observer = new IntersectionObserver(([inst]) => {
if(inst.intersectionRatio === 0) {
this.props?.disappear();
} else {
this.props?.exposure();
}
}, {
root: document.documentElement,
rootMargin: '0px',
threshold: [0] // 目标元素与根元素相交程度触发cb - [0 - 1]
});
this.observer.observe(this.el);
document.addEventListener('visibilitychange', this.visibilitychange);
}
visibilitychange = ()=> {
setTimeout(() => {
if(document.visibilityState !== 'visible') {
this.el.setAttribute('style', 'display: none');
} else {
this.el.setAttribute('style', '');
}
}, 20)
}
componentWillUnmount() {
this.observer?.disconnect();
this.props?.disappear();
document.removeEventListener('visibilitychange', this.visibilitychange);
}
render() {
return <div id={this.id}>
{this.props.children}
</div>;
}
}
jsfunction Comp() {
function handleClick() {
setCount(c => c + 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
function handleClick2() {
fetchSomething().then(() => {
// react< 18 会渲染两次
setCount(c => c + 1);
setFlag(f => !f);
});
}
// 若不想batching ?
function handleClick3() {
fetchSomething().then(() => {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
});
}
return <>
<button onClick={handleClick}>Next</button>
</>
}
import {flushSync} from 'react-dom'
jshandleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
console.log(this.state);
// V18前 { count: 1, flag: false }
// V18中 { count: 0, flag: false },除非使用flushSync
this.setState(({ flag }) => ({ flag: !flag }));
});
};
jsimport { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
js// 紧急的更新:展示用户的输入
startTransition(() => {
setInputValue(e.target.value)
});
// 非紧急的更新: 展示结果
setContent(e.target.value);
与 setTimeout 的区别
startTransition
是立即执行的,传给startTransition
的函数是同步运行,但是其内部的所有更新都会标记为非紧急,React将在稍后处理更新时决定如何render
这些updates
,这意味着将会比setTimeout中的更新更早的被renderstartTransition
中的更新是可中断的Hooks是react16.8以后新增的钩子API; 目的:增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!