有关react hooks这一堆东西的详细解释(万字长文)

本文主要会就 React hooks 中用的比较多的 useState 做一个详细讲解,其他的 hooks 会涉及到原理和使用但不会涉及到源码仿写,望周知

实现 useState

useState 是怎么用的

在实现一个 useState 之前,先来看看 useState 大致是怎么用的

import React, {useState} from "react";

export default function App() {
const [n, setN] = useState(0)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n+1)}>+1</button>
</p>
</div>
);
}

这里是一个简单的 加法器的应用,所以这里的效果就是点击一下 button, n 就会加 1。那么这里面的过程是怎么样的呢,这里分 2 部走

  • 首次渲染
    首次渲染时,会调用 App() 函数,得到一个 虚拟 DOM,然后再创建真实 DOM
  • 用户点击 button 时
    会先调用 setN(n+1) 函数,然后再次 render 渲染,渲染就调用 App() 函数,然后再次得到一个新的虚拟 DOM,这个虚拟 DOM 会和之前的虚拟 DOM 做一个 Diff 的操作,然后再更新对应的真实 DOM

这里注意,每次调用 App() 函数时,都会调用 useState(0)

所以这里我们可以看到两个问题

  • 在执行 setN时,n 不会变,但是 App() 函数会重新执行
  • App() 函数在执行后,在 useState(0) 时,每次的 n 的值都是不同的

分析 useState

对于数据 X 来说,使用 useState 会有如下的规律

  • setN

    1. setN 一定会修改数据 X, 将 n+1 存入 X
    2. setN 一定会触发 <App/> 并重新渲染(执行 App() 函数)
  • useState
    useState 肯定会从 X 读取 n 的最新值

  • X
    每个组件都有自己的数据 X,其实就是所谓的 state

实现 useState 初级版本

有了以上的分析,可以写一个简单的 useState

let _state; // 全局变量

const useState = initValue => {
// _state 用来保存一个全局的状态
_state = _state === undefined ? initValue : _state;
// setState 是一个 callback, 只要 setState 被调用,就会重新 render 一遍
// 同时 _state 也会得到更新
const setState = newValue => {
_state = newValue;
render();
};
return [_state, setState];
};

// render 函数
const render = () => {
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
};

这里为了方便起见,其 render 函数就直接调用了 ReactDom.render。比较关键的是这个全局的 _state,这个 _state 是一个全局的变量,为了下次渲染时不被初始化而设立的,setState 实际上就是一个回调,作用就是 保存 state 并开启渲染,所以这里可以知道为何在调用 setN 时,n 不会变了。因为此的 n 只是被存放在 _state 中,最后返回的 _state 中才是变化的 n,而只有再次渲染才能得到这个变化的 n

实现 useState 升级版本

但是目前还有一个问题,就是万一同一个组件,调用用了 2 次以上的 useState 怎么办。以上的初始版本的代码会带来什么问题呢?就是因为 _state 是全局的,导致组件的数据之间会相互冲突

解决思路

解决的思路有两个

  • _state 变成对象的形式,比如变成 _state: {n: 0, m: 0}
    实际上这样做是不可取的,因为 useState 中参数就是传一个初始值(initValue),这种情况下我们怎么知道是 n 是 0, 还是 m 是 0 呢
  • _state 变成数组的形式,比如 _state: [0, 0]
    这种是可以的,因为可以通过下标区分每个 useState 的数据

实现代码

let _state = []; // 全局变量
let index = 0; // 解决 state 冲突的问题,因为有可能使用多个 useState

const useState = initValue => {
// 保存当前的 index
let currentIndex = index;
// _state 用来保存一个全局的状态
_state[currentIndex] =
_state[currentIndex] === undefined ? initValue : _state[currentIndex];
// setState 是一个 callback, 只要 setState 被调用,就会重新 render 一遍
// 同时 _state 也会得到更新
const setState = newValue => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};

const render = () => {
index = 0; // 注意这里要 index 置零
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
};

既然要采取上面的数组的方案做,那么 _state 自然初始值就是一个数组。其他的逻辑并没有变化,主要是增加了一个全局的变量 index 以及一个 useState 中的临时变量 currentIndex。每次当 useState 执行时,index 就自增 1 与其他的 state 做区分。currentIndex 的作用是能让 _state[0] 有个初始值而不至于是 undefined。同时在每次 render 时需要将 index 置为零。这样一来每次执行 useState 得到的就是不同的 n 以及 setN

测试代码链接点击这里

数组方案的主要缺点

数组的一个很明显的缺点就是它的 调用顺序。因为数组非常强调顺序,所以在 React 中不允许出现类似如下的代码

import {useState} from 'react'
function App() {
...
const [n, setN] = useState(0);
let m, setM;
if (n % 2 === 0) {
[m, setM] = useState(0);
}
...
}

React 就会报如下的错误

❌ :React Hook “React.useState” is called conditionally. React Hooks must be called in the exact same order in every component render

意思是说,React 不能依照条件调用 useState,在每个组件渲染时应该依照一样的顺序调用 hooks

所以在使用 React 相关的 hook 时,上面的代码是不被 React 允许的

数组方案的其他缺点以及解决方案

  • App 组件使用了全局的 _stateindex,那么其他组件怎么用?不还是冲突了么?
    只需要在每个组件内部维护一个 _stateindex 即可
  • 对于类组件来说,不存在全局作用域重名的问题,对于函数组件来说就有,这个时候怎么办?
    只需要将 _stateindex 放在组件对应的虚拟 DOM 对象上即可

原理是这样的

在函数组件调用了 useState 时,就会更新绑定在虚拟 DOM 对象上的 _stateindex,当下次数据有变化触发 render 时,会再次调用 useState 并生成一个新的虚拟 DOM 对象,这个对象会跟之前的旧对象进行一个 Diff 的操作,对比出要更改的节点后会出一个 Patch,然后再根据这个 Patch 更新虚拟 DOM 上的数据,这里面包括了 _stateindex,最后再修改生成一个真实 DOM 节点。

useState 注意点

  • 对于其使用状态

    const [n, setN] = React.useState(0) // 这里 n 的初始值就是 0
    const [m, setM] = React.useState({name: 'xxx'}) // 这里 m 的初始值就是 {name: 'xxx'}
  • 不可以局部更新
    代码链接

    const [user, setUser] = useState({ name: "xxx", age: 18 });
    const onClick = () => {
    setUser({
    name: "Jack"
    });
    };
    return (
    <div className="App">
    <h1>{user.name}</h1>
    <h2>{user.age}</h2>
    <button onClick={onClick}>Click</button>
    </div>
    );

    从贴出的代码链接可以看出,点击 button 后,user.age 并没有显示出来。这是因为 setState 并不会帮你合并属性,要合并的话需要自己额外操作,比如利用拓展运算符 ...

  • setState(obj) 中的地址要变
    代码链接

    const [user, setUser] = useState({ name: "xxx", age: 18 });
    const obj = user // 注意这里 !!!
    const onClick = () => {
    obj.name = 'Jack' // 注意这里 !!!
    setUser(obj); // 在这种情况下页面数据是不会更新的 !!!
    };
    return (
    <div className="App">
    <h1>{user.name}</h1>
    <h2>{user.age}</h2>
    <button onClick={onClick}>Click</button>
    </div>
    );

    只要 obj 的地址没变,React 就认为其数据没有变化,因此就不会触发更新

  • useState 可以接受一个函数

    const [state, setState] = useState(() => initialState) // 该函数返回初始值 state,且只执行一次
  • setState 可以接受一个函数

    代码链接

    const [n, setN] = useState(0)
    const onClick = ()=>{
    setN(n+1)
    setN(n+1) // 你会发现 n 不能加 2
    // setN(i=>i+1)
    // setN(i=>i+1) // 但是使用了函数之后就可以加 2
    }
    return (
    <div className="App">
    <h1>n: {n}</h1>

    <button onClick={onClick}>+2</button>
    </div>
    );

    所以为了避免 bug,应该优先使用这种在 setState 中传函数更新 state 的方式

useState 总结

  • 每个函数组件对应着一个 React 节点(FiberNode)
  • 每个节点保存着 _stateindex
  • useState 会读取 _state[index]
  • index 由 useState 出现的顺序决定
  • setState 会修改 _state,并且之后会触发更新

⚠ 注意:这里只是一个对 useState 的思路型源码,并非是 React hook 中的源码。另这里的 _state 对应源码中的 memorizedState,而 index 在源码中是利用链表来实现的。这里只是抛砖引玉一下。

useReducer

useReducer 本质就是用来践行 Flux/Redux 的思想的这么一个 hook,例子的代码链接在这里

使用 useReducer

首先是声明一个 initial 初始变量

const initial = {
n: 0
};

然后声明 reducer

const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};

然后在 App() 函数组件中使用将这个 reducer 以及 initial 值当作参数传入 useReducer 中,返回的就是一个 statedispatch

// 注意这里参数的传入顺序
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);

可以看出,以上的代码是分几步走的

  1. 创建初始值 initial
  2. 创建 reducer 这个操作的集合,它接受一个 state 和一个 action
  3. reducerinitial 传给 useReducer,并得到一个读(state)和写(dispatch)的 API
  4. 调用写(dispatch) API 传入 {type: '操作类型'} 去对 state 做相应的更新

从以上描述我们不难看出,这个 useReducer 可以用来代替 redux

使用 useReducer 代替 redux

代码链接

由于代码太长了所以这里就不放代码了,详情可以点击链接中的代码去查看

由以上的代码我们可以知道一个基本的使用 useReducer 代替 redux 的步骤为

  1. 声明一个 store 对象,对象中存放着该组件使用的数据
  2. 声明一个 reducer ,所有的相关的 crud 之类的操作都往这个里面放
  3. 使用 createContext 创建一个 Context,为组件间传递数据做准备
  4. App 组件中使用 useReducer 创建读写 API,在其子组件中使用 useContext 创建读写 API(读: state 访问数据, 写: dispatch 对数据进行操作)
  5. 将第 4 步的返回内容放入到第 3 步的 Context.Provider 组件的 value
  6. 使用 Context.ProviderContext 提供给所有组件
  7. 然后各个子组件使用 useContext(Context) 获取到读写 API

当然了,如果涉及到模块化的问题,将代码分开就可以了。比如

Context.js 中可以放入

// Context.js
mport React from "react";

const Context = React.createContext(null);
export default Context;

reducers 可以单独建立一个文件夹,里面放入的都是对应数据的一些操作,比如对 book 的操作可以这么写

// reducers/books_reducer.js
export default {
setBooks: (state, action) => {
return { ...state, books: action.books };
},
deleteBook: () => {}
};

然后 App 子组件就放入 components 文件夹中,里面就是组件的一些东西了,这里不再赘述

最后的最后,在从 src/index.js 中引入这些文件即可,然后再做相应的代码优化。实际上模块化的本质就是分类整理代码,这样做了之后基本就能起到替代 redux 的作用了

useReducer 注意点

需要注意的是,useReducer 本身也不会帮你合并属性,所以同 useState 一样,要合并属性时需要自己动手操作

useReducer 总结

总的来说,useReducer 这个 hook 其实本质上借鉴了 Flux/Redux 的一些思想,我们可以通过一些 hooks 来创建一个类似 redux 的写法,只要将其模块化后就可以替代 redux

useRef

在理解 uesRef 之前,先看一个例子

useState 的 bug

import React, { useState } from "react";

export default function App() {
const [n, setN] = useState(0);
const log = () => setTimeout(() => console.log("n =", n), 3000);
return (
<div className="App">
<p>{n}</p>
<p>
<button
onClick={() => {
setN(n + 1);
}}
>
+1
</button>
</p>
<p>
<button onClick={log}>log</button>
</p>
</div>
);
}

现在有个问题,在点击完 +1 button 然后点击 log,打印出来的 n 是正常的。但是如果是先 log 然后立即执行 +1,那么我们可以发现一个 bug,就是 n 居然没有变化!!!居然还是之前的旧数据!!!

wtf, 这是什么情况???

先来看一张图

  • 如果先点 +1 再点 log
    setN 会先执行,然后触发 render,3 秒过后,log 里面读到的 n 就是 已经从 useState 里面读出来的 n,所以 n = 1
  • 如果先点 log 再点 +1
    log 会先执行,但是注意,此时 log 里的 n 还是初始状态下读到的 旧的 n,随后 setN 执行后会产生一个新的 n。然而此时其触发的 render 早就更新页面上的 n 了。所以会出现这种 “滞后” 的 bug

useState bug 的解决方案

  • nsetN 挂在全局变量 window
    这种解决方案太傻了,不考虑变量之间相互污染的问题么?

  • 使用 useRef

    import React, { useRef } from "react";

    export default function App() {
    const n = useRef(0);
    const log = () => setTimeout(() => console.log("n =", n.current), 3000);
    return (
    <div className="App">
    <p>{n.current}</p>
    <p>
    <button
    onClick={() => {
    n.current += 1;
    }}
    >
    +1
    </button>
    </p>
    <p>
    <button onClick={log}>log</button>
    </p>
    </div>
    );
    }

    但你会发现这种方案,页面上的元素不更新了,于是你可以这么改代码

    import React, { useRef, useState } from "react";

    export default function App() {
    const n = useRef(0);
    const update = useState(0)[1]; // 注意这里!!!
    const log = () => setTimeout(() => console.log("n =", n.current), 3000);
    return (
    <div className="App">
    <p>{n.current}</p>
    <p>
    <button
    onClick={() => {
    n.current += 1;
    update(n.current); // 注意这里!!!
    }}
    >
    +1
    </button>
    </p>
    <p>
    <button onClick={log}>log</button>
    </p>
    </div>
    );
    }

    这里做了一个小 trick,就是使用 useState 返回的第二个函数强制更新。。。当然这种方式太过 hack 肯定是不推荐的。不过由此我们可以知道 useRef 可以作为一种 贯穿始终的状态 来解决 useState 使用上的 bug

与 forwardRef 的关系

使用 useRef 时,props 是不能直接传递 ref 属性的,比如下面的代码

function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button2 ref={buttonRef}>按钮</Button2>
</div>
);
}

const Button2 = props => { // 这里的 props 中的 ref 传不进来
return <button className="red" {...props} />;
};

浏览器会报错

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

React 这个时候提醒你使用 React.forwardRef() 来传 props 中的 ref 属性,于是你可以这么改

function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按钮</Button3>
</div>
);
}

// 注意这里使用 React.forwardRef 多包裹了一层
const Button3 = React.forwardRef((props, ref) => {
return <button className="red" ref={ref} {...props} />;
});

当然你也可以通过两次传递 ref 来得到 button 的引用,这里就不细讲,有兴趣的童鞋可以参考这个链接
这个例子本质上也是参考上面代码的例子,只是这里我们需要理解的是 ref 属性是可以传一个引用的

所以由上面的例子我们知道了,其实 useRef 既能引用 DOM 对象也能引用普通对象,但是在传 ref 时需要用到 forwardRef

而对于 forwardRef 来说,由于 props 不包含 ref(主要是大部分时候也不需要 props 去传一个 ref),这个时候才需要 forwardRef

forwardRef 本身就是通过 ref 的透传来实现对于指定的 DOM 的定位的,这一点和 Vue 的 refs 是一样的

useRef 总结

由于每次的渲染,组件函数中对应的 state 都会不一样,比如上述代码中的 n。如果希望拿到同一个 n,那么可以考虑使用 useRef 这个 hook,那么这个时候你就需要访问的是 n.current 而不是 n 了。

useRef 不能做到变化时自动更新 render,前面说过解决方案,就是通过监听 n,当 n.current 变化时调用 update 解决,这里需要配合 useState 这个 hook 来使用。也就是说这个功能需要你自己加

useContext

其实想想,Context 的译文就是 “上下文”。那么什么是上下文呢?

  • 全局变量 是全局的 上下文
  • 上下文 是局部的 全局变量

实际上 useContext 的用法跟 useRef 差不多,不过 useContext 不仅能贯穿始终,还能贯穿不同组件

使用 useContext

import React, { useContext, useState } from "react";

const themeContext = React.createContext(null);

export default function App() {
const [theme, setTheme] = useState("red");
return (
// 这里 value 传入读写 API
<themeContext.Provider value={{ theme, setTheme }}>
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
</themeContext.Provider>
);
}

function ChildA() {
const { theme, setTheme } = useContext(themeContext); // 子组件中可以用 setTheme 函数
console.log(theme); // 子组件中可以拿到 theme 状态
return (
<div>
<button onClick={() => setTheme("red")}>red</button>
</div>
);
}

function ChildB() {
const { setTheme } = useContext(themeContext); // 子组件中可以用 setTheme 函数
return (
<div>
<button onClick={() => setTheme("blue")}>blue</button>
</div>
);
}

代码链接在这里

所以我们从中可以看到使用 useContext 的步骤了

  1. 使用 C = CreateContext(initial) 创建上下文
  2. 使用 C.Provider 来限定要传的值的作用域在这个范围内
  3. 然后在 C.Provider 的作用域内使用 useContext(C) 来使用上下文(也就是 C.Provider 组件里面的 value)

useContext 注意点

useContext 这个 hook 并不是响应式的,在一个组件中将 C 里面的值改变了(比如上述代码中的 theme 改变了),另一个组件并不会知道这个变化。所以如果需要能够响应式,最好是配合 useState 来使用

useContext 总结

如果希望拿到同一个 n,那么可以考虑使用 useContext 这个 hook,只需要将 useRef 的地方改成 useContext 就行

useEffect

这个就是 “副作用” 了,什么意思呢?

  • 在 js 中,我们把对环境的改变就叫 副作用,一个很典型的例子就是 修改 document.title
  • 一定非得要把一些副作用的操作放到 useEffect 中执行么?不一定
  • 你可以把它理解为 afterRender ,因为这个总在 render 后执行

React 渲染的大致流程如下

App()          -->  执行          --> 生成虚拟 DOM --
|
render 完毕 |
↓ |
执行 useEffect <-- 改变外观 <-- 生成真实 DOM <--
↑ ↑
<div>1000</div> <div>0</div>

如上面所示,在改变外观这里,render 完毕之后,就会执行 useEffect

使用 useEffect

  • useEffect(() => {}, [])
    由于第二个参数传的是一个空数组,所以第一个参数函数只有在第一次渲染时执行
  • useEffect(() => {}, [n])
    由于第二个参数传的是一个 [n],这里表示只要 n 有变化,那么第一个参数函数就会执行
  • useEffect(() => {})
    由于没有传第二个参数,这里就表示 任何一个 state 变化时都不会执行

useEffect 还有个用法,就是当组件处于 componentWillUnmount 时,即组件快要挂掉时使用,比如

useEffect(() => {
let id = setInterval(() => {}, 1000)
return () => { // 这里 return 一个函数,当组件挂掉时调用这个,清理掉定时器,避免内存泄露
clearInterval(id)
}
}, [])

useEffect 还有一个特点跟 useState 一样的,就是如果同时存在多个 useEffect, 会按照其出现的次序执行

useEffect 总结

  • 在对环境有些副作用操作的时候用 useEffect
  • 前面说的 useEffect 的几种用法中,可以都同时存在
  • 可以将 useEffect 理解成 “afterRender”,因为它就是在 render 之后执行的

useLayoutEffect

如果说 useEffect 在浏览器渲染完成后执行,那么 useLayoutEffect 就是在浏览器渲染完成前执行,它的位置在如下所示的位置

App()          -->  执行          --> 生成虚拟 DOM --
|
render 完毕 useLayoutEffect |
↓ ↓ |
执行 useEffect <-- 改变外观 <-- 生成真实 DOM <--
↑ ↑
<div>1000</div> <div>0</div>

useLayoutEffect 特点

  • 由图可知,useLayoutEffect 总是比 useEffect 先执行
  • useLayoutEffect 里的操作最好是影响了 layout,虽然它性能更高,但是如果在这个里面的操作很多,它会影响用户看到画面变化的时间。从用户的角度来说,这是很影响用户体验的

useLayoutEffect 总结

  • 是跟 useEffect 差不多的玩意儿,不同之处在于其执行的优先级,它的优先级比 useEffect
  • 为了用户体验,在能使用 useEffect 解决问题的前提下,尽量不要使用这个 useLayoutEffect

useMemo

这里 Memo 的全称应该是 Memorize 也就是 “记忆”,这个钩子的作用就是记忆代码,如果有多余的代码就不执行,怎么理解呢,在理解这个之前,需要先看看 React.memo

理解 React.memo

假设有如下的代码

function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};

return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button> /*点击 button 只让 n + 1*/
</div>
<Child data={m} /> /* 这里的 m 没有变化,按照理由来说 child 不应该重新渲染才对*/
</div>
);
}

function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码"); // 但是这里却执行了!!!
return <div>child: {props.data}</div>;
}

传入的 props.data 根本没有变化,即 m 没有变,变的只是 n,但是这里 Child() 每次都执行了,要是 Child() 函数里面有很多的代码,势必会造成页面的卡顿现象,于是这个时候,React.memo 就排上用场了

function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};

return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button> /*点击 button 只让 n + 1*/
</div>
<Child2 data={m} /> /* 这里的 m 没有变化,按照理由来说 child 不应该重新渲染才对*/
</div>
);
}

function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码"); // 使用了 React.memo 后,这里就没有执行了!!!
return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child)

如果 props 没变,就没有必要再次执行一个函数组件,这个 React.memo 是属于 React 优化的一部分。原理就是跟缓存的道理一样的。在 React 中,其默认有多余的 render。为了解决这个问题后面才出来 memo

React.memo 的 bug

请看如下的代码

function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
const onClickChild = () => { // 这里只是添加了一个 click 的回调而已。。。
console.log(m);
};

return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
{/* Child2 居然又执行了 */}
</div>
);
}

function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码"); // 这里的代码因为仅仅只是添加了回调的原因,居然又执行了!!!
return <div onClick={props.onClick}>child: {props.data}</div>;
}

const Child2 = React.memo(Child); // memo 在这里就没用了!!!

为什么在这种情况下 React.memo 的优化作用失效了呢?因为在点击 button 更新 n 的数据时会再次渲染页面,就会再次执行 App(),就会再次声明 onClickChild 这个函数,而这个函数的地址已经变了!!!!。因为这个函数地址变的原因,React 就判断其 props 产生了变化,于是就认为 Child 组件已经变化了,需要重新渲染它!!,于是乎 Child() 就会再次执行!!!

那么还有什么解决办法呢?有的,就是使用 useMemo,使用这个 hook 就不会有这个问题

使用 useMemo

使用 useMemo 去解决上面的 bug 的话,只需要将 onClickChild 函数修改成如下

const onClickChild = useMemo(() => {
const fn = div => {
console.log("on click child, m: " + m);
console.log(div);
};
return fn;
}, [m]); // 这里只要 m 变化才能说明 `onClickChild` 变化了,当然这里的 m 要是改为 n 会打印出旧的 n 的数据
  • useMemo 第一个参数是一个工厂函数 () => value, 第二个参数是依赖[n, m],只有当依赖变化时,才会计算出新的 value
  • 如果依赖没有变化,则服用之前的 value

useMemo 注意点

  • 如果本身 value 是一个函数,你就必须要这么写

    useMemo(() => x => console.log(x), [m]) // 注意这里是一个返回函数的函数
  • 如果觉得这很不优雅,推荐使用 useCallback

useMemo 总结

从本质上看 useMemo 有点像 Vue 中的 Computed,其计算属性本来就依赖一个会变化的数据,数据变化时 vue 会帮你做相应的更新操作

useMemo 一般是结合 React.memo 做一些代码层面的优化工作

useCallback

useCallback 就是为了解决上述 useMemovalue 是一个函数时的传参写法问题的,它的用法如下

useCallback(x => console.log(x), [m])

其等价于

useMemo(() => x => console.log(x), [m])

useCallback 总结

这个是跟 useMemo 功能相近的 hook,主要为了补充 useMemo 传参的写法用的

useImperativeHandle

你可以将其理解为 setRef,其作用就是 自定义 ref 的属性代码链接在这里

关键的代码在这一句

setRef(ref, () => {
return {
x: () => {
realButton.current.remove();
},
realButton: realButton
};
});

其实就是给 ref 增添了属性,让其对应的 ref 能够访问到 realButton 这个属性以及能够调用 x 这个方法

hooks 其他内容(dlc)

如何自定义 hook

useState 举例,可以将自己定义的 hook 作为一个模块封装起来然后 export 出去,具体来说就像如下代码所做的

import { useState, useEffect } from "react";

const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list); // 异步请求
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList,
addItem: name => {
setList([...list, { id: Math.random(), name: name }]);
},
deleteIndex: index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}
};
};
export default useList;

所以我们可以看到,这里本质上就是对 useState 这个 hook 做了一层封装,然后返回的依然是 state 以及 setState 这个方法,即返回的是一个读(查)写(增删改) API

自定义 hook 注意点

  • 也可以在自定义 hook 里面使用 Context API
  • useState 可以在函数里使用,关键是这个函数是在组件里面运行的就可以

Stale Closure

这个主要是被尤大吐槽过的,说他在读一些使用 hooks 写的库时就十分的脑壳疼,评价其为过时的闭包(stale closure)

那么过时闭包是怎么一回事呢,来看如下的代码

function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
const message = `Current value is ${value}`;
return function logValue() {
console.log(message);
};
}

return increment;
}

const inc = createIncrement(1);
const log = inc(); // logs 1
inc(); // logs 2
inc(); // logs 3
// Does not work!
log(); // logs "Current value is 1"

最后 log 居然还是之前第一次的 value, 这不科学!!!但是这是正常的,毕竟你保存的是第一个函数执行的地址,而其他函数执行地址跟这个不一样,所以才有这个问题

于是解决办法也有,其实只要每次得到最新的 value 就可以了

function createIncrementFixed(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
return function logValue() {
const message = `Current value is ${value}`; // 注意这里将这句移动到 logValue 里面来了 !!!
console.log(message);
};
}

return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // logs 1
inc(); // logs 2
inc(); // logs 3
// Works!
log(); // logs "Current value is 3"

所以说 hooks 中也有解决这些过时闭包的 bug 出现的措施,就是依赖更新。
比如 useEffect 中要传的第二个参数,比如 useStatesetState 传一个函数更新的形式,都是为了解决这个 bug 而出现的举措。
这里就不详细说明了,有兴趣的童鞋可以去参考我列出的链接

参考链接