React Hooks入门: 基础

前言

  首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励,但愿你们多多关注呀!React 16.8中新增了Hooks特性,而且在React官方文档中新增长了Hooks模块介绍新特性,可见React对Hooks的重视程度,若是你还不清楚Hooks是什么,强烈建议你了解一下,毕竟这可能真的是React将来的发展方向。   javascript

起源

  React一直以来有两种建立组件的方式: Function Components(函数组件)与Class Components(类组件)。函数组件只是一个普通的JavaScript函数,接受props对象并返回React Element。在我看来,函数组件更符合React的思想,数据驱动视图,不含有任何的反作用和状态。在应用程序中,通常只有很是基础的组件才会使用函数组件,而且你会发现随着业务的增加和变化,组件内部可能必需要包含状态和其余反作用,所以你不得不将以前的函数组件改写为类组件。但事情每每并无这么简单,类组件也没有咱们想象的那么美好,除了徒增工做量以外,还存在其余种种的问题。java

  首先类组件共用状态逻辑很是麻烦。好比咱们借用官方文档中的一个场景,FriendStatus组件用来显示朋友列表中该用户是否在线。react

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';
  }
}
复制代码

  上面FriendStatus组件会在建立时主动订阅用户状态,并在卸载时会退订状态防止形成内存泄露。假设又出现了一个组件也须要去订阅用户在线状态,若是想用复用该逻辑,咱们通常会使用render props和高阶组件来实现状态逻辑的复用。git

// 采用render props的方式复用状态逻辑
class OnlineStatus 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() {
    const {isOnline } = this.state;
    return this.props.children({isOnline})
  }
}

class FriendStatus extends React.Component{
  render(){
    return (
      <OnlineStatus friend={this.props.friend}> { ({isOnline}) => { if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } } </OnlineStatus>
    );
  }
}
复制代码
// 采用高阶组件的方式复用状态逻辑
function withSubscription(WrappedComponent) {
  return class 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() {
      return <WrappedComponent isOnline={this.state.isOnline}/> } } } const FriendStatus = withSubscription(({isOnline}) => { if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }) 复制代码

  上面两种复用状态逻辑的方式不只须要费时费力地重构组件,并且Devtools查看组件的层次结构时,会发现组件层级结构变深,当复用的状态逻辑过多时,也会陷入组件嵌套地狱(wrapper hell)的状况。可见上述两种方式并不能完美解决状态逻辑复用的问题。github

  不只如此,随着类组件中业务逻辑逐渐复杂,维护难度也会逐步提高,由于状态逻辑会被分割到不一样的生命周期函数中,例如订阅状态逻辑位于componentDidMount,取消订阅逻辑位于componentWillUnmount中,相关逻辑的代码相互割裂,而逻辑不相关的代码反而有可能集中在一块儿,总体都是不利于维护的。而且相好比函数式组件,类组件学习更为复杂,你须要时刻提防this在组件中的陷阱,永远不能忘了为事件处理程序绑定this。如此种种,看来函数组件仍是有特有的优点的。数组

Hooks

  函数式组件一直以来都缺少类组件诸如状态、生命周期等种种特性,而Hooks的出现就是让函数式组件拥有类组件的特性。官方定义:缓存

Hooks are functions that let you “hook into” React state and lifecycle features from function components.app

  要让函数组件拥有类组件的特性,首先就要实现状态state的逻辑。ide

State: useState useReducer

  useState就是React提供最基础、最经常使用的Hook,主要用来定义本地状态,咱们以一个最简单的计数器为例:函数

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div> <span>{count}</span> <button onClick={()=> setCount(count + 1)}>+</button> <button onClick={() => setCount((count) => count - 1)}>-</button> </div>
  );
}
复制代码

  useState能够用来定义一个状态,与state不一样的是,状态不只仅能够是对象,并且能够是基础类型值,例如上面的Number类型的变量。useState返回的是一个数组,第一个是当前状态的实际值,第二个用于更改该状态的函数,相似于setState。更新函数与setState相同的是均可以接受值和函数两种类型的参数,与useState不一样的是,更新函数会将状态替换(replace)而不是合并(merge)。

  函数组件中若是存在多个状态,既能够经过一个useState声明对象类型的状态,也能够经过useState屡次声明状态。

// 声明对象类型的状态
const [count, setCount] = useState({
    count1: 0,
    count2: 0
});

// 屡次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
复制代码

  相比于声明对象类型的状态,明显屡次声明状态的方式更加方便,主要是由于更新函数是采用的替换的方式,所以你必须给参数中添加未变化的属性,很是的麻烦。须要注意的是,React是经过Hook调用的次序来记录各个内部状态的,所以Hook不能在条件语句(如if)或者循环语句中调用,并在须要注意的是,咱们仅能够在函数组件中调用Hook,不能在组件和普通函数中(除自定义Hook)调用Hook。

  当咱们要在函数组件中处理复杂多层数据逻辑时,使用useState就开始力不从心,值得庆幸的是,React为咱们提供了useReducer来处理函数组件中复杂状态逻辑。若是你使用过Redux,那么useReducer可谓是很是的亲切,让咱们用useReducer重写以前的计数器例子:

import React, { useReducer } from 'react'

const reducer = function (state, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

function Example() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  const {count} = state;
  return (
    <div> <span>{count}</span> <button onClick={()=> dispatch({ type: "increment"})}>+</button> <button onClick={() => dispatch({ type: "decrement"})}>-</button> </div>
  );
}
复制代码

  useReducer接受两个参数: reducer函数和默认值,并返回当前状态state和dispatch函数的数组,其逻辑与Redux基本一致。useReducer和Redux的区别在于默认值,Redux的默认值是经过给reducer函数赋值默认参数的方式给定,例如:

// Redux的默认值逻辑
const reducer = function (state = { count: 0 }, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}
复制代码

  useReducer之因此没有采用Redux的逻辑是由于React认为state的默认值多是来自于函数组件的props,例如:

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, { count: initialState });
  // 省略...
}
复制代码

  这样就能实现经过传递props来决定state的默认值,固然React虽然不推荐Redux的默认值方式,但也容许你相似Redux的方式去赋值默认值。这就要接触useReducer的第三个参数: initialization。

  顾名思义,第三个参数initialization是用来初始化状态,当useReducer初始化状态时,会将第二个参数initialState传递initialization函数,initialState函数返回的值就是state的初始状态,这也就容许在reducer外抽象出一个函数专门负责计算state的初始状态。例如:

const initialization = (initialState) => ({ count: initialState })

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, initialState, initialization);
  // 省略...
}
复制代码

  因此借助于initialization函数,咱们就能够模拟Redux的初始值方式:

import React, { useReducer } from 'react'

const reducer = function (state = {count: 0}, action) {
  // 省略...
}

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, undefined, reducer());
  // 省略...
}
复制代码

Side Effects: useEffect useLayoutEffect

  解决了函数组件中内部状态的定义,接下来亟待解决的函数组件中生命周期函数的问题。在函数式思想的React中,生命周期函数是沟通函数式和命令式的桥梁,你能够在生命周期中执行相关的反作用(Side Effects),例如: 请求数据、操做DOM等。React提供了useEffect来处理反作用。例如:

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`
    return () => {
      console.log('clean up!')
    }
  });

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

  在上面的例子中咱们给useEffect传入了一个函数,并在函数内根据count值更新网页标题。咱们会发现每次组件更新时,useEffect中的回调函数都会被调用。所以咱们能够认为useEffect是componentDidMount和componentDidUpdate结合体。当组件安装(Mounted)和更新(Updated)时,回调函数都会被调用。观察上面的例中,回调函数返回了一个函数,这个函数就是专门用来清除反作用,咱们知道相似监听事件的反作用在组件卸载时应该及时被清除,不然会形成内存泄露。清除函数会在每次组件从新渲染前调用,所以执行顺序是:

render -> effect callback -> re-render -> clean callback -> effect callback

  所以咱们可使用useEffect模拟componentDidMount、componentDidUpdate、componentWillUnmount行为。以前咱们提到过,正是由于生命周期函数,咱们无可奈何将相关的代码拆分到不一样的生命周期函数,反而将不相关的代码放置在同一个生命周期函数,之因此会出现这个状况,主要问题在于咱们并非依据于业务逻辑书写代码,而是经过执行时间编码。为了解决这个问题,咱们能够经过建立多个Hook,将相关逻辑代码放置在同一个Hook来解决上述问题:

import React, { useState, useEffect } from 'react';

function Example() {
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  
  useEffect(() => {
    otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // 省略...
}
复制代码

  咱们经过多个Hook来集中逻辑关注点,避免不相关的代码糅杂而出现的逻辑混乱。可是随之而来就遇到一个问题,假设咱们的某个行为肯定是要在区分componentDidUpdate或者componentDidMount时才执行,useEffect是否能区分。好在useEffect为咱们提供了第二个参数,若是第二个参数传入一个数组,仅当从新渲染时数组中的值发生改变时,useEffect中的回调函数才会执行。所以若是咱们向其传入一个空数组,则能够模拟生命周期componentDidMount。可是若是你想仅模拟componentDidUpdate,目前暂时未发现什么好的方法。

  useEffect与类组件生命周期不一样的是,componentDidUpdatecomponentDidMount都是在DOM更新后同步执行的,但useEffect并不会在DOM更新后同步执行,也不会阻塞更新界面。若是须要模拟生命周期同步效果,则须要使用useLayoutEffect,其使用方法和useEffect相同,区域只在于执行时间上。

Context:useContext

  借助Hook:useContext,咱们也能够在函数组件中使用context。相比于在类组件中须要经过render props的方式使用,useContext的使用则至关方便。

import { createContext } from 'react'

const ThemeContext = createContext({ color: 'color', background: 'black'});

function Example() {
    const theme = useContext(Conext);
    return (
        <p style={{color: theme.color}}>Hello World!</p>
    );
}

class App extends Component {
  state = {
    color: "red",
    background: "black"
  };

  render() {
    return (
        <Context.Provider value={{ color: this.state.color, background: this.state.background}}> <Example/> <button onClick={() => this.setState({color: 'blue'})}>color</button> <button onClick={() => this.setState({background: 'blue'})}>backgroud</button> </Context.Provider> ); } } 复制代码

  useContext接受函数React.createContext返回的context对象做为参数,返回当前context中值。每当Provider中的值发生改变时,函数组件就会从新渲染,须要注意的是,即便的context的未使用的值发生改变时,函数组件也会从新渲染,正如上面的例子,Example组件中即便没有使用过background,但background发生改变时,Example也会从新渲染。所以必要时,若是Example组件还含有子组件,你可能须要添加shouldComponentUpdate防止没必要要的渲染浪费性能。

Ref: useRef useImperativeHandle

  useRef经常使用在访问子元素的实例:

function Example() {
    const inputEl = useRef();
    const onButtonClick = () => {
        inputEl.current.focus();
    };
    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>Focus the input</button>
        </>
    );
}
复制代码

  上面咱们说了useRef经常使用在ref属性上,实际上useRef的做用不止于此

const refContainer = useRef(initialValue)

  useRef能够接受一个默认值,并返回一个含有current属性的可变对象,该可变对象会将持续整个组件的生命周期。所以能够将其当作类组件的属性同样使用。

  useImperativeHandle用于自定义暴露给父组件的ref属性。须要配合forwardRef一块儿使用。

function Example(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));
    return <input ref={inputRef} />; } export default forwardRef(Example); 复制代码
class App extends Component {
  constructor(props){
      super(props);
      this.inputRef = createRef()
  }
  
  render() {
    return (
        <>
            <Example ref={this.inputRef}/>
            <button onClick={() => {this.inputRef.current.focus()}}>Click</button>
        </>
    );
  }
}
复制代码

New Feature: useCallback useMemo

  熟悉React的同窗见过相似的场景:   

class Example extends React.PureComponent{
    render(){
        // ......
    }
}

class App extends Component{
    render(){
        return <Example onChange={() => this.setState()}/> } } 复制代码

  其实在这种场景下,虽然Example继承了PureComponent,但实际上并不可以优化性能,缘由在于每次App组件传入的onChange属性都是一个新的函数实例,所以每次Example都会从新渲染。通常咱们为了解决这个状况,通常会采用下面的方法:

class App extends Component{
    constructor(props){
        super(props);
        this.onChange = this.onChange.bind(this);
    }

    render(){
        return <Example onChange={this.onChange}/> } onChange(){ // ... } } 复制代码

  经过上面的方法一并解决了两个问题,首先保证了每次渲染时传给Example组件的onChange属性都是同一个函数实例,而且解决了回调函数this的绑定。那么如何解决函数组件中存在的该问题呢?React提供useCallback函数,对事件句柄进行缓存。

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

  useCallback接受函数和一个数组输入,并返回的一个缓存版本的回调函数,仅当从新渲染时数组中的值发生改变时,才会返回新的函数实例,这也就解决咱们上面提到的优化子组件性能的问题,而且也不会有上面繁琐的步骤。

useCallback相似的是,useMemo返回的是一个缓存的值。

const memoizedValue = useMemo(
  () => complexComputed(),
  [a, b],
);
复制代码

  也就是仅当从新渲染时数组中的值发生改变时,回调函数才会从新计算缓存数据,这可使得咱们避免在每次从新渲染时都进行复杂的数据计算。所以咱们能够认为:

useCallback(fn, input) 等同于 useMemo(() => fn, input)

  若是没有给useMemo传入第二个参数,则useMemo仅会在收到新的函数实例时,才从新计算,须要注意的是,React官方文档提示咱们,useMemo仅能够做为一种优化性能的手段,不能当作语义上的保证,这就是说,也会React在某些状况下,即便数组中的数据未发生改变,也会从新执行。

自定义Hook

  咱们前面讲过,Hook只能在函数组件的顶部调用,不能再循环、条件、普通函数中使用。咱们前面讲过,类组件想要共享状态逻辑很是麻烦,必需要借助于render props和HOC,很是的繁琐。相比于次,React容许咱们建立自定义Hook来封装共享状态逻辑。所谓的自定义Hook是指以函数名以use开头并调用其余Hook的函数。咱们用自定义Hook来重写刚开始的订阅用户状态的例子:

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

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

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

    return isOnline;
}

function FriendStatus() {
    const isOnline = useFriendStatus();
    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}
复制代码

  咱们用自定义Hook重写了以前的订阅用户在线状态的例子,相比于render prop和HOC复杂的逻辑,自定义Hook更加的简洁,不只于此,自定义Hook也不会引发以前咱们说提到过的组件嵌套地狱(wrapper hell)的状况。优雅的解决了以前类组件复用状态逻辑困难的状况。

总结

  借助于Hooks,函数组件已经能基本实现绝大部分的类组件的功能,不只于此,Hooks在共享状态逻辑、提升组件可维护性上有具备必定的优点。能够预见的是,Hooks颇有多是React可预见将来大的方向。React官方对Hook采用的是逐步采用策略(Gradual Adoption Strategy),并表示目前没有计划会将class从React中剔除,可见Hooks会很长时间内和咱们的现有代码并行工做,React并不建议咱们所有用Hooks重写以前的类组件,而是建议咱们在新的组件或者非关键性组件中使用Hooks。     若有表述不周之处,虚心接受批评指教。愿你们一同进步!

相关文章
相关标签/搜索