likes
comments
collection
share

践行代数效应的 React hooks 是如何实现的

作者站长头像
站长
· 阅读数 52

代数效应

如果你只是一个普通的React用户,那什么都不用知道,用就行了;如果你是个像我一个的好奇宝宝,那就请读下去吧。

下面是原文的地址有兴趣的可以瞅瞅。 zhuanlan.zhihu.com/p/76158581 zhuanlan.zhihu.com/p/169805499

我理解的代数效应

和初高中数学一样,举个简单的例子,有一道题目是 a^2 + b ^3 + c^4 + x = 100,求 x 的值,乍一看不会做不出来,再往后看题: 已知 a^2 + b ^3 + c^4 = 44 。那这个时候我们将 a^2 + b ^3 + c^4 = 44 代入式子得到 x + 44 = 100,很轻松就得到了 x = 66 。我们并不需要知道 a b c 的值,也不需要知道 a^2 + b ^3 + c^4 为啥等于 44,拿他解题就完事了。这个题也因为有这个代入的式子变得简单了。就上面文章中介绍的未来 js 的语法也一样,这个语法和上面题的代入式也一样,最终的目的都是为了让我们开发代码变的简单,我们并不需要去关注这个语法帮我们做了什么事。因此我对代数效应的理解就是将某些复杂或者通用的过程或者解法提炼出来通过更简单的使用方式来替代,让解决问题的过程变得简单。

为什么说 React hooks 是在践行代数效应

我们函数组件开发中,可以通过 React 提供的 hooks 来存储数据,作为一个普通的 React 用户,那我们什么都不用知道(怎么存的这个值,存在哪),用就行了,这就和前面介绍的代数效应的理念一致了。因此 React hooks 是在践行代数效应。

hooks 原理

下面我们一步步看看 hook 怎么实现的,先看看 hook 的结构,也就是怎么存的,存在什么地方的。

hooks 的结构

一个 hook 对应一个对象,对象长下面这个样子。

const hook: Hook = {
    memoizedState: null, // 我们取到的值
    baseState: null,  // 更新相关字段
    baseQueue: null, // 更新相关字段
    queue: null, // 更新相关字段
    next: null, // 下一个hook
  };
  // effect 的结构,最终存储在hook对象的 memoizedState 上
 const effect: Effect = {
    tag, // effect 类型
    create, // effect 回调
    destroy, // 销毁函数
    deps, // 依赖项,
    // Circular
    next: (null: any),
  };
  // hook 的更新结构
 const update: Update<S, A> = {
    lane, // 优先级
    action, // 具体的更新
    hasEagerState: false, // 优化相关
    eagerState: null, // 优化相关
    next: (null: any), // 下一个更新
  };

单个 hook 最终通过链表的形式连接在一起,存储在对应 Fiber 节点的 memoizedState 属性上。

践行代数效应的 React hooks 是如何实现的 另外,effect 还会以链表的形式存在 Fiber 的 update 的 lastEffect 属性中,个人理解是为了方便后续找到hook 并执行。

为什么 React hook 会有两条使用规则。

React hook很好用,但是他有两条限制:

1、只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook,  确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。

  • 为什么?

我们可以在单个组件中使用多个 State Hook 或 Effect Hook,那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。如果在条件语句中使用了 hook ,那么就不能保证顺序,你通过hook 获取到的值就会串台。我理解这也是为什么 hook 要用链表来存储的一部分原因吧,只需要顺序遍历。另一部分原因是在当前场景下链表相对于数组性能更好一点。

2、只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook

  • 为什么?

这个从 hook 存储的位置就很好解释了,因为 hook 是存储在 Fiber 上的,脱离了 React 环境就没办法正常运行了。

hook 运行流程

接下来以下面这段代码为例,看看 hook 的运行流程

function App() {
  const [num, setNum] = useState(1);
  const [count, setCount] = useState(2);
  useEffect(()=>{
    console.log('执行effect');
    return ()=>{
      console.log('卸载 effect');
    }
  },[num])
  const [state, dispatch] = useReducer(reducer, 1);

  useLayoutEffect(()=>{
    console.log('执行LayoutEffect');
    return ()=>{
      console.log('卸载 LayoutEffect');
    }
  })

  const clickHandler = () =>{
    setNum(num + 1);
    setNum(num + 2);
    setCount(count * 2);
  }

  return (
    <div className="App">
      {count}
      <div onClick={clickHandler}>
        更新 {num}
      </div>
    </div>
  );
}

React 对 hook 的处理都在 Render 阶段,入口在 renderWithHooks 函数中,在这个函数中会执行函数组件对应的函数。

挂载阶段

APP函数刚开始执行时,Fiber.memoizedState 为 null, Fiber.updateQueue 也为 null。 当执行到第一个 useState 时,会创建对应的 hook 对象,并挂在 Fiber.memoizedState上 最后 useEffect 会返回一个数组[hook.memoizedState, dispatch],之后的 hook 生成的 hook 对象都会挂在上一个hook 的 next 上。在执行 useEffect 时,会得到一个 hook 对象,然后会创建一个 effect 对象,这个对象放在 hook 的 memoizedState 属性上,同时也会把这个 effect 对象挂在Fiber.updateQueue.lastEffect(这个属性会一直指向最后一个 effect 对象,这个 effect 的 next 一直指向第一个 effect 对象会形成一个环形链表)上。当所有 hook 执行完成后,就得到了下图这样的 Fiber 结构。

践行代数效应的 React hooks 是如何实现的

更新阶段

首先我们点击页面,此时会执行点击事件触发一个更新给 num + 1,count +2,那这时候 React 干了什么呢? React 会根据顺序执行 hook 的 dispatch 函数(也就是代码中的 setNum 和 setCount ),dispatch 函数会为对应的 hook 创建一个更新对象Update(对象详情见前面hook结构)并添加更新任务并开始调度,这个 Update 最终会挂在 hook.queue.pending 上,结构和前面讲到的 effect 一样也是一个环形链表。 当点击事件执行完成,Render 开始之前会得到下面的 Fiber 结构。 践行代数效应的 React hooks 是如何实现的 接下来会在新一轮任务的 Render 阶段(renderWithHooks 函数)中执行 APP 函数,和前面挂载阶段一样,依次生成新的 hook 对象链表,不同的是,新的 hook 对象是基于老 hook 对象生成的,如果有更新的话也会把更新应用到新 hook 对象上。当执行到 effect 函数时,会判断 deps (依赖项)中是否发生了改变,如果改变则打上需要执行的 tag。 践行代数效应的 React hooks 是如何实现的

如何把更新应用到新 hook 上的。

以上面的 num 更新为例: 下面是丐版的应用更新代码

执行了 setNum(num + 1) 以及 setNum(num + 2);
const hook = {
  baseQueue: null, 
  baseState:   1,
  memoizedState: 1,
  queue:{}
}
//下面这个对更新的引用是为了方便描述更新对象的环形链表
// 第一个更新
hook.queue.penging = {
    action:2 ,
    eagerState: 2,
    hasEagerState: true,
    lane: 1,
    next: null
}
// 第二个更新
hook.queue.penging.next = {
    action:3 ,
    eagerState: null,
    hasEagerState: false,
    lane: 1,
    next: null
}
hook.queue.penging.next.next = hook.queue.penging;
hook.queue.penging = hook.queue.penging.next;

// 处理更新
let newState = hook.memoizedState;
cosnt firstUpdate = hook.queue.penging.next;
let update = firstUpdate;
do {
    const action = update.action;
    newState = typeof action === 'function' ? action(newState) : action;  
} while(update && update !== firstUpdate)
// 更新应用完成
hook.memoizedState = newState;
return [hook.memoizedState, dispatch];
// 最终更新结果为 num 为 3;

如果执行执行 setNum(num =>num + 1) 以及 setNum(num =>num + 2);
// 第一个更新
hook.queue.penging = {
    action:num =>num + 1 ,
    eagerState: 2,
    hasEagerState: true,
    lane: 1,
    next: null
}
// 第二个更新
hook.queue.penging.next = {
    action:num =>num + 1 ,
    eagerState: null,
    hasEagerState: false,
    lane: 1,
    next: null
}
那么最终,num 为 4

这里只简单的介绍下怎么操作的,实际情况会复杂一点。

最后

感谢大家的阅读,有不对的地方也欢迎大家指出来。

参考资料

React设计原理-卡颂 React18.2.0源码

转载自:https://juejin.cn/post/7253664416788283447
评论
请登录