React Hook源码解析(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: React Hook源码解析(二)

640.jpg

写在前面


这是React Hook源码解析的第二篇。第一篇在这里:


React Hook源码解析(一)


在上一篇文章中,主要分析了Hook在React中是如何保存的,以及Hook的更新过程。本文中,我们将通过下面两个问题,继续深入研究Hook,以弥补上文中略过的一些细节。

1、如果我连续多次调用setState,Hook会怎么处理呢?

2、Hook的useEffect 是如何工作的?


连续多次setState


先看示例代码:


const App = () => {
    const [count, setCount] = useState(0);
    const handleClick = () => {        setCount(count + 1);        setCount(count + 2);        setCount(count + 3);    };
    return <React.Fragment>        <span style={{ marginRight: '10px' }}>{count}</span>        <button onClick={handleClick}>点击</button>    </React.Fragment>};


我们点击一次button,最终页面上会输出多少呢?熟悉React的朋友们,很快就会得到答案:3

在上一篇源码解析中,这部分内容被忽略了。本文我们来看看这里的内部逻辑。

首先,先复习一下hook的结构:


var hook = {    memoizedState: null, // 当前的state值    baseState: null,     queue: null, // 存储更新信息    baseUpdate: null,    next: null // 指向下一个hook对象的指针  };


我们先看一下组件挂载完成后的hook,注意queue字段的值:


640.png


之前讲过,调用setCount的时候,实际上调用的是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)这个函数。这个函数通过闭包保存了对应的Fiber和hook对象的queue的引用。


首次setCount的时候,hook对queue的处理如下:


// 更新信息var _update2 = {    expirationTime: expirationTime, // Fiber调度相关    suspenseConfig: suspenseConfig,    action: action, // setCount函数接受的参数    eagerReducer: null,    eagerState: null,    next: null};
var last = queue.last;
// 首次更新时if (last === null) {  // This is the first update. Create a circular list.  // 第一次更新,构建一个 环  _update2.next = _update2;} else {  // 后续更新  var first = last.next;
  if (first !== null) {    // Still circular.    _update2.next = first;  }  last.next = _update2;}// queue的最近一次更新指向_update2queue.last = _update2;


第一次更新后,会构造一个环形结构:


640.png


第二次、第三次setCount时,会继续构造queue链:


var first = last.next;
if (first !== null) {  // Still circular.  _update2.next = first;}last.next = _update2;


最终会形成下图的结构:

640.png

组件重新渲染时,react会从hook的queue链中,找到最新的值,赋值给hook的memoizedState,我们就可以拿到最新的state了:


// 代码有省略
  ...  // 循环,直到拿到queue链上最新的值  do {    var updateExpirationTime = _update.expirationTime;
    if (updateExpirationTime < renderExpirationTime$1) {      ...    } else {      ...      if (_update.eagerReducer === reducer) {        ...      } else {        var _action = _update.action;        _newState = reducer(_newState, _action);      }    }    prevUpdate = _update;    _update = _update.next;  } while (_update !== null && _update !== first);        hook.memoizedState = _newState; // 最新的state值,本例中为3  hook.baseUpdate = newBaseUpdate; // 最新的基础更新信息,action=3  hook.baseState = newBaseState; // 最新的基础state值,本例中为3  queue.lastRenderedState = _newState; // 最近渲染的state值,本例中为3  return [hook.memoizedState, dispatch];


这里有一个注意事项,在上一篇文章中,我们提到过,setState中是支持传入函数的。假设我们在setState中传入的参数是一个函数,在本例中,如果我们点击按钮后的代码改成:


const handleClick = () => {    setCount(count => count + 1);    setCount(count => count + 2);    setCount(count => count + 3); };


最终的count值就不是3了,而是6。这是因为每次传入reducer的是最新的state:


... do {  var action = update.action; // 这里的action是我们传入的回调函数  newState = reducer(newState, action); // newState 是最新的 state  update = update.next; // 取hook对象queue链上的下一次更新} while (update !== null); ...


useEffect是如何工作的


首先上示例代码:


const fakeReq = function(input) {    return new Promise( resolve => {        setTimeout(() => {            resolve(`${input} - ${Date.now()}`);        }, 500);    });}
const App = () => {    const [input, setInput] = useState('');    const [res, setRes] = useState('');
    useEffect(() => {        fakeReq(input).then(res => {            setRes(res);        });    },[input]);
    return <React.Fragment>        <input value={input} onChange={e => setInput(e.target.value)} />        <div>            返回结果为:<span>{res}</span>        </div>    </React.Fragment>};


上面的代码中,我们在输入框进入输入的同时,会发起一个请求,并且将返回的结果显示在页面上。首先,我们来看看React是怎么保存useEffect的。


在代码中,调用useEffect后,同样会生成一个hook对象,只是这个hook对象的memoizedState字段不太一样:


...// fiberEffectTag 和 hookEffectTag 是两个标识// create、deps是我们传入useEffect的两个参数function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {  var hook = mountWorkInProgressHook();  var nextDeps = deps === undefined ? null : deps;  sideEffectTag |= fiberEffectTag;  // useEffect生成的hook对象的memoizedState是一个特殊的对象  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);}...


我们来看看pushEffect干了什么:


function pushEffect(tag, create, destroy, deps) {    // effect对象  var effect = {    tag: tag,    create: create,    destroy: destroy,    deps: deps,    // Circular    next: null  };
  // componentUpdateQueue是一个全局变量,用来保存组件的最新的副作用  if (componentUpdateQueue === null) {    componentUpdateQueue = createFunctionComponentUpdateQueue();    componentUpdateQueue.lastEffect = effect.next = effect;  } else {    var lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {      componentUpdateQueue.lastEffect = effect.next = effect;    } else {      var firstEffect = lastEffect.next;      lastEffect.next = effect;      effect.next = firstEffect;      componentUpdateQueue.lastEffect = effect;    }  }
  return effect;}


构造一个带环的链:

640.png


在本例中,初始化完成后,最终Fiber对象的hook链为:

640.png

当我们在输入框进行输入时,来看看useEffect是如何起作用的。

输入时,会触发组件的重新渲染,假设我们输入了3,此时传入useEffect的依赖变成了:


useEffect(() => {    fakeReq(input).then(res => {        setRes(res);    });},['3']);


useEffect的更新代码为:


function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {  // 当前处理的hook  var hook = updateWorkInProgressHook();  // 最新传入的依赖  var nextDeps = deps === undefined ? null : deps;  var destroy = undefined;
  if (currentHook !== null) {    // 上一次的effect    var prevEffect = currentHook.memoizedState;    destroy = prevEffect.destroy;
    if (nextDeps !== null) {      var prevDeps = prevEffect.deps;      // 对比两次依赖是否相同。如果相同,则在componentUpdateQueue上增加一个 tag = NoEffect$1 的 effect。这里的 NoEffect$1 是一个常量, 值为 0。这里很重要      if (areHookInputsEqual(nextDeps, prevDeps)) {        pushEffect(NoEffect$1, create, destroy, nextDeps);        return;      }    }  }    sideEffectTag |= fiberEffectTag;  // 如果两次依赖不同,在 componentUpdateQueue 上增加一个 effect,并且更新hook的memorizedState  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);}


经过React的调度,会在 commitHookEffectList 这个函数中,判断是否需要执行 useEffect 中传入的函数:


function commitHookEffectList(unmountTag, mountTag, finishedWork) {
  var updateQueue = finishedWork.updateQueue;  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {    var firstEffect = lastEffect.next;    var effect = firstEffect;
    do {      if ((effect.tag & unmountTag) !== NoEffect$1) {        // Unmount        var destroy = effect.destroy;        effect.destroy = undefined;
        if (destroy !== undefined) {          destroy();        }      }            if ((effect.tag & mountTag) !== NoEffect$1) {        // Mount        var create = effect.create;        effect.destroy = create();
        {          var _destroy = effect.destroy;
          if (_destroy !== undefined && typeof _destroy !== 'function') {            var addendum = void 0;
            if (_destroy === null) {              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';            } else if (typeof _destroy.then === 'function') {              ...            } else {              addendum = ' You returned: ' + _destroy;            }            ...          }        }      }      effect = effect.next;    } while (effect !== firstEffect);  }}


NoEffect$1是一个等于0的全局常量,从上面代码的do...while...部分可以看到,当一个 effect 的 tag 为 0时,和任何变量做与运算,值都为0,不会进行任何操作。而上面的分析也提到了,useEffect的dep没有变时,会声明一个 tag = NoEffect$1 的effect。因此,useEffect的dep没有变化时,useEffect的函数不会被执行。


我们再来看看,react是怎么比较两次的deps是否相同的:


// 判断useEffect中传入的Deps是否相同function areHookInputsEqual(nextDeps, prevDeps) {   ...  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {    // 这里的 is$1 ,就是 Object.is 这个方法    if (is$1(nextDeps[i], prevDeps[i])) {      continue;    }    return false;  }  return true;}


总结


回到本文开头的两个问题:

1、如果我连续多次调用setState,Hook会怎么处理呢?

2、Hook的useEffect 是如何工作的?

对于每一个hook,react会在hook对象的queue字段上,以有环链的形式,存储更新信息。连续多次更新,会沿着queue链计算出最新该hook最新的值。

使用useEffect,也会生成一个hook对象。只是该hook对象与useState生成的hook对象有区别。组件重新渲染时,会判断传入useEffect的dep依赖是否与上一次相同,相同的话,则会为此次更新打上特殊的tag,保证不会执行useEffect中传入的函数。


输写在后面


本文在前一篇文章的基础上,进一步分析了hook中state的更新机制。另外,大致分析了useEffect是如何存储,如何工作的。由于本文不涉及react的调度更新过程,看起来不太连贯,请多包涵。关于react hook的更多解析,请关注我后续的文章。


相关文章
|
4天前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
17 4
|
7天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
10天前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
55 2
|
11天前
|
JavaScript 前端开发 算法
React 虚拟 DOM 深度解析
【10月更文挑战第5天】本文深入解析了 React 虚拟 DOM 的工作原理,包括其基础概念、优点与缺点,以及 Diff 算法的关键点。同时,分享了常见问题及解决方法,并介绍了作者在代码/项目上的成就和经验,如大型电商平台的前端重构和开源贡献。
25 3
|
16天前
|
移动开发 JSON 数据可视化
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
36 0
|
2月前
|
前端开发 Java UED
JSF 面向组件开发究竟藏着何种奥秘?带你探寻可复用 UI 组件设计的神秘之路
【8月更文挑战第31天】在现代软件开发中,高效与可维护性至关重要。JavaServer Faces(JSF)框架通过其面向组件的开发模式,提供了构建复杂用户界面的强大工具,特别适用于设计可复用的 UI 组件。通过合理设计组件的功能与外观,可以显著提高开发效率并降低维护成本。本文以一个具体的 `MessageComponent` 示例展示了如何创建可复用的 JSF 组件,并介绍了如何在 JSF 页面中使用这些组件。结合其他技术如 PrimeFaces 和 Bootstrap,可以进一步丰富组件库,提升用户体验。
48 0
|
2月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
31 0
|
2月前
|
开发者 Java
JSF EL 表达式:乘技术潮流之风,筑简洁开发之梦,触动开发者心弦的强大语言
【8月更文挑战第31天】JavaServer Faces (JSF) 的表达式语言 (EL) 是一种强大的工具,允许开发者在 JSF 页面和后台 bean 间进行简洁高效的数据绑定。本文介绍了 JSF EL 的基本概念及使用技巧,包括访问 bean 属性和方法、数据绑定、内置对象使用、条件判断和循环等,并分享了最佳实践建议,帮助提升开发效率和代码质量。
32 0
|
前端开发 JavaScript
常用react hook分析
useMemo、useCallBack、useState、useRef、React.memo()的使用
常用react hook分析
|
5月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
337 0

推荐镜像

更多