写点什么

React 组件复用的发展史

  • 2022-11-08
    浙江
  • 本文字数:13572 字

    阅读完需:约 45 分钟

Mixins

React Mixin 通过将共享的方法包装成 Mixins 方法,然后注入各个组件来实现,官方已经不推荐使用,但仍然可以学习一下,了解为什么被遗弃。


React MiXin 只能通过 React.createClass()来使用,如下:


const mixinDefaultProps = {}const ExampleComponent = React.createClasss({  mixins: [mixinDefaultProps],  render: function(){}})
复制代码

Mixins 实现

import React from 'react'
var createReactClass = require('create-react-class')
const mixins = { onMouseMove: function(e){ this.setState({ x: e.clientX, y: e.clientY }) }}
const Mouse = createReactClass({ mixins: [mixins], getInitialState: function() { return { x: 0, y: 0 } }, render() { return (<div onMouseMove={this.onMouseMove} style={{height: '300px'}}> <p>the current mouse position is ({this.state.x},{this.state.y})</p> </div>) }})
复制代码

Mixins 问题

  • Mixins 引入了隐式的依赖关系


你可能会写一个有状态的组件,然后你的同事可能添加一个读取这个组件statemixin。几个月之后,你可能希望将该state移动到父组件,以便与其兄弟组件共享。你会记得更新这个mixin来读取props而不是state吗?如果此时,其它组件也在使用这个mixin呢?


  • Mixins 引起名称冲突


无法保证两个特定的mixin可以一起使用。例如,如果FluxListenerMixinWindowSizeMixin都定义来handleChange(),则不能一起使用它们。同时,你也无法在自己的组件上定义具有此名称的方法。


  • Mixins 导致滚雪球式的复杂性


每一个新的需求都使得mixins更难理解。随着时间的推移,使用相同mixin的组件变得越来越多。任何mixin的新功能都被添加到使用该mixin的所有组件。没有办法拆分 mixin 的“更简单”的部分,除非或者引入更多依赖性和间接性。逐渐,封装的边界被侵蚀,由于很难改变或者删除现有的 mixins,它们不断变得更抽象,直到没有人了解它们如何工作。

高阶组件

高阶组件(HOC)是 React 中复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。


高阶组件是参数为组件,返回值为新组件的函数


组件是将props转换为UI,而高阶组件是将组件转换为另一个组件。


const EnhancedComponent = higherOrderComponent(WrappedComponent)
复制代码

HOC 的实现

  • Props Proxy: HOC 对传给 WrappedComponent 的 props 进行操作

  • Inheritance Inversion HOC 继承 WrappedComponent,官方不推荐

Props Proxy

import React from 'react'
class Mouse extends React.Component { render() { const { x, y } = this.props.mouse return ( <p>The current mouse position is ({x}, {y})</p> ) }}
class Cat extends React.Component { render() { const { x, y } = this.props.mouse return (<div style={{position: 'absolute', left: x, top: y, backgroundColor: 'yellow',}}>i am a cat</div>) }}
const MouseHoc = (MouseComponent) => { return class extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } onMouseMove = (e) => { this.setState({ x: e.clientX, y: e.clientY }) } render() { return ( <div style={{height: '300px'}} onMouseMove={this.onMouseMove}> <MouseComponent mouse={this.state}/> </div> )
} }}
const WithCat = MouseHoc(Cat)const WithMouse = MouseHoc(Mouse)
const MouseTracker = () => { return ( <div> <WithCat/> <WithMouse/> </div> )}
export default MouseTracker
复制代码


请注意:HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。


在 Props Proxy 模式下,我们可以做什么?


  • 操作 Props


在 HOC 里面可以对 props 进行增删改查操作,如下:


参考 React 实战视频讲解:进入学习


  const MouseHoc = (MouseComponent, props) => {    props.text = props.text + '--I can operate props'   return class extends React.Component {      render() {        return (          <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>            <MouseComponent {...props} mouse={this.state} />          </div>        )      }  }
MouseHoc(Mouse, { text: 'some thing...' })
复制代码


  • 通过 Refs 访问组件


  const MouseHoc = (MouseComponent) => {    return class extends React.Component {      ...      render() {        const props = { ...this.props, mouse: this.state }        return (          <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>            <MouseComponent {...props}/>          </div>        )      }    }  }
class Mouse extends React.Component { componentDidMounted() { this.props.onRef(this) } render() { const { x, y } = this.props.mouse return ( <p>The current mouse position is ({x}, {y})</p> ) } }
const WithMouse = MouseHoc(Mouse)
class MouseTracker extends React.Component { onRef(WrappedComponent) { console.log(WrappedComponent)// Mouse Instance } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <WithMouse mouse={this.state} ref={this.onRef}/> </div> ) } }
复制代码


  • 提取 state


  <MouseComponent mouse={this.state}/>
复制代码


  • 包裹 WrappedComponent


  <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>    <MouseComponent {...props}/>  </div>
复制代码

Inheritance Inversion

该模式比较少见,一个简单的例子如下:


  function iiHOC(WrappedComponent) {    return class WithHoc extends WrappedComponent {      render() {        return super.render()      }    }  }
复制代码


Inheritance Inversion允许 HOC 通过 this 访问到 WrappedComponent,意味着它可以访问到 state、props、组件生命周期方法和 render 方法,HOC 可以增删改查 WrappedComponent 实例的 state,这会导致 state 关系混乱,容易出现 bug。要限制 HOC 读取或者添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 一起


class Mouse extends React.Component {  render(props) {    const { x, y } = props    return (      <p>The current mouse position is ({x}, {y})</p>    )  }}
const MouseHoc = (MouseComponent) => { return class extends MouseComponent { constructor(props) { super(props) this.state = { x: 0, y: 0 } } onMouseMove = (e) => { this.setState({ x: e.clientX, y: e.clientY }) } render() { const props = { mouse: this.state } return ( <div style={{height: '300px'}} onMouseMove={this.onMouseMove}> {super.render(props)} </div> ) } }}
const WithMouse = MouseHoc(Mouse)
复制代码

HOC 约定

  • 将不相关的 props 传递给被包裹组件


HOC为组件添加特性。自身不应该大幅改变约定。HOC返回的组件与原组件应保持类似的接口。


HOC应该透传与自身无关的props。大多数 HOC 都应该包含一个类似于下面的render方法:


  render() {    // 过滤掉专用于这个高阶组件的 props 属性,且不要进行透传    const { extraProp, ...passThroughProps } = this.props
// 将 props 注入到被包裹的组件中 // 通常为 state 的值或者实例方法 const injectedProp = someStateOrInstanceMethod
// 将 props 传递给被包装组件 return ( <WrappedComponent injectedProp = {injectedProp} {...passThroughProps} /> )
}
复制代码


这中约定保证来HOC的灵活性以及可复用性。


  • 最大化可组合性


并不是所有的HOC都一样,有时候它仅接受一个参数,也就是被包裹的组件:


  const NavbarWithRouter = withRouter(Navbar)
复制代码


HOC通常可以接收多个参数。比如在Relay中,HOC额外接收来一个配置对象用于指定组件数据依赖:


  const CommentWithRelay = Relay.createContainer(Comment, config)
复制代码


最常见的HOC签名如下:


// React Redux的`connect`函数const ConnectedComment = connect(commentSelector, commentActions)(CommentList)
// 拆开来看// connnect是一个函数,它的返回值为另外一个函数const enhance = connect(commentListSelector, commentListActions)// 返回值为 HOC, 它会返回已经连接 Redux store的组件const ConnectedComment = enhance(CommentList)
复制代码


换句话说,connect是一个返回高阶组件的高阶函数。


这种形式可能看起来令人困惑或者不必要,但是它有一个有用的属性。像connect函数返回的单参数HOC具有签名Component => Component。输出类型与输入类型相同的函数很容易组合在一起。


// 而不是这样const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// 你可以编写组合工具函数const enhance = compose(withRouter, connect(commentSelector))const EnhancedComponent = enhance(WrappedComponent)
复制代码


  • 包装显示名称以便轻松调试


HOC创建的容器组件与任何其他组件一样,会显示在React Developer Tools中。为了方便调试,请选择一个显示名称,已表明是HOC的产品。


比如高阶组件名为withSubscription,被包装组件的显示名称为CommentList,显示名称应该为WithSubscription(CommentList)


  function withSubscription(WrappedComponent) {    class WithSubscription extends React.Component {/*....*/}    WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`    return WithSubscription  }
function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
复制代码

注意事项

  • 不要在render方法中使用 HOC


  render() {    // 每次调用 render 函数都会创建一个新的 EnhancedComponent    // EnhancedComponent1 !== EnhancedComponent2    const EnhancedComponent = enhance(MyComponent)    // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作    return <EnhancedComponent/>  }
复制代码


  • 务必复制静态方法


当你将HOC应用于组件时,原始组件将使用容器组件进行包装,这意味着新组件没有原始组件的任何静态方法。


  // 定义静态方法  WrappedComponent.staticMethod = function(){/*...*/}  // 现在使用 HOC  const EnhancedComponent = enhance(WrappedComponent)
// 增强组件没有 staticMethod typeof EnhancedComponent.staticMethod === 'undefined' // true
复制代码


为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:


  function enhance(WrappedComponent) {    class Enhance extends React.Component {/*...*/}    // 必须准确知道应该拷贝哪些方法    Enhance.staticMethod = WrappedComponent.staticMethod    return Enhance  }
复制代码


但是这样做,你需要知道哪些方法应该被拷贝,你可以使用hoist-non-react-statics自动拷贝所有 React 静态方法:


  import hoistNonReactStatic from 'hoist-non-react-statics'  function enhance(WrappedComponent) {    class Enhance extends React.Component {/*..*/}    hoistNonReactStatic(Enhance, WrappedComponent)    return Enhance  }
复制代码


除了导出组件,另一个可行的方案是再额外导出这个静态方法


  MyComponent.someFunction = someFunction  export default MyComponent
// ...单独导出该方法 export { someFunction }
// ...并在要使用的组件中,import它们 import MyComponent, { someFunction } form './Mycomponent.js'
复制代码


  • Refs 不会被传递


虽然高阶组件约定是将所有 props 传递给被包装组件,但对于 refs 并不适用。因为ref实际上并不是一个 prop,就像key一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

Render Props

“render prop”是指一种 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。


具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑


  <DataProvider render={data => (    <h1>Hello {data.target}</h1>  )}/>
复制代码

Render Props 实现

render props 是一个用于告知组件需要渲染什么内容的函数 prop


class Cat extends React.Component {  render() {    const { x, y } = this.props.mouse     return (<div style={{position: 'absolute', left: x, top: y, backgroundColor: 'yellow',}}>i am a cat</div>)  }}
class Mouse extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } onMouseMove = (e) => { this.setState({ x: e.clientX, y: e.clientY }) } render() { return ( <div style={{height: '300px'}} onMouseMove={this.onMouseMove}> {this.props.render(this.state)} </div> ) }}
export default class MouseTracker extends React.Component { render() { return ( <div> <Mouse render={mouse => { return <Cat mouse={mouse}/> }}/> </div> ) }}
复制代码


有趣的是,你可以使用带有 render prop 的常规组件来实现大多数高阶组件HOC


注意:你不一定要用名为 render的 prop 来使用这种模式。事实上,任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为“render prop”。


尽管之前的例子使用来render,我们可以简单地使用children prop!


<Mouse children={mouse => (  <p>鼠标的位置 {mouse.x}, {mouse.y}</p>)}/>
复制代码


记住,children prop 并不真正需要添加到 JSX 元素的“attributes”列表中。你可以直接放在元素内部!


<Mouse> {mouse => (  <p>鼠标的位置 {mouse.x}, {mouse.y}</p>  )}</Mouse>
复制代码


由于这一技术的特殊性,当你在涉及一个类似的 API 时,建议在你的 propTypes 里声明 children 的类型应为一个函数。


  Mouse.propTypes = {    children: PropTypes.func.isRequired  }
复制代码


将 Render props 与 React.PureComponent 一起使用时要小心


  class Mouse extends React.PureComponent {    // ...  }
class MouseTracker extends React.Component { render() { return ( <div> { // 这是不好的!每个渲染的`render`prop的值将会是不同的 } <Mouse render={mouse => { <Cat mouse={mouse}/> }}/> </div> ) } }
复制代码


在上述例子中,每次<MouseTracker>渲染,它会生成一个新的函数作为<Mouse render>的 prop, 所以同时抵消了继承自React.PureComponent<Mouse>组件的效果。


可以定义一个 prop 作为实例方法:


  class MouseTracker extends React.Component {    renderTheCat(mouse) {      return <Cat mouse={mouse}/>    }    render() {      return (        <div>          <Mouse render={this.renderTheCat}/>        </div>      )    }  } 
复制代码

高阶组件和 render props 问题

  • 很难复用逻辑,会导致组件树层级很深


如果使用 HOC 或者 render props 方案来实现组件之间复用状态逻辑,会很容易形成“嵌套地狱”。


  • 业务逻辑分散在组件的各个方法中


class FriendStatusWithCounter extends React.Component {  constructor(props) {    super(props);    this.state = { count: 0, isOnline: null };    this.handleStatusChange = this.handleStatusChange.bind(this);  }
componentDidMount() { document.title = `You clicked ${this.state.count} times`; ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }
handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }
复制代码


随着应用功能的扩大,组件也会变复杂,逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。比如上面代码,设置 document.title 的逻辑被分割到 componentDidMount 和 componentDidUpdate 中的,订阅逻辑又被分割到 componentDidMount 和 componentWillUnmount 中的。而且 componentDidMount 中同时包含了两个不同功能的代码。


  • 难以理解的 class


需要学习 class 语法,还要理解 Javascript 中this的工作方式,这与其它语言存在巨大差异。还不能忘记绑定事件处理。对于函数组合和 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来问题,比如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。

Hooks

Hook 是 React 16.8 点新增特性,它可以让你在不编写 class 的情况下使用 state 以及其它的 React 特性。

Hooks 实现

使用 State Hoook

import React, { useState } from 'react'
function Example() { const [count, setCount] = useState(0) return ( <div> <p>you clicked {count} times</p> <button onClick={() => setCount(count+1)}> Click me </button> </div> )}
复制代码


声明多个 state 变量


function ExampleWithManyStates() {  // 声明多个 state 变量  const [age, setAge] = useState(42)  const [fruit, setFruit] = useState('banana')  const [todos, setTodos] = useState([{text: 'Learn Hooks'}])}
复制代码


调用 useState 方法的时候做了什么?


它定义了一个“state 变量”。我们可以叫他任何名称,与 class 里面的this.state提供的功能完全相同。


useState 需要哪些参数?


useState()方法里面唯一的参数就是初始 state,可以使用数字或字符串,而不一定是对象。


useState 方法的返回值是什么?


返回值为:当前 state 以及更新 state 的函数。

使用 Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作


数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。


你可以把useEffect Hook看做componentDidMount,componentDidUpdatecomponentWillUnmount这三个函数组合。在 React 组件中,有两种常见副作用操作:需要清除的和不需要清除的。


  • 无需清除的 effect


有时候,我们只想在 React 更新 DOM 之后运行一些额外代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。


import React, { useState, useEffect } from 'react'
function Example() { const [count, setCount] = useState(0)
// 与 componentDidMount 和 componentDidUpdate 相似 useEffect(() => { // 使用浏览器 API 更新文档标题 document.title = `You clicked ${count} times` })
return ( <div> <p>you clicked {count} times</p> <button onClick={() => setCount(count+1)}> Click me </button> </div> )}
复制代码


useEffect 做了什么?


通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数,并且在执行 DOM 更新之后调用它。


为什么在组件内部调用 useEffect


useEffect放在组件内部让我们可以在 effect 中直接访问countstate 变量(或其它 props)。这里 Hook 使用了 JavaScript 的闭包机制。


useEffect 会在每次渲染后都执行吗?


是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。


useEffect 函数每次渲染中都会有所不同?


是的,这是刻意为之的。事实上这正是我们刻意在 effect 中获取最新的count的值,而不用担心过期的原因。因为每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分————每个 effect“属于”一次特定的渲染。


提示:与componentDidMountcomponentDidUpdate不同,使用useEffect调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步执行。在个别情况下(例如测量布局),有单独的useLayoutEffectHook 供你使用,其 API 与useEffect相同。


  • 需要清除的 effect


例如订阅外部数据源,这种情况下,清除工作是非常重要的,可以防止引起内存泄漏。


function Example() {  const [count, setCount] = useState(0)  const [width, setWidth] = useState(document.documentElement.clientWidth)
useEffect(() => { document.title = `You clicked ${count} times` })
useEffect(() => { function handleResize() { setWidth(document.documentElement.clientWidth) } window.addEventListener('resize', handleResize) return function cleanup() { window.removeEventListener('resize', handleResize) } })
return ( <div> <p>you clicked {count} times</p> <button onClick={() => setCount(count+1)}> Click me </button> <p>screen width</p> <p>{width}</p> </div> )}
复制代码


为什么要在 effect 中返回一个函数?


这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。


React 何时清除 effect?


React 会在组件卸载的时候执行清除操作。effect 在每次渲染的时候都会执行,在执行当前 effect 之前会对上一个 effect 进行清除。


注意:并不是必须为 effect 中返回的函数命名,也可以返回一个箭头函数或者起别的名称。

为什么每次更新的时候都要运行 Effect

如下是一个用于显示好友是否在线的FriendStatus组件。从 class 中 props 读取friend.id,然后组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅。


  componentDidMount() {    ChatAPI.subscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )  }  componentWillUnmount() {    ChatAPI.unsubscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )  }
复制代码


但是当组件已经现在屏幕上,friend prop 发生变化时会发生什么?我们组件将继续展示原来的好友状态,这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄漏或崩溃的问题。


在 class 组件中,我们需要添加componentDidUpdate来解决这个问题。


  componentDidMount() {    ChatAPI.subscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )  }  componentDidUpdate(prevProps) {    // 取消订阅之前的friend.id    ChatAPI.unsubscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )    // 订阅新的friend.id    ChatAPI.subscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )  }  componentWillUnmount() {    ChatAPI.unsubscribeToFriendStatus(      this.props.friend,id,      this.handleStatusChange    )  }
复制代码


如果使用 Hook 的话:


function FriendStatus(props) {  useEffect(() => {    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)    return () => {      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange)    }  })}
复制代码


它并不会收到此 bug 影响,因为useEffect默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。具体调用序列如下:


// Mount with { friend: {id: 100}} propsChatAPI.subscribeToFriendStatus(100, handleStatusChange) // 运行第一个effect
// Update with { friend: {id: 200}} propsChatAPI.unsubscribeToFriendStatus(100, handleStatusChange) // 清除上一个effectChatAPI.subscribeToFriendStatus(200, handleStatusChange) // 运行下一个effect
// Update with { friend: {id: 300}} propsChatAPI.unsubscribeToFriendStatus(200, handleStatusChange) // 清除上一个effectChatAPI.subscribeToFriendStatus(300, handleStatusChange) // 运行下一个effect
// UnmountChatAPI.unsubscribeToFriendStatus(200, handleStatusChange) // 清除最后一个effect
复制代码


通过跳过 Effect 进行性能优化


在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在componentDidUpdate中添加对prevPropsprevState的比较逻辑解决:


  componentDidUpdate(prevProps, prevState) {    if (prevState.count !== this.state.count) {      document.title = `You clicked ${count} times`    }  }
复制代码


对于useEffect来说,只需要传递数组作为useEffect的第二个可选参数即可:


  useEffect(() => {    document.title = `You clicked ${count} times`  }, [count])
复制代码


如果组件重新渲染时,count没有发生改变,则 React 会跳过这个 effect,这样就实现了性能的优化。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。


对于有清除操作的 effect 同样适用:


  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline)    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatuschange)    return () => {      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatuschange)    }  }, [props.friend.id]) // 仅在props.friend.id发生变化时,重新订阅
复制代码


注意:如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。

Hook 规则

  • 只在最顶层使用Hook



不要在循环,条件或嵌套函数中调用 Hook,这样能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的useStateuseEffect调用之间保持 hook 状态的正确。


  • 只在React函数中使用Hook



不要在普通的 Javascript 函数中调用 Hook

自定义 Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。


比如,我们有如下组件显示好友的在线状态:


import React, { useState, useEffect } from 'react'
function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null)
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline) } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return () => { ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange) } }) if(isOnline === null) { return 'Loading...' } return isOnline ? 'Online' : 'Offline'}
复制代码


现在假设聊天应用中有一个联系人列表,当用户在线时把名字设置为绿色。我们可以把类似的逻辑复制并粘贴到FriendListItem组件中来,但这并不是理想的解决方案:


import React, { useState, useEffect } from 'react'
function FriendListItem(props) { const [isOnline, setIsOnline] = useState(null)
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline) } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return () => { ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange) } }) return ( <li style={{ color: isOnline ? 'green': 'black'}}> {props.friend.name} </li> )}
复制代码

提取自定义 Hook

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以同样也适用这种方式。


自定义 Hook 是一个函数,其名称以“use”开头,函数内部可以调用其它的 Hook.


import React, { useState, useEffect } from 'react'
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null)
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline) } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange) return () => { ChatAPI.unsubscribeToFriendStatus(friendID, handleStatusChange) } }) return isOnline}
复制代码


所以,之前的FriendStatusFriendListItem组件可以改写成如下:


function FriendStatus(props) {  const isOnline = useFriendStatus(props.friend.id)  if(isOnline === null) {    return 'Loading...'  }  return isOnline ? 'Online' : 'Offline'}
复制代码


function FriendListItem(props) {  const isOnline = useFriendStatus(props.friend.id)  return (    <li style={{ color: isOnline ? 'green': 'black'}}>    {props.friend.name}    </li>  )}
复制代码


这段代码等价于原来的示例代码吗?


等价,它的工作方式完全一样。自定义 Hook 是一种自然遵循 Hook 设计的约定,而不是 React 的特性


自定义 Hook 必须以“use”开头吗?


必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查的你的 Hook 是否违反了 Hook 的规则。


在两个组件中使用相同的 Hook 会共享 state 吗?


不会。每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

React Hooks 原理

上伪代码:

useState

import React from 'react'import ReactDOM from 'react-dom'
let _state
function useState(initialValue) { _state = _state || initialValue
function setState(newState) { _state = newState render() } return [_state, setState]}
function App() { let [count, setCount] = useState(0) return ( <div> <div>{count}</div> <button onClick={() => { setCount(count + 1) }}>点击</button> </div> )}
const rootElement = document.getElementById('root')
function render() { ReactDOM.render(<App/>, rootElement)}
render()
复制代码

useEffect

let _deps
function useEffect(callback, depArray) { const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true if (!depArray || hasChangedDeps) { callback() _deps = depArray }}
复制代码


useEffect(() => {  console.log(count)})
复制代码

Not Magic, just Arrays

以上代码虽然实现了可以工作的useStateuseEffect,但是都只能使用一次。比如:


const [count, setCount] = useState(0)const [username, setUsername] = useState('fan')
复制代码


count 和 usename 永远相等,因为他们共用一个_state,所以我们需要可以存储多个_state 和_deps。我们可以使用数组来解决 Hooks 的复用问题。


如果所有_state 和_deps 存放在一个数组,我们需要有一个指针能标识当前取的是哪一个的值。


import React from 'react'import ReactDOM from 'react-dom'
let memorizedState = []let cursor = 0 //指针
function useState(initialValue) { memorizedState[cursor] = memorizedState[cursor] || initialValue const currentCursor = cursor function setState(newState) { memorizedState[currentCursor] = newState render() } return [memorizedState[cursor++], setState]}
function useEffect(callback, depArray) { const hasChangedDeps = memorizedState[cursor] ? !depArray.every((el, i) => el === memorizedState[cursor][i]) : true if (!depArray || hasChangedDeps) { callback() memorizedState[cursor] = depArray } cursor++}
function App() { let [count, setCount] = useState(0) const [username, setUsername] = useState('hello world') useEffect(() => { console.log(count) }, [count]) useEffect(() => { console.log(username) }, []) return ( <div> <div>{count}</div> <button onClick={() => { setCount(count + 1) }}>点击</button> </div> )}
const rootElement = document.getElementById('root')
function render() { cursor = 0 ReactDOM.render(<App/>, rootElement)}
render()
复制代码


到这里,我们就可以实现一个任意复用的useStateuseEffect了。


用户头像

还未添加个人签名 2022-09-05 加入

还未添加个人简介

评论

发布
暂无评论
React组件复用的发展史_React_夏天的味道123_InfoQ写作社区