为了账号安全,请及时绑定邮箱和手机立即绑定

React Hook源码解析(二)

ID: 符合预期的CoyPan
CoyPan,BAT某厂符合预期的FE,正努力成为一名出色的工程师

写在前面

在上一篇文章中,主要分析了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字段的值:

图片描述

之前讲过,调用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的最近一次更新指向_update2
queue.last = _update2;

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

图片描述

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

var first = last.next;

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

最终会形成下图的结构:

图片描述

组件重新渲染时,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;
}

构造一个带环的链:

图片描述

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

图片描述

当我们在输入框进行输入时,来看看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的更多解析,请关注我后续的文章。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
12
获赞与收藏
119

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消