# Hooks

image

HooksReact v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。

使用Hooks,你可以在将含有state的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试。同时,Hooks可以帮助你在不重写组件结构的情况下复用这些逻辑。所以,它也可以作为一种实现状态逻辑复用的方案。

阅读下面的章节使用Hook的动机你可以发现,它可以同时解决MixinHOC带来的问题。

# 官方提供的Hooks

# State Hook

我们要使用class组件实现一个计数器功能,我们可能会这样写:

export default class Count extends 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>
    )
  }
}

通过useState,我们使用函数式组件也能实现这样的功能:

export default function HookTest() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
        Click me
        </button>
    </div>
  );
}

useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。

# Effect Hook

Effect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作

参数

useEffect方法接收传入两个参数:

  • 1.回调函数:在第组件一次render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。
  • 2.状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。
  useEffect(() => {
    // 只要组件render后就会执行
  });
  useEffect(() => {
    // 只有count改变时才会执行
  },[count]);

回调返回值

useEffect的第一个参数可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect之前,会调用这个函数。这个函数常常用来对上一次调用useEffect进行清理。

export default function HookTest() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('执行...', count);
    return () => {
      console.log('清理...', count);
    }
  }, [count]);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
        Click me
        </button>
    </div>
  );
}

执行上面的代码,并点击几次按钮,会得到下面的结果:

image

注意,如果加上浏览器渲染的情况,结果应该是这样的:

 页面渲染...1
 执行... 1
 页面渲染...2
 清理... 1
 执行... 2
 页面渲染...3
 清理... 2
 执行... 3
 页面渲染...4
 清理... 3
 执行... 4

那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。

你可以尝试下面的代码可能更好理解

    var flag = 1;
    var clean;
    function effect(flag) {
      return function () {
        console.log(flag);
      }
    }
    clean = effect(flag);
    flag = 2;
    clean();
    clean = effect(flag);
    flag = 3;
    clean();
    clean = effect(flag);

    // 执行结果

    effect... 1
    clean... 1
    effect... 2
    clean... 2
    effect... 3

模拟componentDidMount

componentDidMount等价于useEffect的回调仅在页面初始化完成后执行一次,当useEffect的第二个参数传入一个空数组时可以实现这个效果。

function useDidMount(callback) {
  useEffect(callback, []);
}

官方不推荐上面这种写法,因为这有可能导致一些错误。

模拟componentWillUnmount

function useUnMount(callback) {
  useEffect(() => callback, []);
}

不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。

# ref Hook

使用useRef Hook,你可以轻松的获取到domref

export default function Input() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

注意useRef()并不仅仅可以用来当作获取ref使用,使用useRef产生的refcurrent属性是可变的,这意味着你可以用它来保存一个任意值。

模拟componentDidUpdate

componentDidUpdate就相当于除去第一次调用的useEffect,我们可以借助useRef生成一个标识,来记录是否为第一次执行:

function useDidUpdate(callback, prop) {
  const init = useRef(true);
  useEffect(() => {
    if (init.current) {
      init.current = false;
    } else {
      return callback();
    }
  }, prop);
}

# 使用Hook的注意事项

# 使用范围

  • 只能在React函数式组件或自定义Hook中使用Hook

Hook的提出主要就是为了解决class组件的一系列问题,所以我们能在class组件中使用它。

# 声明约束

  • 不要在循环,条件或嵌套函数中调用Hook。

Hook通过数组实现的,每次useState 都会改变下标,React需要利用调用顺序来正确更新相应的状态,如果useState 被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误。

我们可以安装一个eslint插件来帮助我们避免这些问题。

// 安装
npm install eslint-plugin-react-hooks --save-dev
// 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

# 自定义Hook

像上面介绍的HOCmixin一样,我们同样可以通过自定义的Hook将组件中类似的状态逻辑抽取出来。

自定义Hook非常简单,我们只需要定义一个函数,并且把相应需要的状态和effect封装进去,同时,Hook之间也是可以相互引用的。使用use开头命名自定义Hook,这样可以方便eslint进行检查。

下面我们看几个具体的Hook封装:

# 日志打点

我们可以使用上面封装的生命周期Hook

const useLogger = (componentName, ...params) => {
  useDidMount(() => {
    console.log(`${componentName}初始化`, ...params);
  });
  useUnMount(() => {
    console.log(`${componentName}卸载`, ...params);
  })
  useDidUpdate(() => {
    console.log(`${componentName}更新`, ...params);
  });
};

function Page1(props){
  useLogger('Page1',props);
  return (<div>...</div>)
}

# 修改title

根据不同的页面名称修改页面title:

function useTitle(title) {
  useEffect(
    () => {
      document.title = title;
      return () => (document.title = "主页");
    },
    [title]
  );
}
function Page1(props){
  useTitle('Page1');
  return (<div>...</div>)
}

# 双向绑定

我们将表单onChange的逻辑抽取出来封装成一个Hook,这样所有需要进行双向绑定的表单组件都可以进行复用:

function useBind(init) {
  let [value, setValue] = useState(init);
  let onChange = useCallback(function(event) {
    setValue(event.currentTarget.value);
  }, []);
  return {
    value,
    onChange
  };
}
function Page1(props){
  let value = useBind('');
  return <input {...value} />;
}

当然,你可以向上面的HOC那样,结合contextform来封装一个更通用的双向绑定,有兴趣可以手动实现一下。

# 使用Hook的动机

# 减少状态逻辑复用的风险

HookMixin在用法上有一定的相似之处,但是Mixin引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。

在不遵守约定的情况下使用HOC也有可能带来一定冲突,比如props覆盖等等,使用Hook则可以避免这些问题。

# 避免地狱式嵌套

大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用HOC,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。

# 让组件更容易理解

在使用class组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。

# 使用函数代替class

相比函数,编写一个class可能需要掌握更多的知识,需要注意的点也越多,比如this指向、绑定事件等等。另外,计算机理解一个class比理解一个函数更快。Hooks让你可以在classes之外使用更多React的新特性。

# 理性的选择

实际上,Hookreact 16.8.0才正式发布Hook稳定版本,笔者也还未在生产环境下使用,目前笔者在生产环境下使用的最多的是HOC

React官方完全没有把classesReact中移除的打算,class组件和Hook完全可以同时存在,官方也建议避免任何“大范围重构”,毕竟这是一个非常新的版本,如果你喜欢它,可以在新的非关键性的代码中使用Hook

# 小结

mixin已被抛弃,HOC正当壮年,Hook初露锋芒,前端圈就是这样,技术迭代速度非常之快,但我们在学习这些知识之时一定要明白为什么要学,学了有没有用,要不要用。不忘初心,方得始终。

阅读全文