上周, Sophie Alpert 和 Dan Abramov 在 React Conf 2018 中 提出了 hooks 这个概念, 让我们一起来看看 Hooks 在解决一个什么问题.
TL;DR
一句话总结 React Hooks 就是在 react 函数组件中, 也可以使用类组件 (classes components) 的 state 和 组件生命周期, 而不需要在 mixin, 函数组件, HOC 组件和 render props 之间来回切换, 使得函数组件的功能更加实在, 更加方便我们在业务中实现业务逻辑代码的分离和组件的复用.
本文将从以下几个方面介绍 hooks
Hooks 在解决什么问题
Hooks 的 API 介绍 和如何使用 hooks
Hooks 是怎么实现的
Hooks 在解决什么问题
React 一直在解决一个问题, 如何实现分离业务逻辑代码, 实现组件内部相关业务逻辑的复用.
一般情况下, 我们都是通过组件和自上而下传递的数据流将我们页面上的大型 UI 组织成为独立的小型 UI, 实现组件的重用. 但是我们经常遇到很难侵入一个复杂的组件中实现重用, 因为组件的逻辑是有状态的, 无法提取到函数组件当中. 这在处理动画和表单的时候, 尤其常见, 当我们在组件中连接外部的数据源, 然后希望在组件中执行更多其他的操作的时候, 我们就会把组件搞得特别糟糕:
难以重用和共享组件中的与状态相关的逻辑, 造成产生很多巨大的组件
逻辑复杂的组件难以开发与维护, 当我们的组件需要处理多个互不相关的 localstate 时, 每个生命周期函数中可能会包含着各种互不相关的逻辑在里面.
复杂的模式, 如渲染道具和高阶组件.
由于业务变动, 函数组件不得不改为类组件.
这时候, Hooks 就派上用场了. Hooks 允许我们将组件内部的逻辑, 组织成为一个可复用的隔离模块.
借用 @Sunil Pai 的两张图来说明这个问题:
- image.PNG
- image.PNG
从 React Hooks 中体验出来的是 React 的哲学在组件内部的实现, 以前我们只在组件和组件直接体现 React 的哲学, 就是清晰明确的数据流和组成形式. 既可以复用组件内的逻辑, 也不会出现 HOC 带来的层层嵌套, 更加不会出现 Mixin 的弊端.
Hooks 的 API 介绍 和如何使用 hooks
@dan_abramov 在会议上给我们介绍了 hooks 的三个关键的 API, 分别是 State Hooks , Effect Hooks , Custom Hooks(自定义 hooks)
state Hooks (useState)
useState 这个方法可以为我们的函数组件带来 local state, 它接收一个用于初始 state 的值, 返回一对变量. 让函数组件拥有自己的组件.
首先如果我们需要用 classes component 实现一个点击按钮 +1 组件应该怎么写呢?
- import React from 'react';
- class Example extends React.Component {
- constructor(props) {
- super(props);
- this.state = {count: 0};
- this.clickBtn = this.clickBtn.bind(this);
- }
- clickBtn = () => {
- this.setState({
- count: this.state.count + 1;
- });
- }
- return (
- <div>
- <p>You clicked {this.state.count} times</p>
- <button onClick={this.clickBtn}>
- Click me
- </button>
- </div>
- );
- }
那使用 useState 是怎么样的呢? 可以看见非常清晰明了.
- // 一个简单的点击计数
- import { useState } from 'react';
- function Example() {
- const [count, setCount] = useState(0);
- return (
- <div>
- <p>You clicked {count} times</p>
- <button onClick={() => setCount(count + 1)}>
- Click me
- </button>
- </div>
- );
- }
- Effect Hooks (useEffect)
Effect Hooks 用于处理一些带有副作用的操作, 下面通过监听窗口宽度的变化代码为例, 说明 effect hooks 的使用 fangfa
- import { useState } from 'react';
- function windowWidth() {
- const [width, setWithd] = useState(Windows.innerWidth);
- useEffect(() => {
- const handleResize = ()=>{
- setWidth(Windows.innerWidth);
- }
- Windows.addEventListener('resize', handleResize);
- });
- return (
- <p> Windows width is {width}</p>
- )
- }
useEffect 可以传入第二个操作来避免性能的损耗, 如果第二个参数数组中的成员变量没有变化则会跳过此次改变. 如何传入一个空数组 , 那么该 effect 只会在组件 mount 和 unmount 时期执行.
- import { useState } from 'react';
- function windowWidth() {
- const [width, setWithd] = useState(Windows.innerWidth);
- useEffect(() => {
- const handleResize = ()=>{
- setWidth(Windows.innerWidth);
- }
- Windows.addEventListener('resize', handleResize);
- }, [width]); // width 没有变化则不处理
- return (
- <p> Windows width is {width}</p>
- )
- }
useEffect 中还可以通过让函数返回一个函数来进行一些取消兼容之类的清理操作, 比如取消订阅等
- import { useState } from 'react';
- function windowWidth() {
- const [width, setWithd] = useState(Windows.innerWidth);
- useEffect(() => {
- const handleResize = ()=>{
- setWidth(Windows.innerWidth);
- }
- Windows.addEventListener('resize', handleResize);
- return () => {
- // 取消监听窗口的宽度变化
- Windows.removeEventListener('resize');
- }
- });
- return (
- <p> Windows width is {width}</p>
- )
- }
如上所示, 内置的 React Hooks 如 useState 和 useEffect 充当基本构建块. 我们可以直接在组件中使用它们, 或者我们可以将它们组合到自定义 Hook 中, 例如 useWindowWidth. 使用自定义 Hooks 感觉就像使用 React 的内置 API 一样.
Custom Hooks 自定义组件
接着上面的监听窗口大小的代码, 我们接着讲自定义 hooks, 证明 react hooks 是怎么使到组件内的逻辑可复用的.
Talk is cheap, show me the code.
- // 一个显示目前窗口大小的组件
- function responsiveComponent(){
- // custom hooks
- const width = useWindowWidth();
- return (
- <p > 当前窗口的宽度是 {width}</p>
- )
- }
上面的代码只有几行, 非常清晰明了说明了他的作用就是监听当前窗口的变化, 这就是 Hooks 的目标 - 使组件真正具有声明性, 即使它们包含状态和副作用.
我们来看看如何实现这个自定义 Hook. 我们使用 React 本地状态来保持当前窗口宽度, 并在窗口调整大小时使用副作用来设置该状态
- import { useState, useEffect} from 'react';
- // custom hooks to listen Windows width change
- function useWindowWidth(){
- const [width, setWidth] = useState(Windows.innerWidth);
- useEffect(() => {
- const handleResize = ()=>{
- setWidth(Windows.innerWidth);
- }
- Windows.addEventListener('resize', handleResize);
- }, [width]); // width 没有变化则不处理
- return width;
- }
[在线编辑例子] https://codesandbox.io/s/2vx1j6qjjr
React Hooks 的规则
Hooks 是 JavaScript 函数, 但它们强加了两个额外的规则:
只能在顶层调用 Hooks. 不要在循环, 条件或嵌套函数中调用 Hook.
仅从 React 功能组件调用 Hooks. 不要从常规 JavaScript 函数中调用 Hook. (还有另一个地方可以调用 Hooks-- 你自己的定制 Hooks.)
其他 Hooks
这里有一些不常用的内置 Hook. 例如, useContext 允许您订阅 React 上下文而不引入嵌套:
- function Example() {
- const locale = useContext(LocaleContext);
- const theme = useContext(ThemeContext);
- // ...
- }
发现一个很有趣的仓库, https://github.com/streamich/react-use , 包含了很多很有趣的自定义 hooks
hooks 是如何工作的
以下内容翻译自 .
react hooks 其实只是一个数组, 并不是奇妙的魔法.
如何实现 useState() 方法
让我们在这里通过一个例子来演示状态 hooks 的实现如何工作.
首先让我们从一个组件开始:
- function RenderFunctionComponent() {
- const [firstName, setFirstName] = useState("Rudi");
- const [lastName, setLastName] = useState("Yardley");
- return (
- <Button onClick={() => setFirstName("Fred")}>Fred</Button>
- );
- }
hooks API 背后的想法是你可以使用一个 setter 函数作为 hook 函数中的第二个数组项返回, 而 setter 将控制由 hook 管理的状态.
那么 React 与此有什么关系呢?
让我们了解这在 React 内部如何工作. 以下内容可在执行上下文中用于呈现特定组件. 这意味着此处存储的数据位于正在渲染的组件之外. 此状态不与其他组件共享, 但它保留在可以随后渲染特定组件的范围内.
1)初始化
创建两个空数组: setters 和 state
将光标设置为 0
image.PNG
初始化: 两个空数组, Cursor 为 0
2) 首次渲染
首次运行组件功能.
每次 useState()调用, 当在第一次运行时, 将 setter 函数 (绑定到光标位置) 推送到 setter 数组, 然后将某个状态推送到 state 数组.
image.PNG
第一次渲染: 作为光标增量写入数组的项目.
3) 后续渲染
每个后续渲染都会重置光标, 并且只从每个数组中读取这些值.
image.PNG
后续渲染: 从数组中读取的项目为光标增量
4) 事件处理
每个 setter 都有一个对它的光标位置的引用, 因此通过触发对任何 setter 的调用, 它将改变状态数组中该位置的状态值.
image.PNG
Setters"记住" 他们的索引并根据它设置内存.
通过伪代码实现 useState 功能
这是一个演示实现的代码示例:
- let state = [];
- let setters = [];
- let firstRun = true;
- let cursor = 0;
- function createSetter(cursor) {
- return function setterWithCursor(newVal) {
- state[cursor] = newVal;
- };
- }
- // useState 的伪代码实现
- export function useState(initVal) {
- if (firstRun) {
- state.push(initVal);
- setters.push(createSetter(cursor));
- firstRun = false;
- }
- const setter = setters[cursor];
- const value = state[cursor];
- cursor++;
- return [value, setter];
- }
- // 模拟使用 useState
- function RenderFunctionComponent() {
- const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
- const [lastName, setLastName] = useState("Yardley"); // cursor: 1
- return (
- <div>
- <Button onClick={() => setFirstName("Richard")}>Richard</Button>
- <Button onClick={() => setFirstName("Fred")}>Fred</Button>
- </div>
- );
- }
- // 模拟 Reacts 渲染周期
- function MyComponent() {
- cursor = 0; // 重置光标的位置
- return <RenderFunctionComponent />; // render
- }
- console.log(state); // Pre-render: []
- MyComponent();
- console.log(state); // 首次渲染: ['Rudi', 'Yardley']
- MyComponent();
- console.log(state); // 后续渲染: ['Rudi', 'Yardley']
- // 点击'Fred' 按钮
- console.log(state); // 点击后: ['Fred', 'Yardley']
总结
Hooks 还处于早期阶段, 但是给我们复用组件的逻辑提供了一个很好的思路, 大家可以在 react-16.7.0-alpha.0 中体验.
来源: https://www.qcloud.com/developer/article/1360473