React的组件复用的发展史

React
287
0
0
2023-01-08

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进行增删改查操作,如下:

  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

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

参考 React面试题详细解答

  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}} props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange) // 运行第一个effect

// Update with { friend: {id: 200}} props
ChatAPI.unsubscribeToFriendStatus(100, handleStatusChange) // 清除上一个effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange) // 运行下一个effect

// Update with { friend: {id: 300}} props
ChatAPI.unsubscribeToFriendStatus(200, handleStatusChange) // 清除上一个effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange) // 运行下一个effect

// Unmount
ChatAPI.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了。