【useState原理】源码调试吃透REACT-HOOKS(一)

1 导读

2022年了,用React开发不运用hook是不行的。同时一方面,因为我在日常开发中现已许久没数组初始化有运用class组数组指针件,所以一向关于hook的规划理念、完数组c语言成原理和相关源索引是什么意思码有必定的兴趣。原因无他,用hook真的太爽了。

开端之前,先抛出几个问题:

  1. react-hook处理了什么问题?
  2. react中的函数是无状况的,hook是怎样做到赋予appetite其状况的appointment
  3. 典型问题:为什么hook必须在顶层调用?–>引申:在函数组件中多个数组c语言hook是怎样记录的
  4. useMemouseCallback是怎样做缓存的?
  5. hoo索引超出矩阵维度k的调用进程,从挂载、初次烘托、二次烘托到销毁的流程?

从这儿开端,咱们一一解答。

2 HOOK的相关概念

和谐器目录 github.com/fac上下文切换ebook/re索引的作用

2.1 为什么要在re源码精灵永久兑换码act中引进hooks?

不知道诸位有没有运用class组件的阅历,属实是又源码精灵永久兑换码臭又长,繁多且命名复杂的生命周期给开发者带来的体验并不好索引符号。在这之前的function组件因为没有状况的概念,只能用来承载简略的UI,这明显不行,react的数据驱动意味着状况逻辑实践上是无处不在的。

依据官方文档的解说,引进hook处理了三个以及更多的问题

  • 在组件之间复用状况逻辑很难
  • 复杂组件变得难以了解
  • 难以了解的cla源码交易平台ss

实践体现上,我也无比认同引进hook的实践效果

  • hook的引进使咱们在无需修改组件结构的情况下即可复用状况逻辑,不管是在跨层级状况共享还是复杂逻辑抽象上都源码1688有了质的进步
  • 咱们在运用函数式组件时不再重视生命周期,只需确保hook在最顶层即可在函数中将和组件相关联的部分自由地拆分
  • hook 使你在非 class 的情况下能够运用更多的 React 特性

2.2 Fiber结构

我有一篇文章讲的是Fiber结构的完成https:///post/7030069221342052389,这索引的优缺点儿只给一下代码:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 实例变量,从字面意思也应该能够看出这儿保存了tag、key、type、state类似这样的有很强实践意义的特色
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
	...
}

除了Fiber结构,还需求着重的一点是,react16.8之后的Fiber架构:

  • Scapproachheduler(调度器),还没看到请疏忽,请记住这个概念
  • Reconciler(和谐器)
  • Renderer(烘appear托器)

3 hook是怎样赋予函数式组件状况的?

开端之前,贴一下我整个hook篇的调试代码

import {useState, useEffect, useRef} from 'react';
const useMockRef = init => {
  const [ref] = useState({current: init});
  return ref;
};
const CountButton = () => {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const realRef = useRef(0);
  const mockRef = useMockRef(0);
  const handleClick = () => {
    setCount(count + 1);
    mockRef.current += 1;
    realRef.current += 1;
  };
  const handleRefCountClick = () => {
    mockRef.current += 10;
    realRef.current += 10;
    console.log('mockRef.current', mockRef.current);
  };
  useEffect(() => {
    setTick(count + Math.random());
  }, [count]);
  return (
    <>
      <button onClick={handleClick}>{count}:Render by state</button>
      <button onClick={handleRefCountClick}>Click to add ref</button>
      <div style={{color: 'red'}}>{tick}</div>
      <div style={{color: 'yellow'}}>{mockRef?.current}</div>
      <div style={{color: 'green'}}>{realRef?.current}</div>
    </>
  );
};
export {CountButton};

【useState原理】源码调试吃透REACT-HOOKS(一)

进入到这儿,咱们需求进入到react源码的部分,这儿咱们需求重视的是FiberBeginWork

github.com/facebook/re…

调试代码

const CountButton = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <>
      <button onClick={handleClick}>{count}:Render by state</button>
    </>
  );
};

3.1 beginWork

在之前的文章中,咱们其完成已对Fiber有必定的了解,也知道appstore了在react中一个Fiber其实也便是对应一个虚拟DOM。那么咱们现在看到function beginWork #L3829

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    ...
}

能够发现beginWork的入参为「current、workInProgress、renderLanes」,前两者对应react架构中的两颗Fiber树renderLanes则是和优先级相关的参数,和S源码编辑器下载cheduler相关,这一appointment部分我还没细心研究暂时MARK。

更详细点说,在这儿函数的入参中:

  • current对应其时组件对应的Fiber节点上一次更新时的节点
  • workInProgress对应其时组件的对应的Fiber节点

其时屏幕上显示内容对应的上下文什么意思Fi数组排序ber树称为current Fiber树,正在approve内存中构建的Fiber树称为workInProgress Fiber树

之所以要有两根Fiber树是因为react运用了一种“双缓存机制数组c语言”,这种机数组去重制的意义是能够数组和链表的区别把其时页面下一帧放到内存中制作,在制作结束后直接用其时帧替换上一帧,省去两帧替换的计算时间(diff瓶颈/),减少白屏闪现的情况。这也是Fiappetiteber架源码网站构的重要作业原理

咱们现在研究的是函数式组件,那么在#L3942咱们能够看到,react依据其时Fiber节点的tag(即FunctionComponent)源码精灵永久兑换码 进行updateFunctionComponent的调用:

 switch (workInProgress.tag) {
   case xx:
     ...
   case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
 }

3.2 updateFuncti源码精灵永久兑换码onComponen源码精灵永久兑换码t

github.com/facebook/re…

咱们转到updateFunctionComponent,疏忽掉一些代上下文英语码,能够发现回来值workInProgress.child其实便是ne数组c语言xtChildren

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  ...
  if (__DEV__) {
    ... 
  } else {
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
    hasId = checkDidRenderIdHook();
  }
  ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
//https://github.com/facebook/react/blob/e225fa43ada4f4cf3d3ba4982cdd81bb093eaa46/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L299
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    // 假如这是一个尚未烘托的新组件,咱们不会经过运用最小的副作用来更新其子集。
    // 相反咱们将在烘托子目标之前将它们悉数添加到子目标。
    // 这意味着咱们能够经过不盯梢副作用来优化这个调理进程   
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 假如其时子项与正在进行的作业相同,则表明咱们还没有开端对这些子项进行任何研究。
    // 因而,咱们运用克隆算法,用于创立一切其时子项的副本。
    // 假如咱们现已有任何发展的作业,在这一点上是无效的,所以咱们把它抛出。
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

3.3 renderWithHooks‼️

g源码1688ith联系上下文ub.com/facebook/re…

明显,renderWitappointmenthHoo源码精灵永久兑换码ks这个函数的作用非常要害,这儿隐藏了hook作业原数组指针理的要害,

【useState原理】源码调试吃透REACT-HOOKS(一)

往下阅览会发现renderWithHooks做的第一件事便是把其时Fiber节点的数组去重「memoizedState、updateQueue、lanes」置空了。

那么这APP儿触及两个概念:源码之家

  • memoizAPPedState:
  • updaappreciateteQueue
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

为什么要这么做呢?看注释是

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;
  // didScheduleRenderPhaseUpdate = false;
  // localIdCounter = 0;
  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because memoizedState === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)
  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so memoizedState would be null during updates and mounts.

翻译过来便是,我的了解是这儿的作用是清除源码编辑器下载其时Fiber节点的遗留状况。

	// 以下内容应已重置
	// currentHook = null;
	// workInProgressHook = null;
	// didScheduleRenderPhaseUpdate = false;
	// localIdCounter = 0;
  // TODO 正告假如在挂载进程中根本没有运用钩子,那么在更新进程中就会运用一些钩子。
  // 现在,咱们将更新呈现标识为挂载,因为 memoizedState === null.
  // 这很棘手,因为它对某些类型的组件是有效的 (e.g. React.lazy)
  // 只有在至少运用一个有状况钩子的情况下,才运用memoizedState去区别挂载/更新
  // 非状况钩子(例如上下文)不会被添加到 memizedState,
  // 因而,在更新和挂载期间,memizedState 将为 null。

阅历了上边的过APP程,终于来到创立新的节点。

【useState原理】源码调试吃透REACT-HOOKS(一)

这儿的Component实践上便是咱们的组件函数

【useState原理】源码调试吃透REACT-HOOKS(一)

而这儿实践上的运行流程大抵如下:

  • createSignatureFunctionForTransform — 这儿实践上是react-refresh的热更新这块的东西,能够暂时不看

  • useState–>resolveDispatcher

3.3.1 组件挂载

HooksDispatcherOnMountInDEV["useState"]

【useState原理】源码调试吃透REACT-HOOKS(一)

​ 解析一下,这儿主要是两部分:

  • mountHookypesDev
  • moun索引失效tState

​ 一步步来:

a. mountHookypesDev
【useState原理】源码调试吃透REACT-HOOKS(一)

其实mountHookypesDev并不是一个很难了解的部分,可是为什么要拿出来说呢?

能够看到,该函数的作用很简略,取得appreciate其时hook的名apple字,塞入hookTypesDev或许创立hookTypesDev

【useState原理】源码调试吃透REACT-HOOKS(一)

那么记不记得上边抛出的一个问题:为什么hook必须在顶进程上下文层调用?

其实这儿appetite就给出了答案,或许说这便索引是原因之一,咱们能够看到图中另一个函数u执行上下文pdateHookTypesDev

【useState原理】源码调试吃透REACT-HOOKS(一)

能够看到哈,mountHookTypesDev往hookTypesDev中填入一切hookName之后,后续的update会按照索引递增的方法来获取函数名,此刻如hook调用顺序改变,取得的hookName就会存在问题,react也会在此抛出正告。

b. mountState

到达这儿,咱们能够看到mountState这个要害函数

【useState原理】源码调试吃透REACT-HOOKS(一)

mountState中又有另一个要害函数mountWorkInProgressHo上下文语境ok

function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

看到这儿能够知道,要害全approve局变量workInProgressHook的作用是记录每次生成的ho上下文字间距怎么调ok目标,用来指向组件中正在调用哪个hook。每一次调用hook函数都会把workInProgressHook指向hook函数发生的hook目标。

TIPS: curren数组去重t索引失效的几种情况lyRenderingFiber即为workInProgressFiber

【useState原理】源码调试吃透REACT-HOOKS(一)

那么初次挂载即会有这么条上下文语境链路:

workInProgressFiber->memoizedState = workInProgressHook = hook

之后再次挂载则会不断进行:

workInProgressHook = workInProgressHook.next = hook;

这也是咱们熟知的hook存储成单向链表保存的由来

【useState原理】源码调试吃透REACT-HOOKS(一)

而再往下走

【useState原理】源码调试吃透REACT-HOOKS(一)

其完成已很好了解了,react在hook上记录下baseState,memoizedState并初始化quene,然后回来咱们了解的[state,setState]


注意:关于quene的意义以上下文无关文法及和数组初始化dispatchSetState的内容咱源码中的图片们放在组索引图件更新的时分讲

【useState原理】源码调试吃透REACT-HOOKS(一)


3.3.2 组件更新

a. diappstorespatchSetState ‼️

组件更新的第一步,即调用上边的dispatchSetState

【useState原理】源码调试吃透REACT-HOOKS(一)

会发现,咱们运用的setState操作的实质是dispatchSe源码中的图片tState.bind(),那么咱们继续下钻

function dispatchSetState(fiber, queue, action) {
  ...
  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null
  };
	...
}

会发现在dispach的时分会呈现一个新的类型update,咱们翻到编译前的代码能够对这个索引失效结构更清晰。明显到这儿你知道了update又是一个链表

【useState原理】源码调试吃透REACT-HOOKS(一)

update并不是一个单纯索引页是哪一页的单向链表,为了体现这个规矩,我在调试代码中加入了新的两次setState咱们来看看

【useState原理】源码调试吃透REACT-HOOKS(一)

咱们看一下其间一个setCountupdate,加深一下形象,继续前进.数组词

【useState原理】源码调试吃透REACT-HOOKS(一)


function dispatchSetState(fiber, queue, action) {
  if (isRenderPhaseUpdate(fiber)) {
    ...
  } else {
    ...
    var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      var eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

往下咱们来到enqueueConcurrentHookUpdateapprove,这儿即触及了咱们一向搁置的queue与上边的update特殊结构,前两步疏忽,咱们需求继续下钻enqueueUpdate

【useState原理】源码调试吃透REACT-HOOKS(一)

可是咱们首要需求先知道这个函数的前两个参数此刻都指代什么,咱们此刻需求回头来到mou数组去重方法ntState,会发现Fiberappstore指的便是currentluRenderingFiber$1。同理其实queu上下文切换e在mount挂载在hooappetitek索引图之后,被经过bind的方法送入了dispatch。源码编辑器

【useState原理】源码调试吃透REACT-HOOKS(一)

不过有一点值得注意的是,经过.bind发生的闭包函数有个特APP色,Fiber源码交易平台向的仍然是其时传入的fiber,也便是说此刻的fib索引的优缺点er是咱们从前传入的workInProgressFiber

enqueueUpdate做了什么呢?这是一个要害过程,看图咱们会发索引符号现,enqueueUpdate用index继续自增的方法向concurrentQueues中分别添加「fiber、queue、update、lane」

【useState原理】源码调试吃透REACT-HOOKS(一)

所以concurrentQueues阅历咱们屡次setCount之后呈现图中的数据

【useState原理】源码调试吃透REACT-HOOKS(一)

【useState原理】源码调试吃透REACT-HOOKS(一)

接下来其实比较费事,有approve一些调度上的代码,为了易于了解,咱们找到使得concur数组指针rentQueuesIndex改变的代码处继续调试

【useState原理】源码调试吃透REACT-HOOKS(一)

咱们看看这个函数做了什索引超出了数组界限什么意思么:

【useState原理】源码调试吃透REACT-HOOKS(一)

诶,这不得懂了,这儿创立了一个环形链表,也便是说一切的更新,即update,会组合成一个单向环形链表上下文切换源码编辑器下载载在queue.pendinapplicationg上。

之所以单向环形是因为react的更新是有优先级的,update的履行顺序并不是固定的,经过单向链表更新可索引超出了数组界限什么意思能会数组去重方法导致第一个update丢掉。而环形链表一个明显的优势便索引失效的几种情况是能够从任何节点开端循环链表,由此确保了状况依靠的连续性。

OK,那么dispatchSetState的内容咱们能够咔了,往下继续前进。上下文

插一句application,为什么咱们setState的时分重复值不会引起重烘托,便是因为这个函数。

看到这个TODO了么费事亲把这个限制去了吧

【useState原理】源码调试吃透REACT-HOOKS(一)

而紧接着便是咱们比较了解的组件更新,组件更新部分细节上会比挂载更多,可是实践难度也不大

【useState原理】源码调试吃透REACT-HOOKS(一)

能够看到,这儿有两个要害函数「updateHookTypeDev」和「updateState」

b. updateHookTypeDev
其实前文现已提过这儿了,所以咱索引超出矩阵维度们越过一下
【useState原理】源码调试吃透REACT-HOOKS(一)
c. updappetiteateStat索引失效的几种情况e

首要updateState的操作其实依靠于updateReducer

【useState原理】源码调试吃透REACT-HOOKS(一)
【useState原理】源码调试吃透REACT-HOOKS(一)

而最终会走到一个咱们了解又生疏的要害函数u上下文图pdateWorkInProgressHook

  • updateWorkInProgressHo索引ok
【useState原理】源码调试吃透REACT-HOOKS(一)

咱们先来看第一部分

function updateWorkInProgressHook() {
  ...
  var nextCurrentHook;
  if (currentHook === null) {
    var current = currentlyRenderingFiber$1.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
	...
}

这儿触及一个地方currentlyRenderingFib上下文无关文法er$1.aappreciatelternate,「alternate」翻译过来即为「替补」。

那它是什么呢?咱们上边从数组的定义前讲过rappeareact的双缓存树架索引是什么意思构,这儿的alternate实践上就指向其时源码1688workInProgress节点对应的烘托在屏幕上的current数组指针节点。上下文无关文法

那么纵览这整个第一步,其实便是获取其时hook


第二部分较为简appear略,作用是获取其时待更新的hook

function updateWorkInProgressHook() {
  ...
  var nextWorkInProgressHook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
	...
}  

而最终一部分,知源码精灵永久兑换码道了上述一二部分变量意义之后相同很APP简单了解,无非react创立了一个新的hook然后用相同的方法做一个单向链表的更新。

【useState原理】源码调试吃透REACT-HOOKS(一)
  • updateReducer剩下的部分
【useState原理】源码调试吃透REACT-HOOKS(一)

剩下的部分触及到了不少咱们提过可是实践上并没有解说的变量:

  • currentHook,这个变量实践上和workPorgressHook对应,指的是current Fiber树上对应的其时hook
【useState原理】源码调试吃透REACT-HOOKS(一)
  • baseQueue,该变量取自currentHook,意义是本次更新之前剩余的待更新行列
  • queue更新行列,本次更源码编程器新增加的待更新行列,pending中存放着环形单项链表式的update

假如你在上边现已知道了queue->pending,那么实践上关于这儿进行hook的更新并不会难以了解。

这儿分appear红几个简略的过程:

  • 数组并更新行列

    【useState原理】源码调试吃透REACT-HOOKS(一)

  • 在update未清空之前do…while更新最新的状况

    【useState原理】源码调试吃透REACT-HOOKS(一)

    【useState原理】源码调试吃透REACT-HOOKS(一)

  • 符号索引失效完成更新

【useState原理】源码调试吃透REACT-HOOKS(一)

  • 更新hook的最新状况并回来

【useState原理】源码调试吃透REACT-HOOKS(一)

3.4 reconcileChildren*

那么此刻现已开端逐级回来,咱们来到updateFunctionComponent调用的最终一个方法reconcileChildren

而这儿咱们不深化看内部逻辑其实也是很简单了解这儿是为其时Fiber节点更新child的进程,因为具体的和谐器实践上便是咱们常说源码时代React diff这一部分,所以暂上下文英语时越过一下,mark今后说。

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  if (current === null) {
		//假如这是一个尚未烘托的新组件,咱们
		//不会经过运用最小的副作用来更新其子集。相反
		//咱们将在烘托子目标之前将它们悉数添加到子目标。这意味着
		//咱们能够经过不盯梢副作用来优化这个调理进程。
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
		//假如其时子项与正在进行的作业相同,则表明
		//咱们还没有开端对这些孩子进行任何研究。因而,咱们运用
		//克隆算法,用于创立一切其时子项的副本。
		//假如咱们现已有任何发展的作业,在这一点上是无效的,所以
		//咱们把它丢掉吧。
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

3.5 completeWork*

依据咱们关于react的烘托流程了解,render数组去重方法阶段分为beginWorkcompleteWor索引是什么意思k

【useState原理】源码调试吃透REACT-HOOKS(一)

而这儿紧接着触发的两个函数popTreeCapproveontextbubbleProperties,看上去都属于在完成r索引失效eact的render递归回上下文图来的作业。

因为具体作业原理中实质数组初始化上和咱们想说的h索引的作用ook现已没有多少联系,咱们暂时疏忽即可。

4 总上下文无关文法

React源码的阅览之索引的作用路必定是绵联系上下文长的,定一些TODO:

  • useEffect的作业原理

  • react的ren源码精灵永久兑换码der阶段作业原理

  • react的commit阶段作业原理

那个人源码中的图片再不曾呈现,他只数组去重方法跟我说继续前进