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 _statefunction 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 _depsfunction 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了。