关于react.js:React组件复用的发展史

49次阅读

共计 18994 个字符,预计需要花费 48 分钟才能阅读完成。

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,它们一直变得更形象,直到没有人理解它们如何工作。

相干 React 实战视频解说:进入学习

高阶组件

高阶组件(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 计划来实现组件之间复用状态逻辑,会很容易造成“嵌套天堂”。

  • 业务逻辑扩散在组件的各个办法中
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了。

正文完
 0