年初的时候,从头大的flutter终于转回了老本行js代码,就此第一次开始接触了hooks。

一开始看useState这些api其实很奇怪的,返回一个数组,第一个数组项目是value,第二个是setValue的函数。然后就是useEffect,useCallback这些,有点套不上原来class组件的思维。但一年下来,感觉非常爽,就个人感觉hooks理念相比其他前端框架跃进了一大截。

为什么需要hooks

React官方提案中,列举了三个理由

  • Hooks let you reuse logic between components without changing your component hierarchy.
  • Hooks let you split one component into smaller functions based on what pieces are related.
  • Hooks let you use React without classes.

我说下我个人理解:

增强function组件

这个hooks最招牌的好处,function组件本身非常简洁,但在React原来的写法中,只能承担渲染无state变化逻辑的纯组件,但引入hooks后,能使函数组件使用管理state和“管理声明周期”的能力。

从理解上更符合React理念

React核心过程UI=f(state),其本身就是一个函数组件。函数组件精准的阐述了React对应构建用户界面的理念。class组件更像是非必要的妥协。而且有时也有问题:函数式组件与类组件有何不同?

而hooks对应生成最终UI的state管理就像函数式编程的嵌套调用。

// 以下是伪代码

// 函数组件我们这么写
const APP = function () {
  const [state1] = useState();
  const [state2] = useState();
  render({
    state1, 
    state2
  });
}
// 就如同函数式编程的嵌套调用
APP = React.render(
  useState2(
    useState1(
      initState
    )
  )
);复制代码

这一切看起来非常自然且易于调试理解,甚至就像dan的博客提到“Bug-O” 表示法的bug复杂度是bug(o)。

逻辑复用

这个写过的都知道,hooks的状态复用非常爽。其实其他框架在之前都解决了ui的组件化问题,但常常复杂工程带来的就是集成这些小组件的主控组件非常庞大,需要管理非常多的子组件状态。而hooks则可以将这些state拆分出去。

hooks原理简述

来看看hooks在React的数据结构。在新版filber架构中,用fiber树来表示整个vdom结构,函数组件也是一个fiber节点。fiber用child和return,sibling来指示fiber节点的子节点,父节点和兄弟节点。因此看似是个树结构,实际是个链表来串联起来的。

深入React笔记 — 谈谈hooks_hooks

而在每个fiber的数据结构中,有一个memoizedState属性,这个就是存储hooks的地方,每个函数组件有多个hooks,因此memoizedState也是一个链表结构,来串联起来每一个hooks。而在每个hooks节点里面,也有一个memoizedState,这里面存储着每个hooks的数据(例如state,依赖的effect等等)。

在第一次mount阶段,会根据顺序将hooks对象生成并按照顺序一个个加入到memoizedState链表结构中。在hooks执行的时候,会有个workInProgressHook在不断指向hooks链表的最新一个节点,从而对应找到相应的hooks数据,根据更新队列queue,计算出hooks的存储数据memoizedState(所以顺序很重要)。

将hooks加入链表后,React将hook.memoizedState和dispatch返回,dispatch则已经附上了当前的hooks状态,和当前fiber关联,然后用闭包返回保存起来,更新的时候就和当前的fiber绑定关系了。hooks执行阶段,会调用这个dispatchAction,在函数内部,会创建update对象,形成链表,存在hooks数据的queue下。

  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // 设置queue.dispatch, 当前fiber被关联到queue.dispatch中
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  //4. 返回dispatchAction操作接口
  return [hook.memoizedState, dispatch];复制代码

hooks核心就两点 1. 链表 2.闭包(保留了当前的现场)。

hooks的实践

hooks的分散和集中

之前说hooks的一点非常好的地方就是逻辑的复用非常方便,可以将管理state逻辑非常容易的拆分出去,但有时经常碰到“复用”过度的地方:hooks嵌套组合非常复杂,导致阅读调试非常烦。我曾经遇到的例子如下图:

深入React笔记 — 谈谈hooks_hooks_02

类似于这样吧,嵌套了四五层,设计者必须小心管理和划分每个hooks职责,但即使这样,也使得很多逻辑分散,不知道一个state的变化到底是哪里导致的,难以阅读。但如果过于hooks设计的集中又会使得代码逻辑集中庞杂,失去了hooks的优势。因此复杂项目需要把握好hooks的内聚和耦合。

依赖追踪的陷阱

hooks引入了副作用及依赖变化的概念。但即使hooks用了很长的时间,也经常会碰到useEffect死循环渲染,和一些不必要的重复render。官方设计了useCallback和useMemo来解决,但大型项目参与人多和代码量增大的情况下,追踪依赖就变成了挺大的调试负担。