在 React Native 中使用 Hooks

4,463 阅读6分钟

React官方在 2018 ReactConf 大会上宣布 React v16.7.0-alpha(内测) 将引入 Hooks。什么是Hooks,我们来了解一下。

什么是Hooks?

在平时开发过程中,我们一般都会遇到如下问题:

1. 难以重用和共享组件中的与状态相关的逻辑2. 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面。3. 由于业务变动,函数组件不得不改为类组件等等。

上面说起来比较抽象,接下来我们以 键盘Api Keyboard 为例说明问题。

export default class App extends Component {
 
  constructor(props) {
    super(props);
    this.state = {
      isShowKeyboard: false
    }
  }
 
  static getDerivedStateFromProps() {
    this.keyboardDidShowListener = Keyboard.addListener(
      "keyboardDidShow",
      this.keyboardDidShowHandler
    );
  }
 
  keyboardDidShowHandler () {
    this.setState({
      isShowKeyboard: true
    });
  }
 
  componentWillUnmount() {
    this.keyboardDidShowListener.remove();
  }
 
  render() {
    return (
      <View style={styles.container}>
        <Text>
          当前键盘状态: {this.state.isShowKeyboard}
        </Text>
      </View>
    );
  }
}

上面的代码用例比较简单,使用 Keyboard 注册监听键盘的显示、隐藏状态。可以看到键盘事件的注册,注销,状态的render都放在了Component中,如果当前Component中涉及很多这样的逻辑,会造成当前Component职责非常重,并且状态数据不能共享,当在另一个Component中需要监听键盘事件时,需要重新编写或Copy重复代码,冗余非常严重,对于功能维护和扩展都不是一件好事。 Hooks的出现解决了上面的问题,它允许开发者定义函数组件,也可以使用类组件(class components)的 state 和 组件生命周期,而不需要在 mixin、 函数组件、HOC组件和 render props 之间来回切换。方便我们在业务中实现业务逻辑代码的分离和组件的复用。与使用 setState 相比,组件是没有状态的。来看看使用Hooks的方式:

import { useState, useEffect } from 'react';
import {
    Keyboard
} from 'react-native';
 
export default function keyboard() {
    const [keyboardStatus, setKeyboardStatus] = useState(false);
    Keyboard.addListener(
        "keyboardDidShow",
        this.keyboardDidShowHandler
    );
    useEffect(()=> {
        return ()=> Keyboard.removeListener(
            "keyboardDidShow",
            ()=> setKeyboardStatus(true)
        );
    }, [false]);
    return (
        <Text>
            当前键盘状态:{keyboardStatus}
        </Text>
    )
}

上述代码中将关于键盘的业务逻辑剥离到了函数中,称之为 函数组件。当我们在其他Component中使用时,只需要导入进来即可。在函数组件中,我们使用到了 useState、useEffect,它们作为Hooks中提供的Api,起到了什么作用呢?

Hooks Api

官方提供了 hooks 的三个关键的Api,分别是 State Hooks 、 Effect Hooks 、Context Hooks、 Custom Hooks(自定义hooks)。

useState

useState 这个方法可以为函数组件带来 local state,它接收一个用于初始 state 的值,返回一对变量

// 等价于const keyboardStatus= useState(0)[0];const setKeyboardStatus= useState(0)[1];

理解起来比较简单,其实就是定义 state 状态值,以及修改该 state 状态值的行为函数。

useEffect

useEffect 可以简单的理解为替代如下生命周期:

componentDidMount、componentDidUpdate、componentWillUnmount 

useEffect 的代码既会在第一次初始化时(componentDidMount)执行,也会在后续每次触发 render 渲染时(componentDidUpdate)执行,返回值在组件注销时(componentWillUnmount)执行。结合上面的例子:

useEffect(()=> {
   // return 将会在组件注销时调用
   return ()=> Keyboard.removeListener(
       "keyboardDidShow",
       ()=> setKeyboardStatus(true)
   );
}, [false]);

useEffect 的第二个参数,作为性能优化的设置,决定是否执行里面的操作来避免一些不必要的性能损失。只要第二个参数数组中的成员的值没有改变,就会跳过此次执行。如果传入一个空数组 [ ],那么只会在组件 mount 和 unmount 时期执行。

Context Hooks

React 16.3 版本中发布了全新的Context API。目的为了解决子组件嵌套层级过深,父组件的属性难以传达的问题。使用方式不算复杂,首先要利用 Context API创建一个数据提供者(Provider)和数据消费者(Consumer)。(说到这里有点像Java多线程并发的例子)在提供者所在的地方存入数据,在消费者所在的地方取出数据。简单看下 Context 使用方式: (1)创建上下文环境 

// 创建 Contextimport React from 'react';const DataContext = React.createContext({    name: '',    age: 23});
export default DataContext;

(2)定义数据提供者 Provider

export default class App extends Component {
 
  render() {
    return (
      <DataContext.Provider value={{ name: 'Songlcy', age: 27 }}>
        <CustomComponent />
      </DataContext.Provider>
    );
  }
}

(3)定义数据消费者 Consumer

export default class CustomComponent extends Component {
    render() {
        return (
            <DataContext.Consumer>
                {
                    context => (
                        <Text>
                            我的名字:{context.name}, 我的年龄:{context.age}
                        </Text>
                    )
                }
            </DataContext.Consumer>
        )
    }
}

当组件嵌套层次很深的情况下,Context 的优势就会更为明显。 “诶,醒醒!”..... 说了这么多,继续回到Hooks。上面代码中,从 Context — Provider — Consumer 获取到数据,整个取值过程还是比较繁琐的。当我们要从多个 Consumer 中取值的时候,还要进行函数嵌套,更加麻烦。 useContext 是对 Context API 的简化。来看看简化后的样子:

const { name, age } = useContext(DataContext);

我靠!这就完了?” 是的,取值过程就是这么简单,就是这么任性。再来10个 Consumer 又如何!

Custom Hooks

Custom Hooks 即自定义Hooks行为方式,本身并不是Api。核心概念就是将逻辑提取出来封装到函数中,具体实现就是通过一个函数封装跟状态数据(State)有关的逻辑,将这些逻辑从组件中抽取出来。在这个函数中我们可以使用其他的 Hooks,也可以单独进行测试。修改上面的例子:

export default function useKeyboardStatus() {
    const [keyboardStatus, setKeyboardStatus] = useState(false);
    Keyboard.addListener(
        "keyboardDidShow",
        this.keyboardDidShowHandler
    );
    useEffect(()=> {
        return ()=> Keyboard.removeListener(
            "keyboardDidShow",
            ()=> setKeyboardStatus(true)
        );
    },[]);
    return keyboardStatus;
}

代码几乎相同,唯一区别是函数名称用了 use* 前缀,这里需要遵循一个约定,命名要用 use*

Hooks 工作原理

“神马?Hooks 其实就是一个数组!”

回忆下最初我们使用 useState 时的方式:

const [keyboardStatus, setKeyboardStatus] = useState(false);

其实从这句代码我们也能猜出大致的实现思想:

使用一个类似于 setter 的函数作为hook函数中的第二个数组项返回,而 setter 将控制由hook管理的状态(State),状态由第一个数组项返回。

我们可以理解成有两个数组,分别存放 state、setState对应的方法。 当useState()第一次运行时,将setter函数推送到setter数组,状态推送到state数组。每个setter都有一个对它的光标位置的引用,因此通过触发对任何setter的调用,它将改变状态数组中该位置的状态值。说白了就是有个索引,setter方法根据索引修改对应的状态数据值。来看看伪代码的实现方式: 

let state = []; // 存放state状态数据
let setter = []; // 存放 setXXX方法
let initial = true; // 是否是第一次运行
let index = 0;
 
 
useState(initVal) {
  if (initial) {
    state.push(initVal);
    setter.push(createSetter(index));
    initial = false;
  }
 
  const setter = setter[index];
  const value = state[index];
 
  index++;
  return [value, setter];
}
 
 
createSetter(index) {
  return function setterWithIndex(newVal) {
    state[index] = newVal;
  };
}

具体的源码实现,感兴趣的大家可以去看看。不过不建议每步都弄懂,了解了实现思想就可以了。

总结

状态和相关的处理逻辑可以按照功能进行划分,不必散落在各个生命周期中,大大降低了开发和维护的难度。除了这几个hooks还有其他额外的hooks:Hooks API Reference

最后推荐两个个很牛逼的库:

react-use: 封装了各种 Hooks。

eslint-plugin-react-hooks: Hooks ESLint 插件

一个老外写的很不错的 React Native Hooks 文章:React Hooks Basics— Building a React Native App with React Hooks