4个问题带你进阶React Hooks

200 阅读7分钟
原文链接: segmentfault.com

前言

相信大部分人都已经在使用 React hooks 了,但是在开发过程中,我们要 知其然知其所以然。整理了一下最近使用 React hooks 遇到的一些问题,如果有问题或更好的答案,欢迎一起交流。

目录

  • 为什么要在 React 中引入Hook ? Hooks 解决了什么问题
  • mixin、HOC 、render props、hooks
  • React Hooks 原理
  • Hooks 中闭包的坑

一、为什么要在React中引入Hook? Hooks解决了什么问题

1. 组件复用逻辑难

没有hooks之前使用 render props高阶组件render props是接受一个组件作为propsHOC是一个函数,接受一个组件作为参数,返回另一个组件。使用这些开发的组件会形成“嵌套地狱”,调试困难。

2. 复杂组件状态逻辑多

很多组件在最开始写的时候都是很简单的,基本上就是只做一件事,当你的业务逻辑变得复杂之后,组件也会变得复杂起来。大多数情况下,我们不大可能把组件拆分的更小,因为可能有很多共用的状态逻辑,拆分后,组件之间的通信也会很复杂,甚至需要引用 Redux 来管理组件之间的状态。

3. class学习成本高

要想用好 class 组件,你必须了解 ES6 中的class,理解 JavaSript 中 this的工作方式,要注意绑定事件处理器,清楚当前this的指向。

详细查看react 官方文档 Hook 简介

二、mixin、HOC 、render props、hooks

我们在平常开发中经常会遇到很多的页面有一些公共的逻辑,我们不能每次遇到的时候都直接把原来的代码 copy 过来改扒改扒,改的时候又要全局搜索改掉(很难保证没有漏的,费时费力)所以要想办法去复用,mixinHOC , render props等都是实现逻辑复用的方式。

mixin

vue和react中都曾用过mixin(react目前已经抛弃)mixin(混入)本质上就是将对象复制到另一个对象上。

const mixin = function (obj, mixins) {
    const newObj = obj;
    newObj.prototype = Object.create(obj.prototype);
    
    for(let prop in mixins) {
        if(mixins.hasOwnProperty(prop)) {
            newObj.prototype[prop] = mixins[prop];
        }
    }
    return newObj;
}

const obj = {
    sayHello() {
        console.log('hello');
    }
};
const otherObj = function() {
    console.log('otherObj');
}
const Obj = mixin(otherObj, obj);

const a = new Obj(); // otherObj
a.sayHello(); //  hello

mixin存在的几个问题:

  • 相关依赖:mixin有可能去依赖其他的mixin,当我们修改其中一个的时候,可能会影响到其他的mixin
  • 命名冲突:不同的人在写的时候很有可能会有命名冲突,比如像 handleChange等类似常见的名字
  • 增加复杂性:当我们一个组件引入过多的mixin时,代码逻辑将会非常复杂,因为在不停的引入状态,和我们最初想的每个组件只做单一的功能背道而驰。

HOC

HOC是React社区提出的新的方式用来取代mixin的。
高阶函数是函数式编程中一个基本的概念,它描述了一种这样的函数:接受函数作为输入,或是返回一个函数,比如 map, reduce等都是高阶函数。
高阶组件( higher-order component),类似于高阶组件接受一个组件作为参数,返回另一个组件。

function getComponent(WrappedComponent) {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>;
    }
  };
}

HOC的优点为:

  • 不会影响组件内部的状态

HOC的问题是:

  • 需要在原组件上进行包裹和嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突

Render Props

render props: 通过props接受一个返回react element 的函数,来动态决定自己要渲染的结果

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

React Router中就用到了 Render Props

<Router>
  <Route path="/home" render={() => <div>Home</div>} />
</Router>,

它有哪些问题呢

  • 很容易造成“嵌套地狱”

使用 hooks

具体实现就是通过一个函数来封装跟状态有关的逻辑,将这些逻辑从组件中抽取出来。而这个函数中我们可以使用其他的Hooks,也可以单独进行测试,甚至将它贡献给社区。

import { useState, useEffect } from 'react';

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

hooks的引入就是为了解决上面提到的这么问题,因为 使用函数式组件,我们在开发组件的时候,可以当做平常写函数一样自由。

函数复用是比较容易的,直接传不同的参数就可以渲染不同的组件,复杂组件实现,我们完全可以多封装几个函数,每个函数只单纯的负责一件事。而且很多公用的代码逻辑和一些场景我们可以抽出来,封装成自定义hooks使用,比如 Umi Hooks库封装了很多共用的逻辑,比如 useSearch,封装了异步搜索场景的逻辑;比如 useVirtualList,就封装了虚拟列表的逻辑。

三、React Hooks原理

在使用hooks的时候,你可能会对它的规则有很多疑问,比如:

  1. 只在最顶层使用 Hook,不能在循环、条件判断或嵌套函数中调用hook。
  2. 只在 React 函数中调用 Hook,不能再普通函数中调用hook
  3. React怎么知道哪个state对应哪个useState

...

我们先来看一下官方文档给出的解释

每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

React中是通过类似单链表的形式来实现的,通过 next 按顺序串联所有的 hook。可以看下 源码部分

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null, 
|};

export type Effect = {|
  tag: HookEffectTag,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
|};
更详细的推荐查看 React Hooks 原理Under the hood of React’s hooks system

四、Hooks中闭包的坑

我们先来看一下使用 setState 的更新机制:

ReactsetState函数实现中,会根据一个变量isBatchingUpdates 判断是直接更新this.state还是放到 队列中回头再说。而isBatchingUpdates 默认是false,也就表示setState会同步更新this.state。但是,有一个函数 batchedUpdates, 这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理程序过程setState不会同步更新this.state

知道这些,我们下面来看两个例子。

下面的代码输出什么?

class Example extends React.Component {
   constructor() {
     super();
     this.state = {
       val: 0
     };
   }
   
   componentDidMount() {
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 1 次 log
 
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 2 次 log
 
     setTimeout(() => {
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 3 次 log 1 
 
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 4 次 log 2
     }, 0);
   }
 
   render() {
     return null;
   }
 };

打印结果是: 0, 0, 2, 3。

  1. 第一次和第二次都是在react自身生命周期内,触发 isBatchingUpdates 为true, 所以并不会直接执行更新state, 而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0
  2. 两次setState时,获取到 this.state.val 都是 0,所以执行时都是将0设置为1,在react内部会被合并掉,只执行一次。设置完成后 state.val值为1。
  3. setTimeout中的代码,触发时 isBatchingUpdates为false,所以能够直接进行更新,所以连着输出 2, 3

上面代码改用react hooks的话

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

const MyComponent = () => {
    const [val, setVal] = useState(0);

    useEffect(() => {
        setVal(val+1);
        console.log(val);

        setVal(val+1);
        console.log(val);

        setTimeout(() => {
            setVal(val+1);
            console.log(val);

            setVal(val+1);
            console.log(val);
        }, 0)
    }, []);
    return null
};

export default MyComponent;

打印输出: 0, 0, 0, 0。

更新的方式没有改变。首先是因为 useEffect 函数只运行一次,其次setTimeout是个闭包,内部获取到值val一直都是 初始化声明的那个值,所以访问到的值一直是0。以例子来看的话,并没有执行更新的操作。

在这种情况下,需要使用一个容器,你可以将更新后的状态值写入其中,并在以后的 setTimeout中访问它,这是useRef的一种用例。可以将状态值与refcurrent属性同步,并在setTimeout中读取当前值。

关于这部分详细内容可以查看 React useEffect的陷阱

参考