学习React Hook 这一篇就够了

625 阅读5分钟

Hook

在react conf 2018上,react发布了一个新的提案hook。那么这个hook到底是个什么呢,官方的定义是这样的

Hooks are a new feature proposal that lets you use state and other React features without writing a class.

那么hook是什么呢。他其实就是Class的另外一种写法。他最大的优点,就是以简洁的方式实现class能够实现的一切。给大家提供一种更简洁的写法。你用class写的模块,完全可以用hook来实现。虽然这么说,但不是说,对于老的系统,我们需要把所有的代码改造成hook。因为hook可以与class完美的共存。

函数式组件

hook起源于函数式组件,但在hook之前,函数式组件就一直存在。那么什么叫函数式组件呢,就是没有自己的state状态,所有的状态,都由父组件决定。

class写法

class Child extends Component{
    render(){
        //code
        return (
            <div>{this.props.count}</div>
        )

    }
}

函数式组件

function Child(props){
	//code
    return (
        <div>{props.count}</div>
    )
}

这就是所谓的函数式组件,他返回的内容就是render的内容。

useState

纯函数式组件,意义不大。因为我们大部分的组件,都是有状态的。比如说,一个tab组件,tabList,可以由父组件传递,但当前选中态,必须由自己来维护。useState的意义就是取代class组件中的this.statethis.setState、以及constructor生命周期

那么,这个tab组件的以下两种写法对比如下。

class

class Tab extends Component{
    constructor(props){
        super(props);
        this.state = {
            tabList : props.tabList
        }
    }
    render(){
        let {tabList} = this.state;
        return (
            <ul className='tab'>
                {tabList.map((tab , key) => 
                    <li key={key} className={tab.active ? 'active' : ''} onClick={() => {
                        tabList.map((tab , index) => tab.active = key == index);
                    }}>
                        {tab.title}
                    </li>
                )}
            </ul>
        )
    }
}

hook

function Tab(props){
    let [tabList , setTabList] = useState(props.tabList)
    return (
        <ul className='tab'>
            {tabList.map((tab, key) =>
                <li key={key} className={tab.active ? 'active' : ''} onClick={() => {
                    tabList.map((tab, index) => tab.active = key == index);
                    setTabList([...tabList]);
                }}>
                    {tab.title}
                </li>
            )}
        </ul>
    )
}

useState 函数接收一个参数,表示的是tabList的默认值,组件每一次加载时,使用这个默认值,当我们点击tab时,切换会调用setTabList来更新组件状态,这时useState所返回的值,就不是默认值props.tabList,而是setTabList所传入的值。

useState 函数返回一个数组,里面包含两个对象。第一个是对象的值类型tabList,第二个是函数类型setTabList,这里通过es6的数组析构方式赋值。 只要我们调用setTabList,组件就会重新render

useState 的参数只会被调用一次,除了可以接收一个普通值或者对象外,还可以接收一个函数。比如,我们可以通过给他设置一个函数,来达到为Tab组件,设置默认选中第一个的需求。

	let [tabList , setTabList] = useState(() => {
        props.tabList[0].active = true;
        return props.tabList;
    })

注意: hook的组件类型为纯组件,类似于PureComponent,而不是Component,对于setTabList的值,只有在旧对象 === 新对象才会重新render组件,因此我们使用[...tabList]方法去重新生成一个新的对象

useEffect

useEffect(function , array) 主要取代的是componentDidMount componentDidUpdate componentWillUnmount 三个生命周期函数。当然有同学会说,那还有componentWillMount呢。对于这个生命周期,由于,第一他的功能可以被constructor或者componentDidMount 所替代,第二,在React17版本即将被删除,因此,在此不多做叙述。

只加载一次的componentDidMount

还是以Tab 组件为例,如果,我们的tabList来源于服务端,而非父组件的话,我们可以使用 useEffect

	function Tab(props) {
        let [tabList, setTabList] = useState([])
        useEffect(() => {
            fetch(url).then(res => res.json())
            .then(tabList => setTabList(tabList));
        } , [])
    }

我们注意到 useEffect后面,有一个空数组[]。它表示的意思是,组件第一次加载时,会执行useEffect中的方法,当下一次render的时候,如果数组中的值有变化,就继续执行useEffect中的方法,由于空数组永远不会有变化,因些他只执行一次,这样他就完美的替代了componentDidMount函数

注意:如果第二个参数不传,表示这样代码每次render都会执行,这样,就失去了useEffect的意义

加载多次的compontDidUpdate

接下来,我们设计一个计数器功能,每次更新计数,我们都需要更新浏览器的标题

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  } , [count]);

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

这个时候,我们第二参数可以使用 [count] , 当每次更改 count的时候,都会更新 document.title

需要清除的effect

那么,我们什么时候需要清除useEffect呢。举个例子,我们可以在组件加载时componentWillMount监听事件,然后在componentWillUnmount释放所监听事件,useEffect的清除,使用返回一个函数来处理,当组件卸载时,就会执行useEffect所返回的函数

	function Example() {
    	useEffect(()=> {
        	event.on('事件' , ()=> {
            	//处理事件
            });
            return ()=> {
            	event.off('事件');
            }
        } , [])
    
    }

性能优化之useMemo

我们知道react使用虚拟dom,大幅的提升了性能。但有的时候,我们仍然需要componentShouldUpdate,来决定组件,是否要进行虚拟dom的比对。比如下面这个页面

我们看到的是一个多标签的单页应用。上面每个tab代表一个页签,页签下面为页面。我们切换页面的时候,只是简单的将上一个页面display:none,然后将当前页面 display:block,如果我们每次切换,都需要完全比对虚拟dom的话,页面将会相当卡顿,因为页面会非常的多。但不幸的是按照react的原理,以及我们目前的架构,每次切换页面,都会完全虚拟DOM比对,那下我们怎么样去实现class类中的shouldComponentUpdate方法呢?

假如我们需要构造的页面组件为 <Page />

则可以通过如下代码

function MemoPage({ match, Page, name, configList}){
    let memoPage = useMemo(() => <Page name={name} configList={configList} />  , []);
    return (
        <div style={{ display: match ? 'block' : 'none' }} className='main-page'>
            {memoPage}
        </div>
    )
}

顾名思义,useMemo后面也有一个空数组,他的用法与useEffect一致,在这里表示组件,只构建一次。