新闻资讯

新闻资讯 媒体报道

React Hooks不完全解读

编辑:010     时间:2020-02-22

什么是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,对应到<u>Class Component</u>应该这么实现

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解决了什么问题?

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

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

  • 状态逻辑复用困难

    • 组件的状态相关的逻辑通常会耦合在组件的实现中,如果另一个组件需要相同的状态逻辑,只能借助<u>render props</u> 和 <u>high-order components</u>,然而这会破坏原有的组件结构,带来 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. 为了在组件刷新的时候更新文档的标题,我们在componentDidMount和componentDidUpdate中各写了一遍更新逻辑; 2. 绑定朋友状态更新和解绑的逻辑,分散在componentDidMount和componentWillUnmount中,实际上这是一对有关联的逻辑,如果能写在一起最好;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 一般配合<u>Function Components</u>使用,也可以在内置 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的作用是让你在<u>Function Components</u>里面可以执行一些 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

这个效果等同于在componentDidMount、componentDidUpdate和componentWillUnmount实现了事件绑定和解绑。如果只是组件的 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 还包括useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect和useDebugValue,这里就不再赘述了,可以直接参考官方文档

自定义 hook

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

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

实现原理

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

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

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

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

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 顺序一致。
  • 必须在 <u>Function Components</u> 或者自定义 hook 中调用:不管是内置 hook,还是自定义 hook,最终都需要在 <u>Function Components</u> 中调用,因为内部的memoHooks和cursor其实都跟当前渲染的组件实例绑定,脱离了<u>Function Components</u>,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,比如getSnapshotBeforeUpdate和componentDidCatch这2个生命周期,hooks还没有对等的实现办法。建议大家可以在新开发的组件中尝试使用 hooks。如果经过长时间的迭代后 function components + hooks 成为主流,且 hooks 从功能上可以完全替代 class,那么 react team 应该就可以考虑把 class component 移除,毕竟没有必要维护2套实现,这样不仅增加了维护成本,对开发者来说也多一份学习负担。

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

回复列表

相关推荐