useEffect中不能使用async原理详解

目录

当我们尝试在 useEffect 使用 async 的时候会报错,但是一直没有了解为什么,最近在看源码,尝试从源码角度解释报错的原因,下面是具体的代码分析。

当页面中使用 useEffect 的时候,会在初始化的时候执行 mountEffect 如下:

useEffect: function(create, deps) {
  currentHookNameInDev = "useEffect";
  mountHookTypesDev();
  checkDepsAreArrayDev(deps);
  return mountEffect(create, deps);
},

执行 mountEffect 的时候执行 mountEffectImpl 如下:

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === void 0 ? null : deps;
  currentlyRenderingFiber$1.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HasEffect | hookFlags, create, void 0, nextDeps);
}

之后执行 pushEffect,在 pushEffect 中会创建一个 effect 节点,然后添加到当前函数对应 fiber 的 updateQueue 上面,数据结构是一个环链。

function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag,
    create,
    destroy,
    deps,
    next: null
  };

  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    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;
}

中间又是一大堆调度,协调的逻辑,不是我们关注的重点,这里省略掉直接进入到 schedulePassiveEffects,这个函数作用是从函数组件对应的 fiber 上获取上面挂载的 effect,然后将 effect 和 fiber 堆到 pendingPassiveHookEffectsUnmount 和 pendingPassiveHookEffectsMount 这个两个队列中

function schedulePassiveEffects(finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;
    do {
      var _effect = effect
      , next = _effect.next
      , tag = _effect.tag;
      if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
        //
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

这里是推入的逻辑,只展示推入挂载队列的方法,推入卸载队列是一样的

function enqueuePendingPassiveHookEffectMount(fiber, effect) {
  pendingPassiveHookEffectsMount.push(effect, fiber);
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalPriority$1, function() {
      flushPassiveEffects();
      return null;
    });
  }
}

之后又是一大推调度,协调的逻辑,等待协调执行完毕后,之后会进入 flushPassiveEffectsImpl ,函数太长了,只贴出相关的部分,逻辑是循环挂载 effect 队列中的每一个 effect 传入到 invokePassiveEffectCreate 执行

// ...
var mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (var _i = 0; _i < mountEffects.length; _i += 2) {
  var _effect2 = mountEffects[_i];
  var _fiber = mountEffects[_i + 1];
  {
    setCurrentFiber(_fiber);
    {
      invokeGuardedCallback(null, invokePassiveEffectCreate, null, _effect2);
    }
    if (hasCaughtError()) {
      if (!(_fiber !== null)) {
        {
          throw Error("Should be working on an effect.");
        }
      }
      var _error4 = clearCaughtError();
      captureCommitPhaseError(_fiber, _error4);
    }
    resetCurrentFiber();
  }
}
// ...

这个函数会获取 create 并执行,然后将执行结果挂载到 destroy 上,这里的 create 就是 useEffect 中的第一个参数,从这里可以看出,如果有返回值,那么 destroy 就是第一个函数的返回值,没有就是 undefined

function invokePassiveEffectCreate(effect) {
  var create = effect.create;
  effect.destroy = create();
}

卸载的时候会通过函数组件对应的 fiber 获取 effect 链表,然后遍历链表,获取环链上的每一个节点,如果 destroy 不是 undefined 就执行,所以如果 useEffect 第一个参数传入 async, 那么这里的 destroy 就是一个 promise 对象,对象是不能执行的,所以报错。

function commitHookEffectListUnmount(tag, 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 & tag) === tag) {
        // Unmount
        var destroy = effect.destroy;
        effect.destroy = undefined;

        if (destroy !== undefined) {
          destroy();
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

既然知道了原因那么,解决方案就非常简单,直接手写一个自定义 hook,包裹一下就可以处理这个问题了,hook 实现如下。

import { useEffect } from 'react'

export default function useAsyncEffect<T, U extends any[]>(
  method: () => Promise<T>,
  deps: U
) {
  useEffect(() => {
    (async () => {
      await method()
    })()
  }, deps)
}

使用:

import React, { useState } from 'react'
import { useAsyncEffect } from './useAsyncEffect'

export default function Demo() {
  const [count, setCount] = useState(0)

  function fetchData(): Promise<number> {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(count + 1)
      }, 2000)
    })
  }
  useAsyncEffect(async () => {
    const count = await fetchData()
    setCount(count)
  }, [fetchData])

  return (
    <div>{count}</div>
  )
}

这里其实有问题,因为返回值永远是undefined,你可以开动脑筋尝试修复一下。

以上就是对 useEffect 中为啥不能使用 async 的简单分析,有不足之处,欢迎提出,更多关于useEffect不能使用async的资料请关注我们其它相关文章!

(0)

相关推荐

  • React中useEffect 与 useLayoutEffect的区别

    目录 前置知识 useEffect commitBeforeMutationEffects commitMutationEffects commitLayoutEffects 后续阶段 useLayoutEffect 结论 前置知识 我们可以将 React 的工作流程划分为几大块: render 阶段:主要生成 Fiber节点 并构建出完整的 Fiber树 commit 阶段:在上一个render 阶段中会在 rootFiber 上生成一条副作用链表,应用的DOM操作就会在本阶段执行 commi

  • React useEffect不支持async function示例分析

    目录 引言 React为什么这么设计呢? 简单改造 1.简单改造的写法(不推荐) 2.把异步提取成单独函数或自定义hook(推荐) 引言 useEffect相比大家都耳熟能详啦,如下这种写法,应该是非常常见的需求. useEffect(async () => { await getPoiInfo(); // 请求数据 }, []); 但是 React 本身并不支持这么做,理由是 effect function 应该返回一个销毁函数(effect:是指return返回的cleanup函数),如果

  • useEffect支持async及await使用方式

    目录 引言 背景 React 为什么要这么做? useEffect 怎么支持 async...await... 自定义 hooks 还可以支持 useEffect 的清除机制么? 总结与思考 引言 本文是深入浅出 ahooks 源码系列文章的第六篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 注:本系列对 ahooks 的源码

  • React-hooks中的useEffect使用步骤

    目录 1.理解函数副作用 什么是副作用? 常见的副作用 2.基础使用 使用步骤 示例代码 3.依赖项控制 useEffect 永远是在 DOM渲染完成之后执行 1.理解函数副作用 什么是副作用? 对于React组件来说,主作用是根据数据(state/props)渲染UI,除此之外都是副作用(比如手动修改DOM.发送ajax请求). 常见的副作用 数据请求(发送ajax) 手动修改 DOM localstorage操作 useEffect 函数的作用就是为react函数组件提供副作用 2.基础使用

  • 浅谈react useEffect闭包的坑

    问题代码 看一段因为useEffect导致的闭包问题代码 const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('clic

  • useEffect中不能使用async原理详解

    目录 当我们尝试在 useEffect 使用 async 的时候会报错,但是一直没有了解为什么,最近在看源码,尝试从源码角度解释报错的原因,下面是具体的代码分析. 当页面中使用 useEffect 的时候,会在初始化的时候执行 mountEffect 如下: useEffect: function(create, deps) { currentHookNameInDev = "useEffect"; mountHookTypesDev(); checkDepsAreArrayDev(d

  • Node.Js中实现端口重用原理详解

    本文介绍了Node.Js中实现端口重用原理详解,分享给大家,具体如下: 起源,从官方实例中看多进程共用端口 const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); for (let i =

  • Android中的LeakCanary的原理详解

    场景:最新的leakCanary2.8.1: debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' 原理:首先就是我们在引入最新的依赖包,什么都不用干了,因为他的初始化在清单文件中注册了contentProvider(),把初始化放到了这里面的onCreate()去初始化了,在初始化的过程中,他会用application监听观察对象activity.fragment等对象的生命周期的变化,当执行销毁的生命周期

  • Java中的 HTTP 协议原理详解

    目录 前言 1.HTTP 特点 2.HTTP 组成 2.1 请求对象 2.1.1 请求行 2.1.2 请求报头 2.1.3 空行 2.1.4 请求正文 2.2 响应对象 2.2.1 状态行 2.2.2 响应报头 2.2.3 空行 2.2.4 响应正文 总结 前言 HTTP(Hyper Text Transfer Protocol)超文本传输协议,下文简称 HTTP,它的作用是用于实现服务器端和客户端的数据传输的.它可以传输任意的数据类型,如文本.HTML.图片.文件.声音等类型. 简单来说,HT

  • php中的ini配置原理详解

    使用php的同学都知道php.ini配置的生效会贯穿整个SAPI的生命周期.在一段php脚本的执行过程中,如果手动修改ini配置,是不会启作用的.此时如果无法重启apache或者nginx等,那么就只能显式的在php代码中调用ini_set接口.ini_set是php向我们提供的一个动态修改配置的函数,需要注意的是,利用ini_set所设置的配置与ini文件中设置的配置,其生效的时间范围并不相同.在php脚本执行结束之后,ini_set的设置便会随即失效. 因此本文打算分两篇,第一篇阐述php.

  • Java中ArrayList的工作原理详解

    1.ArrayList 以数组实现.节约空间,但数组有容量限制.超出限制时会增加50%容量,用System.arraycopy()复制到新的数组.因此最好能给出数组大小的预估值.默认第一次插入元素时创建大小为10的数组.按数组下标访问元素-get(i).set(i,e)的性能很高,这是数组的基本优势.如果按下标插入元素.删除元素-add(i,e).remove(i).remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了.越是前面的元素,修改时要移

  • Python中的延迟绑定原理详解

    直接看下面例子 my_ld = [lambda x:x*i for i in range(3)] my_list = [ld(2) for ld in my_ld] print(my_list) 本想是想通过以上代码,输出[0, 2, 4]的,但结果却是[4, 4, 4] 下面说下本人对这个结果的理解: 因为Python解释器,遇到lambda(或者def),只是定义了一个匿名函数对象,并保存在内存中,只有等到调用这个匿名函数的时候,才会执行函数内部的代码(x*i).所以匿名函数中的i并不是立即

  • Java中synchronized实现原理详解

    记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字"同步",也成为了我们解决多线程情况的百试不爽的良药.但是,随着我们学习的进行我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它. 诚然,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么

  • C++ 中的虚函数表及虚函数执行原理详解

    为了实现虚函数,C++ 使用了虚函数表来达到延迟绑定的目的.虚函数表在动态/延迟绑定行为中用于查询调用的函数. 尽管要描述清楚虚函数表的机制会多费点口舌,但其实其本身还是比较简单的. 首先,每个包含虚函数的类(或者继承自的类包含了虚函数)都有一个自己的虚函数表.这个表是一个在编译时确定的静态数组.虚函数表包含了指向每个虚函数的函数指针以供类对象调用. 其次,编译器还在基类中定义了一个隐藏指针,我们称为 *__vptr,*__vptr 是在类实例创建时自动设置的,以指向类的虚函数表.*__vptr

  • Android中Lifecycle的原理详解

    目录 一.基本使用 二.LifecycleObserver接口和LifecycleOwner接口 三.getLifecycle() 四.绑定生命周期 总结 Lifecycle是Android Architecture Components的成员,是一个生命周期感知组件,能够感知Activity.Fragment等组件的生命周期变化,并将变化通知到已注册的观察者.正确的使用有助于更好地组织代码,减少内存泄漏,增强稳定.下面分析他的实现原理,看看到底只怎么感知生命周期的. 一.基本使用 1.引入依赖

随机推荐