React中的类组件与函数组件
什么是React组件?
在使用React编写页面时,可以将UI拆分为独立可复用的代码片段,称之为组件。
可以认为组件是React复用UI代码片段的最小单位。
每个组件类似于一个函数,他负责接收输入(props),返回UI。
React组件有哪几种?
目前React有两种组件:函数组件与类组件。
函数组件就是编写一个javascript函数,接收props并返回一个react元素。
function Hello(props) {
return <div>Hello, {this.props.name}</div>
}
类组件是使用ES6的class语法,定义一个类,通过render方法返回react元素。
class Hello extends Component {
constructor(props) {
super(props)
}
render() {
return <div>Hello, {this.props.name}</div>
}
}
类组件与函数组件的相同点
能力边界
函数组件和类组件只是表达的方式不同,两者的能力边界几乎无区别,一个用函数组件可以重构成类组件,而不影响其功能,反之亦然。
组件性能
函数组件与类组件几乎无性能差距,这个在React官方文档中有说明:在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
类组件与函数组件的不同点
设计思想
函数组件是基于函数式编程(FP)的思想,类组件基于面向对象编程(OOP)的思想。
参考来源zh-hans.legacy.reactjs.org/docs/hooks-…
组件状态
我们试着来比较两种组件的组件状态定义和维护的区别:
类组件使用组件的state属性存储状态,通过调用setState方法更新组件状态,进而更新UI
class Counter extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
render() {
<div onClick={this.handleClick}>{count}</div>
}
}
而函数组件是在react16支持hooks以后才拥有的维护组件状态的能力
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => setCount(count => count + 1)
return <div onClick={handleClick}>{count}</div>
}
生命周期与副作用
类组件有对应的生命周期函数,可以将业务逻辑放在生命周期中,在合适的时机执行;函数组件将带有副作用的业务逻辑放在hooks函数中执行。
例如我们需要在组件挂载后执行一段业务逻辑,类组件中的实现是使用componentDidMount这个方法:
class Counter extends Component {
constructor(props){
this.state = {
count: 0,
}
}
componentDidMount() {
console.log('组件挂载')
}
render() {
<div>{this.state.count}</div>
}
}
函数组件使用hooks来执行带有副作用的业务逻辑,我们可以使用useEffect(fn, deps), 如果只需要在组件挂载后执行一次,第二个参数传入一个空数组(代表无任何依赖)即可。
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('组件挂载')
}, [])
return <div>{count}</div>
}
性能优化
react中的每一次更新,都会重新生成一颗新的vDom树,通过和旧的vDom树使用diff算法,标记处需要更新的节点。
react中的性能优化的核心:在构建新的vDom树的过程中,尽可能减少不必要的render,即尽可能复用原有的节点。
类组件可以通过shouldComponentUpdate生命周期,来阻止不必要的render
class Counter extends Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.count === nextProps.count) {
return false
}
return true
}
render() {
<div>{this.props.count}</div>
}
}
React官方提供了PureComponent组件,这个组件通过props和state的浅对比实现了shouldComponentUpdate,多数时候我们可以直接使用pureComponent,而非自己实现shouldComponentUpdate来进行性能优化。
class Counter extends PureComponent {
render() {
return <div>{this.props.count}</div>
}
}
对于函数组件来说,没有生命周期方法,想要减少render来优化性能只能使用React.memo, 前提是你的组件在相同 props 的情况下渲染相同的结果(React.memo只比较props)。
逻辑复用
类组件因为其面向对象的特性,可以使用继承来复用逻辑,但是这样的复用方式相比于组合,非常不灵活,且需要对父类足够了解,而且通常我们在使用React框架时,也很少会做多次继承。
class BaseModalView extends Component {
this.state = {
visible: false
}
show() {
this.setState({ visible: true })
}
hide() {
this.setState({ visible: false })
}
render() { ... }
}
class ResultModalView extends BaseModalView {
constructor(props) {
super(props)
this.state = {
...this.state,
result: { code: 0 }
}
}
render() { ... }
}
类组件与函数组件都可以使用高阶组件(HOC)的方式来复用逻辑,使用这种方式有一定的灵活性,可以1个以上的高阶组件函数组合。
function HOC(WrappedComponent) {
const hocProps = { hoc: true };
return props => <WrappedComponent {...props} {...hocProps}/>;
}
// 包裹一个类组件,复用逻辑
HOC(class A extends Component { ... })
// 包裹一个函数组件,复用逻辑
HOC(function B(props) { ... })
React16.8开始支持了hooks特性,推出hooks的其中一个目的就是解决在组件之间复用状态逻辑很难,函数组件可以利用hooks这个特性实现逻辑复用。
// 这个例子封装了一个获取当前时间的hook函数,且每1s自动刷新一次值
function useTime() {
let timer = null
const [time, setTime] = useState(new Date())
useEffect(() => {
timer = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
}
相比高阶组件,或者renderProps, 或者更早的mixin, 使用hooks来复用逻辑的好处:
1.避免过的嵌套或者回调地域,无论多少个hooks组合在一起使用代码仍然在同一层级。
2.可以将组件中相互关联的部分拆分成更小的函数,避免单个生命周期中因组合了不同功能的代码而增加的维护成本。
3.低耦合,且每个hooks易于单独编写测试用例。
异常处理
当我们组件渲染过程中出现异常时,为防止组件树崩溃,我们需要对错误进行捕获和处理,甚至降级渲染UI。
类组件提供了生命周期方法componentDidCatch, 如果组件渲染阶段发生异常,这个方法会在commit阶段被调用。
class Hello extends Component {
this.state = { hasError: false }
componentDidCatch(error, info) {
console.log(error.stack)
this.setState({ hasError: true })
}
render() {
if (this.state.hasError) {
return <div>component error</div>
}
return <div>Hello, {this.props.user.name}</div>
}
}
在componentDidCatch我们可以获取错误信息和堆栈,进行上报,甚至可以更新state, 让组件在下一次渲染中渲染降级UI。
不过当前的React不推荐在这个生命周期方法中通过更新状态来降级渲染UI,原因是有一个更好的解决方案:使用静态方法getDerivedStateFromError, 这个静态方法会在渲染阶段发生错误时被调用,可以返回一个新的state用于降级渲染,从官方描述上看,相比使用componentDidCatch手动更新状态做降级,使用这个方案不需要重新进行一次schedule -> render -> commit的过程。
以下是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) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 调用示例,当被包裹的Main组件或者其子组件发生错误时,ErrorBoundary会捕获并做降级渲染。
function App() {
return (
<ErrorBoundary>
<Main />
</ErrorBoundary>
);
}
而函数组件目前还不具备官方支持的错误边界的处理方案,这是类组件和函数组件在能力边界上一点点小小的区别。