关于javascript:一文看懂React-Hooks

52次阅读

共计 13895 个字符,预计需要花费 35 分钟才能阅读完成。

一、Hook 简介

React Hooks 是从 React 16.8 版本推出的新个性,目标是解决 React 的状态共享以及组件生命周期管理混乱的问题。React Hooks 的呈现标记着,React 不会再存在无状态组件的状况,React 将只有类组件和函数组件的概念。

家喻户晓,React 利用开发中,组件的状态共享是一件很麻烦的事件,而 React Hook 只共享数据处理逻辑,并不会共享数据自身,因而也就不须要关怀数据与生命周期绑定的问题。如下所示,是应用类组件实现计数器的示例。

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>
    );
  }
}

能够发现,类组件须要本人申明状态,并编写操作状态的办法,并且还须要保护状态的生命周期,显得特地麻烦。如果应用 React Hook 提供的 State Hook 来解决状态,那么代码将会简洁许多,重构后的代码如下所示。

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>
  );
}

能够看到,Example 从一个类组件变成了一个函数组件,此函数组件领有本人的状态,并且不须要调用 setState() 办法也可更新本人的状态。之所以能够如此操作,是因为类组件应用了 useState 函数。

二、根底 Hook

2.1 useState

useState 函数是 React 自带的一个 Hook 函数,而 Hook 函数领有 React 状态和生命周期治理的能力。
能够看到,useState 函数的入参只有一个,就是 state 的初始值,这个初始值能够是数字、字符串、对象,甚至是一个函数,如下所示。

function Example (props) {const [ count, setCount] = useState(() => {return props.count || 0})
    return (
      <div>
        You clicked : {count}
        <button onClick={() => { setCount(count + 1)}}>
          Click me
        </button>
      </div>
      )
  }

并且,当入参是一个函数时,此函数只会在类组件初始渲染的时候才会被执行一次。
如果须要同时对一个 state 对象进行操作,那么能够间接应用函数进行操作,该函数会接管 state 对象的值,而后执行更新操作,如下所示。

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

  function handleClick() {setCount(count + 1)
  }

  function handleClickFn() {setCount((prevCount) => {return prevCount -1})
  }

  return (
      <>
        You clicked: {count}
        <button onClick={handleClick}>+</button>
        <button onClick={handleClickFn}>-</button>
      </>
  );
}

在下面的代码中,handleClick 和 handleClickFn 都是更新的最新的状态值。并且操作同一个状态对象值的时候,为了节约性能,React 会把屡次状态更新进行合并,并一次性的更新状态对象的值。
在 React 利用开发中,当某个组件的状态发生变化时,它会以该组件为根,从新渲染整个组件树,如下所示。

function Child({onButtonClick, data}) {
  return (<button onClick={onButtonClick}>{data.number}</button>
  )
}

function Example () {const [number, setNumber] = useState(0)
  const [name, setName] = useState('hello')  
  const addClick = () => setNumber(number + 1)
  const data = {number}
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child onButtonClick={addClick} data={data} />
    </div>
  )
}

在下面的代码中,子组件援用 number 对象的数据,当父组件的 name 对象的数据发生变化时,尽管子组件没有产生任何变动,它也会执行重绘操作。在我的项目开发中,为了防止这种不必要的子组件反复渲染,须要应用 useMemo 和 useCallback 进行包裹,如下所示。

import {memo, useCallback, useMemo, useState} from "react";

function Child({onButtonClick, data}) {
  return (<button onClick={onButtonClick}>{data.number}</button>
  )
}

Child = memo(Child)

function Example () {const [number, setNumber] = useState(0)
  const [name, setName] = useState('hello') 
  const addClick = useCallback(() => setNumber(number + 1), [number])
  const data = useMemo(() => ({ number}), [number])
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child onButtonClick={addClick} data={data} />
    </div>
  )
}

其中,useMemo 和 useCallback 是 React Hook 提供的两个 API,次要用于缓存数据、优化晋升利用性能。它俩的共同点是,只有当依赖的数据发生变化时,才会调用回调函数去从新计算结果,不同点如下。

  • useMemo:缓存的后果是回调函数返回的值。
  • useCallback:缓存的是函数。因为函数式组件每当 state 发生变化,就会触发整个组件更新,当应用 useCallback 之后,一些没有必要更新的函数组件就会缓存起来。

在下面的示例中,咱们把函数对象和依赖项数组作为参数传入 useMemo,因为应用了 useMemo,所以只有当某个依赖项发生变化时才会从新计算缓存的值。通过 useMemo 和 useCallback 的优化解决后,能够无效防止每次渲染带来的性能开销。

2.2 useEffect

失常状况下,在 React 的函数组件的函数体中,网络申请、模块订阅以及 DOM 操作都属于副作用的领域,官网不倡议开发者在函数体中写这些副作用代码的,而 Effect Hook 就是专门用来解决这些副作用的。上面是应用类组件实现计数器的例子,副作用代码都写在 componentDidMount 和 componentDidUpdate 生命周期函数中,如下所示。

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

  componentDidMount() {document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {document.title = `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>
    );
  }
}

能够看到,componentDidMount 和 componentDidUpdate 两个生命周期函数中的代码是一样的。之所以呈现同样的代码,是因为很多状况下,咱们心愿在组件加载和更新时执行同样的操作。从概念上说,咱们心愿能够对它进行合并解决,遗憾的是类组件病没有提供这样的办法。不过,当初应用 Effect Hook 就能够防止这种问题,如下所示。

import React, {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>
  );
}

事实上,useEffect 只会在每次 DOM 渲染后执行,因而不会阻塞页面的渲染。并且,useEffect 同时具备 componentDidMount、componentDidUpdate 和 componentWillUnmount 等生命周期函数的执行机会。同时,咱们还能够应用 useEffect 在组件外部间接拜访 state 变量或是 props,因而能够在 useEffect 执行函数值的更新操作。
在类组件中,通常会在 componentDidMount 生命周期中设置订阅音讯,并在 componentWillUnmount 生命周期中革除。例如,有一个 ChatAPI 模块,用来订阅好友的在线状态,如下所示。

class FriendStatus extends React.Component {constructor(props) {super(props);
    this.state = {isOnline: null};
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

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

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

  handleStatusChange(status) {
    this.setState({isOnline: status.isOnline});
  }

  render() {if (this.state.isOnline === null) {return 'Loading...';}
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

能够发现,componentDidMount 和 componentWillUnmount 是绝对应的,即在 componentDidMount 生命周期中的设置须要在 componentWillUnmount 生命周期中进行解除。不过,手动解决模块订阅是相当麻烦的,如果应用 Effect 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 function cleanup() {ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {return 'Loading...';}
  return isOnline ? 'Online' : 'Offline';
}

事实上,每个 Effect 都会返回一个革除函数,当 useEffect 的返回值是一个函数的时候,React 会在组件卸载的时候执行一遍革除操作。useEffect 会在每次渲染后执行,但有时候咱们心愿只有在 state 或 props 扭转的状况下才执行渲染,上面是类组件的写法。

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

如果应用 React Hook,只须要传入第二个参数即可,如下所示。

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

能够发现,第二个参数是一个数组,能够将 Effect 用到的所有 props 和 state 都传进去。如果只须要在组件挂载和卸载时才执行,那么第二个参数能够传一个空数组。

除了 useEffect 外,useLayoutEffect 也能够执行副作用和清理操作。不同之处在于,useEffect 会在浏览器渲染实现后执行,而 useLayoutEffect 是在浏览器渲染前执行。

2.3 useContext

在类组件中,组件之间的数据共享是通过属性 props 来实现的。在函数组件中,因为没有构造函数 constructor 和属性 props 的概念,组件之间传递数据只能通过 useContext 来实现。
useContext 是 React Hook 提供的跨级组件数据传递的一种形式,能够很不便的去订阅上下文的扭转,并在适合的时候从新渲染组件。useContext 的应用形式如下。

const value = useContext(MyContext);

能够看到,useContext 接管一个上下文对象 context,并返回该上下文对象的以后值。以后上下文对象的值由下层组件中距离以后组件最近的 数据提供者决定。
useContext 的次要作用就是实现组件之间的数据传递。首先,新建一个命名 Example.js 文件,并增加如下代码。

import {useState,createContext} from "react";

const CountContext = createContext()

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

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={()=>{setCount(count+1)}}>click me</button>
            <CountContext.Provider value={count}>
            </CountContext.Provider>
        </div>
    )
}

在下面的代码中,咱们把 count 变量应用 Provider 包裹起来,即容许它实现跨层级组件值的传递,当父组件的 count 变量发生变化时子组件也会发生变化。
有了上下文变量之后,接下来就能够应用 useContext 接管上下文变量的值了。在 Example.js 文件中新建一个 Counter 组件,用来显示上下文对象 count 变量的值,代码如下。

function Counter(){const count = useContext(CountContext)  
    return (<h2>{count}</h2>)
}

而后,咱们还须要在 <CountContext.Provider> 标签中引入 Counter 组件,如下所示。

<CountContext.Provider value={count}>
    <Counter/>
</CountContext.Provider>

能够发现,应用 useContext 形式在组件之间传递值时,须要应用 Provider 包裹须要传递的变量。

三 其余 Hook

3.1 useReducer

家喻户晓,JavaScript 的 Redux 状态治理框架由 Action、Reducer 和 Store 三个对象形成,而 Reducer 是惟一能够更新组件中 State 的路径。Reducer 实质上是一个函数,它接管两个参数,状态和管制业务逻辑的判断参数,如下所示。

function countReducer(state, action) {switch(action.type) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        default:
            return state;
    }
}

useReducer 是 React Hooks 提供的一个函数,次要用来在某些简单的场景中替换 useState。例如,蕴含简单逻辑的 state 且蕴含多个子值,或者前面的 state 依赖于后面的 state 等。useReducer 的语法格局如下。

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

能够发现,useReducer 的应用形式与 Redux 状态框架时十分类似的,它接管一个形如 (state, action) => newState 的 Reducer,并返回以后 state 以及 dispatch 办法。例如,上面是应用 useReducer 实现计数器的代码。

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 (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

有时候,须要惰性地创立初始 state,那么只须要将初始化函数作为 useReducer 的第三个参数传入即可,如下所示。

function init(initialCount) {return {count: initialCount};
}

function reducer(state, action) {…. // 省略其余代码}

function Example({initialCount}) {const [state, dispatch] = useReducer(reducer, initialCount, init);
    return (…  // 省略其余代码);
}

有时候,Reducer Hook 的返回值与以后 state 的值是雷同的,此时须要跳过子组件的渲染及副作用的执行。
须要留神的是,因为 React 不会对组件树的深层节点进行不必要的渲染,所以不用放心跳过渲染后再次渲染该组件。如果为了防止在渲染期间执行高开销的计算,能够应用 useMemo 进行优化。

3.2 useMemo

在类组件中,每一次状态的更新都会触发组件树的从新绘制,而从新绘制必然会带来不必要的性能开销。同样,在函数组件中,为了防止 useState 每次渲染时带来的高开销计算,React Hook 提供了 useMemo 函数。

useMemo 之所以可能带来性能上的晋升,是因为在依赖不变的状况下,useMemo 会返回雷同的援用,防止子组件进行无意义的反复渲染。例如,上面是一个一般的 useState 的应用示例。

function Example() {const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    
    function expensive() {
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {sum += i;}
        return sum;
    }

    return (
        <>
            {count}:{expensive()}
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </>
    );
}

在下面的示例中,无论是批改 count 还是 val 的值,都会触发 expensive 办法的执行。不过,因为 expensive 办法的执行只依赖于 count 的值,而在批改 val 值的时候是没有必要再次计算的。所以,为了防止这种不必要的计算,能够应用 useMemo 优化下面的代码,如下所示。

function Example() {const [count, setCount] = useState(1);
    const [val, setValue] = useState('');

    const expensive = useMemo(() => {
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {sum += i;}
        return sum;
    }, [count]);

    return (
        <>
            {count}:{expensive()}
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </>
    );
}

在下面的代码中,咱们应用 useMemo 来解决耗时计算,而后将计算结果传递给 count 并触发状态刷新。通过 useMemo 解决后,count 只会在扭转的时候才会触发耗时计算执行状态刷新,而批改 val 则不会触发刷新。

3.3 useCallback

和 useMemo 一样,useCallback 也是用来做性能优化的,即只有当依赖的数据发生变化时,才会调用回调函数从新计算结果。不同之处在于,useMemo 次要用于缓存计算结果的值,而 useCallback 缓存的是函数。useCallback 的语法格局如下。
const fnA = useCallback(fnB, [a])
在下面的语句中,useCallback 会将传递给它的函数 fnB 返回,并且会将函数 fnB 的运行后果进行缓存。并且,当依赖 a 变更时还会返回新的函数。因为返回的是函数,无奈判断返回的函数是否产生变更,所以须要借助 ES6 新增的数据类型 Set 来辅助判断,如下所示。

function Example() {const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {}, [count]);
    set.add(callback);

    return (
        <div>
            {count}:{set.size}
            <div>
                <button onClick={() => setCount(count + 1)}>+</button>
                <input value={val} onChange={e => setVal(e.target.value)}/>
            </div>
        </div>
    );
}

能够看到,每次批改 count 时 set.size 就会加 1,而 useCallback 依赖变量 count,所以每次 count 产生变更时就会返回一个新的函数。而 val 产生变更时 set.size 则不会发生变化,阐明它返回的是缓存的旧函数。
再看另外一个场景:有一个蕴含子组件的父组件,子组件会接管一个函数作为 props。通常来说,如果父组件产生更新,那么子组件也会执行更新,但在大多数场景下,子组件的更新是没有必要的。此时咱们能够应用 useCallback 返回缓存的函数,并把这个缓存的函数作为 props 传递给子组件,如下所示。

function Parent() {const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
    const callback = useCallback(() => {return count;}, [count]);

    return (
        <div>
            {count}
            <Child callback={callback}/>
            <div>
                <button onClick={() => setCount(count + 1)}>+</button>
                <input value={val} onChange={e => setVal(e.target.value)}/>
            </div>
        </div>
    );
}

function Child({callback}) {const [count, setCount] = useState(() => callback());
    useEffect(() => {setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}

事实上,useEffect、useMemo、useCallback 都是自带闭包的。即每次组件渲染时,它们都会捕捉组件函数上下文中的状态信息,所以应用这三种 Hook 时,它们反映的都是以后的状态。如果要获取组件上一次的状态,那么能够应用 ref 来进行获取。

3.4 useRef

在 React 开发中,Ref 次要作用是获取组件实例或者 DOM 元素,创立 Ref 次要有两种形式,即 createRef 和 useRef。其中,应用 createRef 形式创立的 Ref 每次渲染都会返回一个新的援用,而 useRef 每次渲染都会返回雷同的援用。
应用 createRef 形式创立 Ref 次要是类组件。例如,上面是应用 createRef() 办法来创立 Ref 的例子,如下所示。

class Example extends React.Component {constructor(props) {super(props);
        this.myRef = React.createRef();}

    componentDidMount() {this.myRef.current.focus();
    }

    render() {return <input ref={this.myRef} type="text" />;
    }
}

如果应用 React Hooks 的 useRef() 办法创立 Ref,代码如下。

function Example() {const myRef = useRef(null);

    useEffect(() => {myRef.current.focus();
    }, [])

    return <input ref={myRef} type="text" />;
}

应用 useRef 形式创立的 Ref 返回一个援用的 DOM 对象,返回的对象将在组件的整个生存期内继续存在。

四、自定义 Hook

通过自定义 Hook,咱们能够将组件逻辑提取到可重用的函数中。在后面的聊天程序中,应用 React 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.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {return 'Loading...';}
  return isOnline ? 'Online' : 'Offline';
}

当初,假如聊天利用中有一个联系人列表,当用户在线时须要把名字设置为绿色。要实现这个需要,须要新建一个 FriendListItem 组件,并增加如下代码。

function FriendListItem(props) {
  
  …. // 省略其余雷同的代码

  return (<li style={{ color: isOnline ? 'green' : 'black'}}>
      {props.friend.name}
    </li>
  );
}

能够发现,FriendStatus 和 FriendListItem 之间的状态逻辑根本是一样的。在类组件中,共享组件之间的状态逻辑有 props 和高阶组件两种形式。而在 React Hook 中,如果要共享两个函数之间的逻辑,能够自定义一个 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.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}

在 React 中,自定义的 Hook 是一个以 use 结尾的函数,函数外部能够调用其余的 Hook,并且自定义 Hook 时,入参和返回值都能够依据须要自定义,没有非凡的约定。
当初,须要共享的状态逻辑曾经被提取到 useFriendStatus 的自定义 Hook 中。而后,咱们就能够像应用一般函数一样调用自定义的 Hook 函数,如下所示。

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 时,所有 state 和副作用都是齐全隔离的。须要再次强调的是,自定义 Hook 时须要以 use 结尾来命名,这也是为了动态代码检测工具的检测。

五、Hook 规定

Hook 实质上来说就是一个 JavaScript 函数,然而在应用它时须要遵循两条规定。

  • 只在最顶层应用 Hook,不能在循环、条件或嵌套函数中调用 Hook。
  • 只能在 React 函数中调用 Hook,不能在一般 JavaScript 函数中调用 Hook。

Hooks 的设计极度依赖事件定义的程序,如果在后序的渲染环节中 Hooks 的调用程序发生变化,就可能会呈现不可预知的问题。在 React 利用开发过程中,为了保障 Hooks 调用程序的稳定性,官网开发了一个名叫 eslint-plugin-react-hooks 的 ESLint 插件来进行动态代码检测。应用前,须要先将此插件增加到 React 我的项目中,如下所示。

npm install eslint-plugin-react-hooks --save-dev

装置实现后,会在 package.json 配置文件中看到如下配置脚本。

{
  "plugins": [
     ...  // 省略其余插件包
    "react-hooks"
  ],
  "rules": {
     ...    // 省略其余规定
    "react-hooks/rules-of-hooks": "error",   // 查看 Hook 的规定
    "react-hooks/exhaustive-deps": "warn"  // 查看 effect 的依赖
  }
}

通过下面的配置后,如果代码 不合乎 Hook 标准,那么零碎就会给出相应的正告,并提醒开发者进行对应的批改。

正文完
 0