React系列 --- 从Mixin到HOC再到HOOKS(四)

React系列

React系列 --- 简单模拟语法(一)
React系列 --- Jsx, 合成事件与Refs(二)
React系列 --- virtualdom diff算法实现分析(三)
React系列 --- 从Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement与Component部分源码解析(五)
React系列 --- 从使用React了解Css的各类使用方案(六)
React系列 --- 从零构建状态管理及Redux源码解析(七)
React系列 --- 扩展状态管理功能及Redux源码解析(八)html

Mixins(已废弃)

这是React初期提供的一种组合方案,经过引入一个公用组件,而后能够应用公用组件的一些生命周期操做或者定义方法,达到抽离公用代码提供不一样模块使用的目的.node

曾经的官方文档demo以下react

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.map(clearInterval);
  },
};

var TickTock = React.createClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return { seconds: 0 };
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the mixin
  },
  tick: function() {
    this.setState({ seconds: this.state.seconds + 1 });
  },
  render: function() {
    return <p>React has been running for {this.state.seconds} seconds.</p>;
  },
});

React.render(<TickTock />, document.getElementById('example'));

可是Mixins只能应用在createClass的建立方式,在后来的class写法中已经被废弃了.缘由在于:git

  1. mixin引入了隐式依赖关系
  2. 不一样mixins之间可能会有前后顺序甚至代码冲突覆盖的问题
  3. mixin代码会致使滚雪球式的复杂性

详细介绍mixin危害性文章可直接查阅Mixins Considered Harmfulgithub

高阶组件(Higher-order component)

HOC是一种React的进阶使用方法,大概原理就是接收一个组件而后返回一个新的继承组件,继承方式分两种算法

属性代理(Props Proxy)

最基本的实现方式segmentfault

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

从代码能够看出属性代理方式其实就是接受一个 WrappedComponent 组件做为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件数组

抽离state && 操做props

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        name: 'PropsProxyHOC',
      };
    }

    logName() {
      console.log(this.name);
    }

    render() {
      const newProps = {
        name: this.state.name,
        logName: this.logName,
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

class Main extends Component {
  componentDidMount() {
    this.props.logName();
  }

  render() {
    return <div>PropsProxyHOC</div>;
  }
}

export default PropsProxyHOC(Main);

demo代码能够参考这里
有种常见的状况是用来作双向绑定浏览器

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { fields: {} };
    }

    getField(fieldName) {
      const _s = this.state;
      if (!_s.fields[fieldName]) {
        _s.fields[fieldName] = {
          value: '',
          onChange: event => {
            this.state.fields[fieldName].value = event.target.value;
            // 强行触发render
            this.forceUpdate();
            console.log(this.state);
          },
        };
      }

      return {
        value: _s.fields[fieldName].value,
        onChange: _s.fields[fieldName].onChange,
      };
    }

    render() {
      const newProps = {
        fields: this.getField.bind(this),
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <input type="text" {...this.props.fields('name')} />;
  }
}

export default PropsProxyHOC(Main);

demo代码能够参考这里性能优化

获取被继承refs实例

由于这是一个被HOC包装过的新组件,因此想要在HOC里面获取新组件的ref须要用些特殊方式,可是无论哪一种,都须要在组件挂载以后才能获取到.而且不能在无状态组件(函数类型组件)上使用 ref 属性,由于无状态组件没有实例。

经过父元素传递方法获取

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      const newProps = {};
      // 监听到有对应方法才生成props实例
      typeof this.props.getInstance === 'function' && (newProps.ref = this.props.getInstance);
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  componentWillMount() {
    console.log('componentWillMount: ', this.wrappedInstance);
  }

  componentDidMount() {
    console.log('componentDidMount: ', this.wrappedInstance);
  }

  // 提供给高阶组件调用生成实例
  getInstance(ref) {
    this.wrappedInstance = ref;
  }

  render() {
    return <HOCComponent getInstance={this.getInstance.bind(this)} />;
  }
}

export default ParentComponent;

demo代码能够参考这里

经过高阶组件当中间层

相比较上一方式,须要在高阶组件提供设置赋值函数,而且须要一个props属性作标记

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    // 返回ref实例
    getWrappedInstance = () => {
      if (this.props.withRef) {
        return this.wrappedInstance;
      }
    };

    //设置ref实例
    setWrappedInstance = ref => {
      this.wrappedInstance = ref;
    };

    render() {
      const newProps = {};
      // 监听到有对应方法才赋值props实例
      this.props.withRef && (newProps.ref = this.setWrappedInstance);
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  componentWillMount() {
    console.log('componentWillMount: ', this.refs.child);
  }

  componentDidMount() {
    console.log('componentDidMount: ', this.refs.child.getWrappedInstance());
  }

  render() {
    return <HOCComponent ref="child" withRef />;
  }
}

export default ParentComponent;

demo代码能够参考这里

forwardRef

React.forwardRef 会建立一个React组件,这个组件可以将其接受的 ref 属性转发到其组件树下的另外一个组件中。这种技术并不常见,但在如下两种场景中特别有用:

  • 转发 refs 到 DOM 组件
  • 在高阶组件中转发 refs
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

如下是对上述示例发生状况的逐步解释:

  1. 咱们经过调用 React.createRef 建立了一个 React ref 并将其赋值给 ref 变量。
  2. 咱们经过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,做为其第二个参数。
  4. 咱们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

劫持渲染

最简单的例子莫过于loading组件了

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return this.props.isLoading ? <div>Loading...</div> : <WrappedComponent {...this.props} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  constructor() {
    super();
    this.state = {
      isLoading: true,
    };
  }

  render() {
    setTimeout(() => this.setState({ isLoading: false }), 2000);
    return <HOCComponent isLoading={this.state.isLoading} />;
  }
}

export default ParentComponent;

固然也能用于布局上嵌套在其余元素输出
demo代码能够参考这里

反向继承(Inheritance Inversion)

最简单的demo代码

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    render() {
      return super.render();
    }
  };
}

在这里WrappedComponent成了被继承的那一方,从而能够在高阶组件中获取到传递组件的全部相关实例

获取继承组件实例

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    componentDidMount() {
      console.log('componentDidMount: ', this);
    }

    render() {
      return super.render();
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  constructor() {
    super();
    this.state = {
      name: 'WrappedComponent',
    };
  }

  render() {
    return <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);

demo代码能够参考这里

修改props和劫持渲染

再讲解demo以前先科普React的一个方法

React.cloneElement(
  element,
  [props],
  [...children]
)

以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。
React.cloneElement() 几乎等同于:

<element.type {...element.props} {...props}>{children}</element.type>

可是,这也保留了组件的 ref。这意味着当经过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

相比属性继承来讲,反向继承修改props会比较复杂一点

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    constructor() {
      super();
      this.state = {
        a: 'b',
      };
    }

    componentDidMount() {
      console.log('componentDidMount: ', this);
    }

    render() {
      const wrapperTree = super.render();
      const newProps = {
        name: 'NewComponent',
      };
      const newTree = React.cloneElement(wrapperTree, newProps, wrapperTree.props.children);
      console.log('newTree: ', newTree);
      return newTree;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);

demo代码能够参考这里

为何须要用到cloneElement方法?

由于render函数内其实是调用React.creatElement产生的React元素,尽管咱们能够拿到这个方法可是没法修改它.能够用getOwnPropertyDescriptors查看它的配置项
图片描述
因此用cloneElement建立新的元素替代

相比较属性继承来讲,后者只能条件性选择是否渲染WrappedComponent,可是前者能够更加细粒度劫持渲染元素,能够获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render),可是依旧不能保证WrappedComponent里的子组件是否渲染,也没法劫持.

注意

静态属性失效

由于高阶组件返回的已经不是原组件了,因此原组件的静态属性方法已经没法获取,除非你主动将它们拷贝到返回组件中

渲染机制

由于高阶组件返回的是新组件,里面的惟一标志也会变化,因此不建议在render里面也调用高阶组件,这会致使其每次都从新卸载再渲染,即便它可能长得同样.
因此建议高阶组件都是无反作用的纯函数,即相同输入永远都是相同输出,不容许任何有可变因素.

嵌套过深

在原组件中若是包裹层级过多会产生相似回调地狱的烦恼,难以调试,可阅读性糟糕

遵照规则

若是没有规范状况下,也可能形成代码冲突覆盖的局面

HOOKS

Hooks是React v16.7.0-alpha中加入的新特性。它可让你在class之外使用state和其余React特性。

Hooks是可让你与React状态以及函数式组件的生命周期特性“挂钩”的函数。钩子是为了让你抛弃类使用React的,因此它不能在类中运行,可是能够用在纯函数中,这就解决了一直以来可能由于须要用到生命周期或者react状态的时候,你不得不将本来的纯函数代码整个替换成Class写法的烦恼.

Hooks也分两种

State Hook

可以让你在不使用Class的状况下使用state和其余的React功能

useState

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

等价于下面Class写法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>Click me</button>
      </div>
    );
  }
}

demo代码能够参考这里
从上面能够看出useState实际上就是在state里声明一个变量而且初始化了一个值并且提供一个能够改变对应state的函数.由于在纯函数中没有this.state.count的这种用法,因此直接使用count替代
上面的count就是声明的变量,setCount就是改变变量的方法.
须要注意的一点是useState和this.state有点不一样,它[color=#ff4753]只有在组件第一次render才会建立状态,以后每次都只会返回当前的值[/color].

赋值初始值的时候若是须要通过某些逻辑处理才能获得的话,能够经过函数传递,例如

const [count, setCount] = useState(() => doSomethings())

若是改变须要根据以前的数据变化,能够经过函数接收旧数据,例如

setCount(prevCount => prevCount + 1)

若是是想声明多个state的时候,就须要使用屡次useState

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}

或者经过组合对象一次合并多个数据

Effect Hook

执行有反作用的函数,你能够把 useEffect Hooks 视做 componentDidMountcomponentDidUpdatecomponentWillUnmount 的结合,[color=#ff4753]useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行[/color]。React 将在组件更新前刷新上一轮渲染的 effect。React 组件中的 side effects 大体能够分为两种

不须要清理

有时咱们想要在 React 更新过 DOM 以后执行一些额外的操做。好比网络请求、手动更新 DOM 、以及打印日志都是常见的不须要清理的 effects

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidMount() {
    console.log(`componentDidMount: You clicked ${this.state.count} times`);
  }

  componentDidUpdate() {
    console.log(`componentDidUpdate: You clicked ${this.state.count} times`);
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>Click me</button>
      </div>
    );
  }
}

如上所示,若是放在render的话在挂载前也会触发,可是为了不这个问题咱们不得不在两个生命周期写一样的代码.可是若是咱们换成HOOKS的写法

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

demo代码能够参考这里

useEffect 作了什么? 经过这个 Hook,React 知道你想要这个组件在每次 render 以后作些事情。React 会记录下你传给 useEffect 的这个方法,而后在进行了 DOM 更新以后调用这个方法。但咱们一样也能够进行数据获取或是调用其它必要的 API。

为何 useEffect 在组件内部调用?useEffect 放在一个组件内部,可让咱们在 effect 中,便可得到对 count state(或其它 props)的访问,而不是使用一个特殊的 API 去获取它。

useEffect 是否是在每次 render 以后都会调用? 默认状况下,它会在第一次 render 以后的每次 update 后运行。React 保证每次运行 effects 以前 DOM 已经更新了。

使用上还有哪些区别? 不像 componentDidMount 或者 componentDidUpdateuseEffect 中使用的 effect 并不会阻滞浏览器渲染页面。咱们也提供了一个单独的 useLayoutEffect来达成这同步调用的效果。它的 API 和 useEffect 是相同的。

须要清理的 Effect

比较常见的就相似挂载的时候监听事件或者开启定时器,卸载的时候就移除.

class Example extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    document.addEventListener('click', this.clickFunc, false);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.clickFunc);
  }

  clickFunc(e) {
    //  doSomethings
    console.log(e);
  }

  render() {
    return <button>click me!</button>;
  }
}

换成HOOKS写法相似,只是会返回新的函数

function Example() {
  useEffect(() => {
    document.addEventListener('click', clickFunc, false);
    return () => {
      document.removeEventListener('click', clickFunc);
    };
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e);
  }

  return <button>click me!</button>;
}

demo代码能够参考这里

咱们为何在 effect 中返回一个函数 这是一种可选的清理机制。每一个 effect 均可以返回一个用来在晚些时候清理它的函数。这让咱们让添加和移除订阅的逻辑彼此靠近。它们是同一个 effect 的一部分!

React 究竟在何时清理 effect? React 在每次组件 unmount 的时候执行清理。然而,正如咱们以前了解的那样,effect 会在每次 render 时运行,而不是仅仅运行一次。这也就是为何 React 会在执行下一个 effect 以前,上一个 effect 就已被清除。

咱们能够修改一下代码看看effect的运行机制

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('addEventListener');
    document.addEventListener('click', clickFunc, false);
    return () => {
      console.log('removeEventListener');
      document.removeEventListener('click', clickFunc);
    };
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e);
    setCount(count + 1);
  }

  return <button>click me! {count}</button>;
}

demo代码能够参考这里
能够看到上面代码在每次更新都是从新监听,想要避免这种状况能够往下继续看.

进阶使用

有时候咱们可能有多套逻辑写在不一样的生命周期里,若是换成HOOKS写法的话咱们能够按功能划分使用多个,React将会按照指定的顺序应用每一个effect。

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`You clicked ${count} times`);
  });

  useEffect(() => {
    document.addEventListener('click', clickFunc, false);
    return () => {
      document.removeEventListener('click', clickFunc);
    };
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

demo代码能够参考这里

为何Effects会在每次更新后执行

若是大家之前使用class的话可能会有疑惑,为何不是在卸载阶段执行一次.从官网解释代码看

componentDidMount() {
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

componentWillUnmount() {
  ChatAPI.unsubscribeFromFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

它在挂载阶段监听,移除阶段移除监听,每次触发就根据this.props.friend.id作出对应处理.可是这里有个隐藏的bug就是[color=#ff4753]当移除阶段的时候获取的this.props.friend.id多是旧的数据,引发的问题就是卸载时候会使用错误的id而致使内存泄漏或崩溃[/color],因此在class的时候通常都会在componentDidUpdate 作处理

componentDidUpdate(prevProps) {
  // Unsubscribe from the previous friend.id
  ChatAPI.unsubscribeFromFriendStatus(
    prevProps.friend.id,
    this.handleStatusChange
  );
  // Subscribe to the next friend.id
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

可是若是咱们换成HOOKS的写法就不会有这种bug

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

这是由于[color=#ff4753]HOOKS会在应用下一个effects以前清除前一个effects[/color],此行为默认状况下确保一致性,并防止因为缺乏更新逻辑而在类组件中常见的错误

经过跳过effects提高性能

就在上面咱们知道每次render都会触发effects机制可能会有性能方面的问题,在class的写法里咱们能够经过componentDidUpdate作选择是否更新

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

而在useEffect里咱们能够经过传递一组数据给它做为第二参数,若是在下次执行的时候该数据没有发生变化的话React会跳过当次应用

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

因此上面提到的bug案例能够经过这个方式作解决

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

注意

若是你想使用这种优化方式,请确保数组中包含了全部外部做用域中会发生变化且在 effect 中使用的变量,不然你的代码会一直引用上一次render的旧数据.

若是你想要effects只在挂载和卸载时各清理一次的话,能够传递一个空数组做为第二参数.至关于告诉React你的effects不依赖于任何的props或者state,因此不必重复执行.

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一个 memoized 回调函数。
把内联回调函数及依赖项数组做为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给通过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将很是有用。
useCallback(fn, deps) 至关于 useMemo(() => fn, deps)。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于以前的 state 等。而且,使用 useReducer 还能给那些会触发深更新的组件作性能优化,由于你能够向子组件传递 dispatch 而不是回调函数 。

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

demo代码能够参考这里
从语法上大家会看到还有一个init的入参,是用来作惰性初始化,将 init 函数做为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg),这么作能够将用于计算 state 的逻辑提取到 reducer 外部,这也为未来对重置 state 的 action 作处理提供了便利

const initialState = 0;
function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState, init);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>Reset</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

demo代码能够参考这里

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。
把“建立”函数和依赖项数组做为参数传入 useMemo,它仅会在某个依赖项改变时才从新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操做,诸如反作用这类的操做属于 useEffect 的适用范畴,而不是 useMemo。
若是没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

demo代码能够参考这里
本质上,useRef 就像是能够在其 .current 属性中保存一个可变值的“盒子”。
你应该熟悉 ref 这一种访问 DOM 的主要方式。若是你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则不管该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它能够很方便地保存任何可变值,其相似于在 class 中使用实例字段的方式。
这是由于它建立的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的惟一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变动 .current 属性不会引起组件从新渲染。若是想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要使用回调 ref 来实现。

HOOKS规范

在顶层调用HOOKS

不要在循环,条件,或者内嵌函数中调用.这都是为了保证你的代码在每次组件render的时候会按照相同的顺序执行HOOKS,而这也是可以让React在多个useState和useEffect执行中正确保存数据的缘由

只在React函数调用HOOKS

  • React函数组件调用
  • 从自定义HOOKS中调用

能够确保你源码中组件的全部有状态逻辑都是清晰可见的.

自定义HOOKS

咱们能够将相关逻辑抽取出来

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

我必须以“use”开头为自定义钩子命名吗? 这项公约很是重要。若是没有它,咱们就不能自动检查钩子是否违反了规则,由于咱们没法判断某个函数是否包含对钩子的调用。

使用相同钩子的两个组件是否共享状态? 不。自定义钩子是一种重用有状态逻辑的机制(例如设置订阅并记住当前值),可是每次使用自定义钩子时,其中的全部状态和效果都是彻底隔离的。

自定义钩子如何得到隔离状态? 对钩子的每一个调用都处于隔离状态。从React的角度来看,咱们的组件只调用useStateuseEffect

问题

Hook 会替代 render props 和高阶组件吗?

一般,render props 和高阶组件只渲染一个子节点。咱们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它本身的 DOM 结构)。但在大部分场景下,Hook 足够了,而且可以帮助减小嵌套。

生命周期方法要如何对应到 Hook?

  • constructor:函数组件不须要构造函数。你能够经过调用 useState 来初始化 state。若是计算的代价比较昂贵,你能够传一个函数给 useState。
  • getDerivedStateFromProps:改成在渲染时安排一次更新。
  • shouldComponentUpdate:详见 React.memo.
  • render:这是函数组件体自己。
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 能够表达全部这些的组合。
  • componentDidCatch and getDerivedStateFromError:目前尚未这些方法的 Hook 等价写法,但很快会加上。

我能够只在更新时运行 effect 吗?

这是个比较罕见的使用场景。若是你须要的话,你能够 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染仍是后续渲染,而后在你的 effect 中检查这个标识。

如何获取上一轮的 props 或 state?

目前,你能够经过ref来手动实现:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  );
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

有相似 forceUpdate 的东西吗?

若是先后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引发从新渲染。
一般,你不该该在 React 中修改本地 state。然而,做为一条出路,你能够用一个增加的计数器来在 state 没变的时候依然强制一次从新渲染:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}

我该如何测量 DOM 节点?

要想测量一个 DOM 节点的位置或是尺寸,你可使用 callback ref。每当 ref 被附加到另外一个节点,React 就会调用 callback。

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    <div>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
    </div>
  );
}

function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}

demo代码能够参考这里
使用 callback ref 能够确保 即使子组件延迟显示被测量的节点 (好比为了响应一次点击),咱们依然可以在父组件接收到相关的信息,以便更新测量结果。

注意到咱们传递了 [] 做为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,所以 React 不会在非必要的时候调用它。

我该如何实现 shouldComponentUpdate?

你能够用 React.memo 包裹一个组件来对它的 props 进行浅比较:

const Button = React.memo((props) => {
  // 你的组件
});

React.memo 等效于 PureComponent,但它只比较 props。(你也能够经过第二个参数指定一个自定义的比较函数来比较新旧 props。若是函数返回 true,就会跳过更新。)

React.memo 不比较 state,由于没有单一的 state 对象可供比较。但你也可让子节点变为纯组件,或者 用useMemo优化每个具体的子节点。

如何惰性建立昂贵的对象?

第一个常见的使用场景是当建立初始 state 很昂贵时,为避免从新建立被忽略的初始 state,咱们能够传一个函数给 useState,React 只会在首次渲染时调用这个函数

function Table(props) {
  // createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

你或许也会偶尔想要避免从新建立 useRef() 的初始值。useRef 不会像 useState 那样接受一个特殊的函数重载。相反,你能够编写你本身的函数来建立并将其设为惰性的:

function Image(props) {
  const ref = useRef(null);

  //  IntersectionObserver 只会被惰性建立一次
  function getObserver() {
    let observer = ref.current;
    if (observer !== null) {
      return observer;
    }
    let newObserver = new IntersectionObserver(onIntersect);
    ref.current = newObserver;
    return newObserver;
  }

  // 当你须要时,调用 getObserver()
  // ...
}

Hook 会由于在渲染时建立函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差异。
除此以外,能够认为 Hook 的设计在某些方面更加高效:

  • Hook 避免了 class 须要的额外开支,像是建立类实例和在构造函数中绑定事件处理器的成本。
  • 符合语言习惯的代码在使用 Hook 时不须要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中很是广泛。组件树小了,React 的工做量也随之减小。

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。

  • useCallback Hook 容许你在从新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工做:
  • useMemo Hook 使控制具体子节点什么时候更新变得更容易,减小了对纯组件的须要。
  • 最后,useReducer Hook 减小了对深层传递回调的须要,就以下面解释的那样。

如何避免向下传递回调?

在大型的组件树中,咱们推荐的替代方案是经过 contextuseReducer 往下传一个 dispatch 函数:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提示:`dispatch` 不会在从新渲染之间变化
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

TodosApp 内部组件树里的任何子节点均可以使用 dispatch 函数来向上传递 actions

function DeepChild(props) {
  // 若是咱们想要执行一个 action,咱们能够从 context 中获取 dispatch。
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return <button onClick={handleClick}>Add todo</button>;
}

总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。

React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,咱们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 一样只会在 React 组件中被调用)。每一个组件内部都有一个「记忆单元格」列表。它们只不过是咱们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),而后把指针移动到下一个。这就是多个 useState() 调用会获得各自独立的本地 state 的缘由。

相关文章
相关标签/搜索