web前端 - react 类组件与函数组件

168 阅读6分钟

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>
  );
}

而函数组件目前还不具备官方支持的错误边界的处理方案,这是类组件和函数组件在能力边界上一点点小小的区别。