设计思想

在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,
但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

好处:
1、跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
2、类定义更为复杂:
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注this的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
3、状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

注意事项

  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
    • 可以基于这条约束,窥探猜测下react的hook机制实现原理
  • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
  • 不能在useEffect中使用useState,React 会报错提示;
  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;

猜测下 在函数里使用 this.state
即赋予函数 状态变量的能力,这种能力可以想到是如何实现的?
===> 闭包。因为 对象是有行为的数据;而闭包是有数据的行为 哈哈

理解hook

loading逻辑在组件里作为通用逻辑,难以复用,即如何实现组件的逻辑复用?
1、render props
2、HOC
底层都是高阶函数
弊端:嵌套地狱

React Hooks 则可以完美解决上面的嵌套问题,它拥有下面这几个特性。

  1. 多个状态不会产生嵌套,写法还是平铺的
  2. 允许函数组件使用 state 和部分生命周期
  3. 更容易将组件的 UI 与状态分离

image.png

funciton hook VS class state

  1. function state 的粒度更细,class state 过于无脑。
  2. function state 保存的是快照,class state 保存的是最新值。
  3. 引用类型的情况下,class state 不需要传入新的引用,而 function state 必须保证是个新的引用。

image.png image.png
在 1s 内频繁点击10次按钮,上面两种写法的执行表现是一样吗?
在第一个例子中,连续点击十次,页面上的数字会从0增长到10。
而第二个例子中,连续点击十次,页面上的数字只会从0增长到1。

===> 主要是引用和闭包的区别

class 组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。

function component 里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了1次。

问题解决:
useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个

useRef 有下面这几个特点:

  1. useRef 是一个只能用于函数组件的方法。
  2. useRef 是除字符串 ref、函数 ref、createRef 之外的第四种获取 ref 的方法。
  3. useRef 在渲染周期内永远不会变,因此可以用来引用某些数据。
  4. 修改 ref.current 不会引发组件重新渲染。


image.png
想让函数组件也是从0加到10, 传入对象每次对象更新行不行,不行,即使 state 是个对象,但每次更新的时候,要传一个新的引用进去,这样的引用依然是没有意义

  1. const [state, setState] = useState({ count: 0 })
  2. setState({
  3. count: state.count + 1
  4. })

useEffect

useEffect 比较重要,它主要有这几个作用:

  1. 代替部分生命周期,如 componentDidMount、componentDidUpdate、componentWillUnmount。
  2. 更加 reactive,类似 mobx 的 reaction 和 vue 的 watch。
  3. 从命令式变成声明式,不需要再关注应该在哪一步做某些操作,只需要关注依赖数据。
  4. 通过 useEffect 和 useState 可以编写一系列自定义的 Hook。

useLayoutEffect

useEffect VS useLayoutEffect:

  1. useEffect 不会 block 浏览器渲染,而 useLayoutEffect 会。
  2. useEffect 会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行。

实现原理

hook属性依赖于组件,一个函数组件有唯一的hook属性
_hooks 属性里面保存了我们执行一个组件的时候,里面所有 Hook 方法相关的信息。

image.png

组件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放进 _list => currentIndex++ => 渲染结束
组件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,获取 _list[currentIndex]=> currentIndex++ => 重复上面步骤 => 更新结束

image.png

为什么 hooks 方法不能放在条件语句里面了。因为每次进入这个函数的时候,都是要和 currentIndex 一一匹配的,如果更新前后少了一个 Hook 方法,那么就完全对不上了,导致出现大问题。

  1. 每个组件实例上挂载一个 _hooks 属性,保证了组件之间不会影响。
  2. 每当遇到一个 hooks 方法,就将其 push 到 currentComponent._hooks._list 中,且 currentIndex 加一。
  3. 每次渲染进入一个组件的时候,都会从将 currentIndex 重置为 0 。遇到 hooks 方法时,currentIndex 重复第二步。这样可以把 currentIndex 和 currentComponent._hooks._list 中的对应项匹配起来,直接取上次缓存的值
  4. 函数组件每次重新执行后,useState 中还能保持上一次的值,就是来自于步骤3中的缓存。
  5. 由于依赖了 currentComponent 实例,所以 hooks 不能用于普通函数中。

useEffect

  1. useEffect(() => {
  2. console.log(count);
  3. }, [count]);
  1. 有两个参数 callback 和 dependencies 数组
  2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行
  3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行
  1. # useState
  2. var _state; // 把 state 存储在外面
  3. function useState(initialValue) {
  4. _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
  5. function setState(newState) {
  6. _state = newState;
  7. render();
  8. }
  9. return [_state, setState];
  10. }
  1. # deps依赖
  2. let _deps; // _deps 记录 useEffect 上一次的 依赖
  3. function useEffect(callback, depArray) {
  4. const hasNoDeps = !depArray; // 如果 dependencies 不存在
  5. const hasChangedDeps = _deps
  6. ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
  7. : true;
  8. /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  9. # 比较depArray 依赖,当发生改变时候 再执行cb
  10. if (hasNoDeps || hasChangedDeps) {
  11. callback();
  12. _deps = depArray;
  13. }
  14. }

问题:useEffect的执行
是每次render都会调用hook函数,但是函数绑定的cb不一定都执行

我们已经实现了可以工作的 useState 和 useEffect。
但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。(闭包内的单例变量是一个引用)所以外界通过useXX得到的数据,内部依赖使用的同一个,那么外界就会同更新

  1. const [count, setCount] = useState(0);
  2. const [username, setUsername] = useState('fan');

使用数组,来解决 Hooks 的复用问题

关键

  1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
  1. let memoizedState = []; // hooks 存放在这个数组
  2. let cursor = 0; // 当前 memoizedState 下标
  3. function useState(initialValue) {
  4. memoizedState[cursor] = memoizedState[cursor] || initialValue;
  5. const currentCursor = cursor;
  6. function setState(newState) {
  7. memoizedState[currentCursor] = newState;
  8. render();
  9. }
  10. return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
  11. }
  12. function useEffect(callback, depArray) {
  13. const hasNoDeps = !depArray;
  14. const deps = memoizedState[cursor];
  15. const hasChangedDeps = deps
  16. ? !depArray.every((el, i) => el === deps[i])
  17. : true;
  18. if (hasNoDeps || hasChangedDeps) {
  19. callback();
  20. memoizedState[cursor] = depArray;
  21. }
  22. cursor++;
  23. }

为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。
A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A:共享同一个 memoizedState,共享同一个顺序。
Q:“Capture Value” 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

真正react实现

虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

单链表顺序串联hook

React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。

  1. type Hooks = {
  2. memoizedState: any, // 指向当前渲染节点 Fiber
  3. baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
  4. baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
  5. queue: UpdateQueue<any> | null,// UpdateQueue 通过
  6. next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
  7. }
  8. type Effect = {
  9. tag: HookEffectTag, # effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
  10. // 看吧 果然是 帮用户订阅数据变化,再将回调放到生命周期里
  11. create: () => mixed, // 初始化 callback
  12. destroy: (() => mixed) | null, // 卸载 callback
  13. deps: Array<mixed> | null,
  14. next: Effect, // 同上
  15. };

memoizedState和cursor存在哪里

我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

image.png

和react生命周期结合

  1. var useState = (initState) => {
  2. let data = initState;
  3. const dispatch = (newData) => {
  4. data = newData;
  5. }
  6. return [data, dispatch];
  7. }

在每次渲染的过程中,函数都会被重新调用而重新初始化,这并不是我们期望的。因此我们需要一个数据结构对每次执行的 state 进行存储,同时还需要区分初始化和更新状态的不同执行方式。

  • 对于同一个 state,在 mounted 和 updated 的不同状态下,hook 是如何共享的 =>挂在FiberNode节点下
  • dispatchAction 中的 storeUpdateActions 和 updateState 中的 updateMemorizedState 是如何运作的

==> 在我们实际场景中,普遍存在着一个更新周期中多次调用的 re-render 行为

  1. function Demo () {
  2. const [count, setCount] = useState(0);
  3. return <div onClick={() => {
  4. setCount(count++);
  5. setCount(count++)
  6. setCount(count++)
  7. }}>{count}<div>
  8. }

实际上组件不会渲染3次,而是根据最后的状态渲染
这意味着在调用 dispatch 更新的时候,我们并不是直接进行更新逻辑,而是将其存储进行update时统一的调度更新,根据执行的有序性,我们采用队列存储一个 hook 的多次调用

采用链表形式进行存储:存储同个组件内的多个hook;(包括相同的 Hook 多次调用)

  1. type Hook = {
  2. memoizedState: any, // 上一次完整更新之后的最终状态值
  3. queue: UpdateQueue<any, any> | null, // 更新队列
  4. next: any // 下一个 hook
  5. }

整个链表在 mounted 的时候构建,在 update 时按照顺序执行。
因此不能在条件循环等场景下使用。

useEffect同理:
在mount时创建一个 hook 对象,新建一个 effectQueue,以单向链表的方式存储每一个 effect,将 effectQueue 绑定在 fiberNode 上,并在完成渲染之后依次执行该队列中存储的 effect 函数

与 useState 不同的一点是,useEffect 拥有一个 deps 依赖数组。当依赖数组变更的时候,一个新的副作用函数会被追加至链尾。

最佳实践-业务hook设计