翼度科技»论坛 编程开发 JavaScript 查看内容

React深入分析useEffect源码

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
热身准备

这里不再讲
  1. useLayoutEffect
复制代码
,它和
  1. useEffect
复制代码
的代码是一样的,区别主要是:

  • 执行时机不同;
    1. useEffect
    复制代码
    是异步,
    1. useLayoutEffect
    复制代码
    是同步,会阻塞渲染;

初始化 mount

mountEffect
在所有
  1. hook
复制代码
初始化时都会通过下面这行代码实现
  1. hook
复制代码
结构的初始化和存储,这里不再讲
  1. mountWorkInProgressHook
复制代码
方法
  1. var hook = mountWorkInProgressHook();
复制代码
  1. mountEffect
复制代码
方法中,只有这几行代码。先来解读下几个参数:

  • fiberFlags:有副作用的更新标记,用来标记hook所在的
    1. fiber
    复制代码

  • hookFlags:副作用标记;
  • create:使用者传入的回调函数;
  • deps:使用者传入的数组依赖;
  1. function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  2.   // hook初始化
  3.   var hook = mountWorkInProgressHook();
  4.   // 判断是否有传入deps,如果有会作为下次更新的deps
  5.   var nextDeps = deps === undefined ? null : deps;
  6.   // 给hook所在的fiber打上有副作用的更新的标记
  7.   currentlyRenderingFiber$1.flags |= fiberFlags;
  8.   // 将副作用操作存放到fiber.memoizedState.hook.memoizedState中
  9.   hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
  10. }
复制代码
上面代码中都有注释,接下来我们看看
  1. React
复制代码
是如何存放副作用更新操作的,主要就是
  1. pushEffect
复制代码
方法
  1. function pushEffect(tag, create, destroy, deps) {
  2.   // 初始化副作用结构,
  3.   var effect = {
  4.     tag: tag,
  5.     create: create,   // 回调函数
  6.     destroy: destroy,  // 回调函数里的return(mount时是undefined)
  7.     deps: deps,    // 依赖数组
  8.     // 闭环链表
  9.     next: null
  10.   };
  11.   // 下面的一大段代码看着复杂,但是有没有很熟悉的感觉?
  12.   var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
  13.   if (componentUpdateQueue === null) {
  14.     componentUpdateQueue = createFunctionComponentUpdateQueue();
  15.     currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
  16.     // effect.next = effect形成环形链表
  17.     componentUpdateQueue.lastEffect = effect.next = effect;   
  18.   } else {
  19.     var lastEffect = componentUpdateQueue.lastEffect;
  20.     if (lastEffect === null) {
  21.       componentUpdateQueue.lastEffect = effect.next = effect;
  22.     } else {
  23.       var firstEffect = lastEffect.next;
  24.       lastEffect.next = effect;
  25.       effect.next = firstEffect;
  26.       componentUpdateQueue.lastEffect = effect;
  27.     }
  28.   }
  29.   return effect;
  30. }
复制代码
上面这段代码除了初始化副作用的结构代码外,都是我们前面讲过的操作闭环链表,向链表末尾添加新的
  1. effect
复制代码
,该
  1. effect.next
复制代码
指向
  1. fisrtEffect
复制代码
,并且链表当前的指针指向最新添加的
  1. effect
复制代码
  1. useEffect
复制代码
的初始化就这么简单,简单总结一下:给
  1. hook
复制代码
所在的
  1. fiber
复制代码
打上副作用更新标记,并且
  1. fiber.memoizedState.hook.memoizedState
复制代码
  1. fiber.updateQueue
复制代码
存储了相关的副作用,这些副作用通过闭环链表的结构存储。
相关参考视频讲解:传送门

更新 update


updateEffect
  1. updateWorkInProgressHook
复制代码
在上篇文章也已讲过,不再详述,主要功能就是创建一个带有回调函数的
  1. newHook
复制代码
去覆盖之前的
  1. hook
复制代码
  1. function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  2.   var hook = updateWorkInProgressHook();
  3.   var nextDeps = deps === undefined ? null : deps;
  4.   var destroy = undefined;
  5.   if (currentHook !== null) {
  6.     var prevEffect = currentHook.memoizedState;
  7.     destroy = prevEffect.destroy;
  8.     if (nextDeps !== null) {
  9.       var prevDeps = prevEffect.deps;
  10.       // 比较两次依赖数组中的值是否有变化
  11.       if (areHookInputsEqual(nextDeps, prevDeps)) {
  12.         // 和之前初始化时一样
  13.         pushEffect(hookFlags, create, destroy, nextDeps);
  14.         return;
  15.       }
  16.     }
  17.   }
  18.   // 和之前初始化时一样
  19.   currentlyRenderingFiber$1.flags |= fiberFlags;
  20.   hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
  21. }
复制代码
相信眼眼尖的看官已经注意到上面代码中有两个
  1. pushEffect
复制代码
,一个没有赋值给
  1. hook.memoizedState
复制代码
,一个赋值了,这两者有什么区别呢?
先保留着这个疑问,先来了解下下面这行代码都做了些什么,因为它造就了两个
  1. pushEffect
复制代码
  1. if (areHookInputsEqual(nextDeps, prevDeps)){...}
复制代码
  1. function areHookInputsEqual(nextDeps, prevDeps) {
  2.   // 没有传deps的情况返回false
  3.   if (prevDeps === null) {
  4.     return false;
  5.   }
  6.   // deps不是[],且其中的值有变动才会返回false
  7.   for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
  8.     if (objectIs(nextDeps[i], prevDeps[i])) {
  9.       continue;
  10.     }
  11.     return false;
  12.   }
  13.   // deps = [],或者deps里面的值没有变化会返回true
  14.   return true;
  15. }
复制代码
它会判断两次依赖数组中的值是否有变化以及
  1. deps
复制代码
是否是空数组来决定返回
  1. true
复制代码
  1. false
复制代码
,返回
  1. true
复制代码
表明这次不需要调用回调函数。
现在我们明白了两次
  1. pushEffect
复制代码
的异同,
  1. if
复制代码
内部的
  1. pushEffect
复制代码
是不需要调用的回调函数, 外面的
  1. pushEffect
复制代码
是需要调用的。再来仔细看下这两行代码:
  1. // if内部的,第一个参数是hookFlags = 4
  2. pushEffect(hookFlags, create, destroy, nextDeps);
  3. // if外部的,第一个参数是HasEffect | hookFlags = 5
  4. hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
复制代码
这两行代码的区别是传入的第一个参数不同,而第一个参数就是
  1. effect.tag
复制代码
的值,
  1. effect.tag = 4
复制代码
不会添加到副作用执行队列,而
  1. effect.tag = 5
复制代码
可以。没有添加到副作用执行队列的
  1. effect
复制代码
就不会执行。这样就巧妙的实现了
  1. useEffect
复制代码
基于
  1. deps
复制代码
来判断是否需要执行回调函数。
到这里, 我们搞明白了,不管
  1. useEffect
复制代码
里的
  1. deps
复制代码
有没有变化都会为回调函数创建
  1. effect
复制代码
并添加到
  1. effect
复制代码
链表和
  1. fiber.updateQueue
复制代码
中,但是
  1. React
复制代码
会根据
  1. effect.tag
复制代码
来决定该
  1. effect
复制代码
是否要添加到副作用执行队列中去执行。

执行副作用

我们现在知道了,
  1. useEffect
复制代码
是异步执行的。那么这个回调函数副作用会在什么时候执行呢?
  1. useEffect
复制代码
回调函数会在
  1. layout
复制代码
阶段之后执行。现在我们来了解下具体调用执行的流程。

我画了一个简单的流程图,大致描述了下调用流程。首先在
  1. mutation
复制代码
之前阶段,基于副作用创建任务并放到
  1. taskQueue
复制代码
中,同时会执行
  1. requestHostCallback
复制代码
,这个方法就涉及到了异步了,它首先考虑使用
  1. MessageChannel
复制代码
实现异步,其次会考虑使用
  1. setTimeout
复制代码
实现。使用
  1. MessageChannel
复制代码
时,
  1. requestHostCallback
复制代码
会马上执行
  1. port.postMessage(null);
复制代码
,这样就可以在异步的第一时间执行
  1. workLoop
复制代码
  1. workLoop
复制代码
会遍历
  1. taskQueue
复制代码
,执行任务,如果是
  1. useEffect
复制代码
  1. effect
复制代码
任务,会调用
  1. flusnPassiveEffects
复制代码

Q:可能有人会疑惑为什么优先考虑
  1. MessageChannel
复制代码

A: 首先我们要明白
  1. React
复制代码
调度更新的目的是为了时间分片,意思是每隔一段时间就把主线程还给浏览器,避免长时间占用主线程导致页面卡顿。使用
  1. MessageChannel
复制代码
  1. SetTimeout
复制代码
的目的都是为了创建宏任务,因为宏任务会在当前微任务都执行完后,等到浏览器主线程空闲后才会执行。不优先考虑
  1. setTimeout
复制代码
的原因是,
  1. setTimeout
复制代码
执行时间不准确,会造成时间浪费,即使是
  1. setTimeout(fn, 0)
复制代码
,感兴趣的可以去自己了解下,本文不做赘述了。
  1. schedulePassiveEffects
复制代码
中,会决定是否执行
  1. effect
复制代码
链表中的
  1. effect
复制代码
,判断的依据就是每个
  1. effect
复制代码
上的
  1. effect.tag
复制代码
:
  1. function schedulePassiveEffects(finishedWork) {
  2.   var updateQueue = finishedWork.updateQueue;
  3.   var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  4.   if (lastEffect !== null) {
  5.     var firstEffect = lastEffect.next;
  6.     var effect = firstEffect;
  7.     // 遍历effect链表
  8.     do {
  9.       var _effect = effect,
  10.           next = _effect.next,
  11.           tag = _effect.tag;
  12.       // 基于effect.tag决定是否添加到副作用执行队列
  13.       if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
  14.         enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
  15.         enqueuePendingPassiveHookEffectMount(finishedWork, effect);
  16.       }
  17.       effect = next;
  18.     } while (effect !== firstEffect);
  19.   }
  20. }
复制代码
  1. flushPassiveEffects
复制代码
中,会先执行上次更新动作的销毁函数,然后再执行本次更新动作的回调函数,并且会把回调函数的
  1. return
复制代码
作为下次更新动作的销毁函数。
  1. function flushPassiveEffectsImpl() {
  2.   // 执行上次更新动作的销毁函数
  3.   var unmountEffects = pendingPassiveHookEffectsUnmount;
  4.   pendingPassiveHookEffectsUnmount = [];
  5.   for (var i = 0; i < unmountEffects.length; i += 2) {
  6.     ...destroy()
  7.   }
  8.   // 执行本次更新动作的回调函数
  9.   var mountEffects = pendingPassiveHookEffectsMount;
  10.   pendingPassiveHookEffectsMount = [];
  11.   for (var _i = 0; _i < mountEffects.length; _i += 2) {
  12.     ...create()
  13.   }
  14. }
复制代码
上面代码中的这两行就是来自副作用执行队列,已经过滤掉了不需要执行的
  1. effect
复制代码
,只执行该队列上的副作用函数
  1. var unmountEffects = pendingPassiveHookEffectsUnmount;
  2. var mountEffects = pendingPassiveHookEffectsMount;
复制代码
总结

看完这篇文章, 我们可以弄明白下面这几个问题:

    1. useEffect
    复制代码
    1. useLayoutEffect
    复制代码
    的区别?
    1. useEffect
    复制代码
    是怎么判断回调函数是否需要执行的?
    1. useEffect
    复制代码
    是同步还是异步?
    1. useEffect
    复制代码
    是通过什么实现异步的?
    1. useEffect
    复制代码
    为什么要要优先选用
    1. MessageChannel
    复制代码
    实现异步?
到此这篇关于React深入分析useEffect源码的文章就介绍到这了,更多相关React useEffect内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/article/266738.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具