Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React组件复用 #31

Open
ceerqingting opened this issue Nov 26, 2019 · 0 comments
Open

React组件复用 #31

ceerqingting opened this issue Nov 26, 2019 · 0 comments

Comments

@ceerqingting
Copy link
Owner

ceerqingting commented Nov 26, 2019

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

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

  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方案来实现组件之间复用状态逻辑,会很容易形成“嵌套地狱”。

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

随着应用功能的扩大,组件也会变复杂,逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。比如,组件常常在componentDidMountcomponentDidUpdate中获取数据。但是,同一个componentDidMount中可能也包含很多其它逻辑,如设置事件监听,而之后需在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生bug,并且导致逻辑不一致。测试也难以测试。

  • 难以理解的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了。

参考链接:

React-代码复用(mixin.hoc.render props)

React Hooks 原理

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant