Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks不完全解读 #1

Open
Joe3Ray opened this issue Feb 16, 2020 · 0 comments
Open

React Hooks不完全解读 #1

Joe3Ray opened this issue Feb 16, 2020 · 0 comments
Assignees

Comments

@Joe3Ray
Copy link
Owner

Joe3Ray commented Feb 16, 2020

什么是hooks?

hooks 是 react 在16.8版本开始引入的一个新功能,它扩展了函数组件的功能,使得函数组件也能实现状态、生命周期等复杂逻辑。

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

上面是 react 官方提供的 hooks 示例,使用了内置hookuseState,对应到Class Component应该这么实现

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

简而言之,hooks 就是钩子,让你能更方便地使用react相关功能。

hooks解决了什么问题?

看完上面一段,你可能会觉得除了代码块精简了点,没看出什么好处。别急,继续往下看。

过去,我们习惯于使用Class Component,但是它存在几个问题:

  • 状态逻辑复用困难

    • 组件的状态相关的逻辑通常会耦合在组件的实现中,如果另一个组件需要相同的状态逻辑,只能借助render props 和 high-order components,然而这会破坏原有的组件结构,带来 JSX wrapper hell 问题。
  • side effect 复用和组织困难

    • 我们经常会在组件中做一些有 side effect 的操作,比如请求、定时器、打点、监听等,代码组织方式如下
    class FriendStatusWithCounter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0, isOnline: null };
        this.handleStatusChange = this.handleStatusChange.bind(this);
      }
    
      componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      handleStatusChange(status) {
        this.setState({
          isOnline: status.isOnline
        });
      }
      
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <p>Friend {this.props.friend.id} status: {this.state.isOnline}</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }

    复用的问题就不说了,跟状态逻辑一样,主要说下代码组织的问题。1. 为了在组件刷新的时候更新文档的标题,我们在componentDidMountcomponentDidUpdate中各写了一遍更新逻辑; 2. 绑定朋友状态更新和解绑的逻辑,分散在componentDidMountcomponentWillUnmount中,实际上这是一对有关联的逻辑,如果能写在一起最好;3. componentDidMount中包含了更新文档标题和绑定事件监听,这2个操作本身没有关联,如果能分开到不同的代码块中更利于维护。

  • Javascript Class 天生缺陷

    • 开发者需要理解this的指向问题,需要记得手动 bind 事件处理函数,这样代码看起来很繁琐,除非引入@babel/plugin-proposal-class-properties(这个提案目前还不稳定)。
    • 现代工具无法很好地压缩 class 代码,导致代码体积偏大,hot reloading效果也不太稳定。

为了解决上述问题,hooks 应运而生。让我们使用 hooks 改造下上面的例子

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

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  return isOnline;
}

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  const isOnline = useFriendStatus(props.friend.id);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Friend {props.friend.id} status: {isOnline}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

function FriendStatus(props) {
  // 通过自定义hook复用逻辑
  const isOnline = useFriendStatus(props.friend.id);

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

看,问题都解决了!

怎么使用?

hooks 一般配合Function Components使用,也可以在内置 hooks 的基础上封装自定义 hook。

先介绍下 react 提供的内置 hooks。

useState

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

useState接收一个参数作为初始值,返回一个数组,数组的第一个元素是表示当前状态值的变量,第二个参数是修改状态的函数,执行的操作类似于this.setState({ count: someValue }),当然内部的实现并非如此,这里仅为了帮助理解。

useState可以多次调用,每次当你需要声明一个state时,就调用一次。

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

需要更新某个具体状态时,调用对应的 setXXX 函数即可。

useEffect

useEffect的作用是让你在Function Components里面可以执行一些 side effects,比如设置监听、操作dom、定时器、请求等。

  • 普通side effect
useEffect(() => {
  document.title = `You clicked ${count} times`;
});
  • 需要清理的effect,回调函数的返回值作为清理函数
useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  // Specify how to clean up after this effect:
  return function cleanup() {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

需要注意,上面这种写法,每次组件更新都会执行 effect 的回调函数和清理函数,顺序如下:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

这个效果等同于在componentDidMountcomponentDidUpdatecomponentWillUnmount实现了事件绑定和解绑。如果只是组件的 state 变化导致重新渲染,同样会重新调用 cleanup 和 effect,这时候就显得没有必要了,所以 useEffect 支持用第2个参数来声明依赖

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

第2个参数是一个数组,在数组中传入依赖的 state 或者 props,如果依赖没有更新,就不会重新执行 cleanup 和 effect。

如果你需要的是只在初次渲染的时候执行一次 effect,组件卸载的时候执行一次 cleanup,那么可以传一个空数组[]作为依赖。

useContext

context这个概念大家应该不陌生,一般用于比较简单的共享数据的场景。useContext就是用于实现context功能的 hook。

来看下官方提供的示例

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

代码挺长,但是一眼就能看懂了。把 context 对象传入useContext,就可以拿到最新的 context value。

需要注意的是,只要使用了useContext的组件,在 context value 改变后,一定会触发组件的更新,哪怕他使用了React.memo或是shouldComponentUpdate

useReducer

useReducer(reducer, initialArg)返回[state, dispatch],跟 redux 很像。

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

除此之外,react 内置的 hooks 还包括useCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue,这里就不再赘述了,可以直接参考官方文档

自定义 hook

基于内置 hook,我们可以封装自定义的 hook,上面的示例中已经出现过useFriendStatus这样的自定义 hook,它能帮我们抽离公共的组件逻辑,方便复用。注意,自定义 hook 也需要以use开头。

我们可以根据需要创建各种场景的自定义 hook,如表单处理、计时器等。后面实战场景的章节中我会具体介绍几个例子。

实现原理

hooks 的使用需要遵循几个规则:

  • 必须在顶层调用,不能包裹在条件判断、循环等逻辑中
  • 必须在 Function Components 或者自定义 hook 中调用

之所以有这些规则限制,是跟 hooks 的实现原理有关。

这里我们尝试实现一个简单的版本的useStateuseEffect用来说明。

const memoHooks = [];
let cursor = 0;

function useState(initialValue) {
  const current = cursor;
  const state = memoHooks[current] || initialValue;
  function setState(val) {
    memoHooks[current] = val;
    // 执行re-render操作
  }
  cursor++;
  return [state, setState];
}

function useEffect(cb, deps) {
  const hasDep = !!deps;
  const currentDeps = memoHooks[cursor];
  const hasChanged = currentDeps ? !deps.every((val, i) => val === currentDeps[i]) : true;
  if (!hasDep || hasChanged) {
    cb();
    memoHooks[cursor] = deps;
  }
  cursor++;
}

此时我们需要构造一个函数组件来使用这2个 hooks

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);
  const [name, setName] = useState('Joe');
  useEffect(() => {
    console.log(`Your name is ${name}`);
  });
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  1. 渲染前:memoHooks 为[],cursor 为0
  2. 第一次渲染
    1. 执行const [count, setCount] = useState(0);,memoHooks 为[0],cursor 为0
    2. 执行useEffect(() => { document.title = You clicked ${count} times; }, [count]);,memoHooks 为[0, [0]],cursor 为1
    3. 执行const [name, setName] = useState('Joe');,memoHooks 为[0, [0], 'Joe'],cursor 为2
    4. 执行useEffect(() => { console.log(Your name is ${name}); });,memoHooks 为[0, [0], 'Joe', undefined],cursor 为3
  3. 点击按钮
    1. 执行setCount(count + 1),memoHooks 为[1, [0], 'Joe', undefined],cursor 为0
    2. 执行 re-render
  4. re-render
    1. 执行const [count, setCount] = useState(0);,memoHooks 为[1, [0], 'Joe', undefined],cursor 为0
    2. 执行useEffect(() => { document.title = You clicked ${count} times; }, [count]);,memoHooks 为[1, [1], 'Joe', undefined],cursor 为1。这里由于hooks[1]的值变化,会导致 cb 再次执行。
    3. 执行const [name, setName] = useState('Joe');,memoHooks 为[1, [1], 'Joe', undefined],cursor 为2
    4. 执行useEffect(() => { console.log(Your name is ${name}); });,memoHooks 为[1, [1], 'Joe', undefined],cursor 为3。这里由于依赖为 undefined,导致 cb 再次执行。

通过上述示例,应该可以解答为什么 hooks 要有这样的使用规则了。

  • 必须在顶层调用,不能包裹在条件判断、循环等逻辑中:hooks 的执行对于顺序有强依赖,必须要保证每次渲染组件调用的 hooks 顺序一致。
  • 必须在 Function Components 或者自定义 hook 中调用:不管是内置 hook,还是自定义 hook,最终都需要在 Function Components 中调用,因为内部的memoHookscursor其实都跟当前渲染的组件实例绑定,脱离了Function Components,hooks 也无法正确执行。

当然,这些只是为了方便理解做的一个简单demo,react 内部实际上是通过一个单向链表来实现,并非 array,有兴趣可以自行翻阅源码。

实战场景

操作表单

实现一个hook,支持自动获取输入框的内容。

function useInput(initial) {
  const [value, setValue] = useState(initial);
  const onChange = useCallback(function(event) {
    setValue(event.currentTarget.value);
  }, []);
  return {
    value,
    onChange
  };
}

// 使用示例
function Example() {
  const inputProps = useInput('Joe');
  return <input {...inputProps} />
}

网络请求

实现一个网络请求hook,能够支持初次渲染后自动发请求,也可以手动请求。参数传入一个请求函数即可。

function useRequest(reqFn) {
  const initialStatus = {
    loading: true,
    result: null,
    err: null
  };
  const [status, setStatus] = useState(initialStatus);
  function run() {
    reqFn().then(result => {
      setStatus({
        loading: false,
        result,
        err: null
      })
    }).catch(err => {
      setStatus({
        loading: false,
        result: null,
        err
      });
    });
  }
  // didMount后执行一次
  useEffect(run, []);
  return {
    ...status,
    run
  };
}

// 使用示例
function req() {
  // 发送请求,返回promise
  return fetch('http://example.com/movies.json');
}
function Example() {
  const {
    loading,
    result,
    err,
    run
  } = useRequest(req);
  return (
    <div>
      <p>
        The result is {loading ? 'loading' : JSON.stringify(result || err)}
      </p>
      <button onClick={run}>Reload</button>
    </div>
  );
}

上面2个例子只是实战场景中很小的一部分,却足以看出 hooks 的强大,当我们有丰富的封装好的 hooks 时,业务逻辑代码会变得很简洁。推荐一个github repo,这里罗列了很多社区产出的 hooks lib,有需要自取。

使用建议

根据官方的说法,在可见的未来 react team 并不会停止对 class component 的支持,因为现在绝大多数 react 组件都是以 class 形式存在的,要全部改造并不现实,而且 hooks 目前还不能完全取代 class,比如getSnapshotBeforeUpdatecomponentDidCatch这2个生命周期,hooks还没有对等的实现办法。建议大家可以在新开发的组件中尝试使用 hooks。如果经过长时间的迭代后 function components + hooks 成为主流,且 hooks 从功能上可以完全替代 class,那么 react team 应该就可以考虑把 class component 移除,毕竟没有必要维护2套实现,这样不仅增加了维护成本,对开发者来说也多一份学习负担。

参考文章

@Joe3Ray Joe3Ray self-assigned this Feb 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant