React Hooks入门: 基础

栏目: 服务器 · 发布时间: 5年前

内容简介:首先欢迎大家关注我的React一直以来有两种创建组件的方式: Function Components(函数组件)与Class Components(类组件)。函数组件只是一个普通的JavaScript函数,接受首先类组件共用状态逻辑非常麻烦。比如我们借用官方文档中的一个场景,FriendStatus组件用来显示朋友列表中该用户是否在线。

前言

首先欢迎大家关注我的 Github博客 ,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!React 16.8中新增了Hooks特性,并且在React官方文档中新增加了Hooks模块介绍新特性,可见React对Hooks的重视程度,如果你还不清楚Hooks是什么,强烈建议你了解一下,毕竟这可能真的是React未来的发展方向。

起源

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

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

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 和高阶组件来实现状态逻辑的复用。

// 采用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)的情况。可见上述两种方式并不能完美解决状态逻辑复用的问题。

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

Hooks

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

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

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

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。

如有表述不周之处,虚心接受批评指教。愿大家一同进步!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Growth Hacker Marketing

Growth Hacker Marketing

Ryan Holiday / Portfolio / 2013-9-3 / USD 10.31

Dropbox, Facebook, AirBnb, Twitter. A new generation of multibillion dollar brands built without spending a dime on “traditional marketing.” No press releases, no PR firms, and no billboards in Times ......一起来看看 《Growth Hacker Marketing》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具