前端训练 -- 如何解决 Promise 传染性

前言

  • 我们现在都经常用 Promise 函数,使用 Promise 的一个重要目的是想配合 async await 来避免异步代码带来的回调地狱问题,让代码简洁优雅提高可读性
  • 但鱼和熊掌总是不可兼得的,Promize + async await 所带来的问题就是整条调用链都存在副作用,这直接影响到了纯函数,虽说很多实际项目中副作用也可以正常跑,特别是vue项目,倒是react项目的设计思想就是重视纯函数,从而确保项目架构的健壮性。
    • 【附】:纯函数的定义:函数的结构仅受传参影响,不受任何外界参数影响,也不会去影响函数以外的任何变量;
  • 那如何解决这个问题呢?

思路提示

  • 解决思路是:说玄乎了是【"异步"强行变"同步"】,说人话就是【缓存 + 多次重复调用 + 一次回调】

代码思路在这里!!

  • 假设我们有这么一段 Promise 代码
  • 可以看到因为 requestUserData 这个函数返回的是 Promise,导致后续调用的 getUser、getUserName、mainTest 都必须使用 async await,否则就会产生 .then 而带来的回调地狱问题
js 复制代码
// 模拟请求用户数据的异步函数
function requestUserData() {
    return new Promise((resolve, reject) => {
        // 模拟异步请求
        setTimeout(() => {
            const mockUser = { data: { name: '张三', age: 20 } };
            resolve(mockUser);
        }, 2000);
    })
}

async function getUser() {
    const res = await requestUserData();
    return res.data;
}

async function getUserName() {
    const user = await getUser();
    return user.name;
}

// 测试
async function mainTest() {
    const name = await getUserName();
    console.log(name); // 输出:张三
}
mainTest()
  • 那想象一下,如何能够让这样的代码去掉 async await 也能正常执行呢?
  • 比如像下面这样:
js 复制代码
// 模拟请求用户数据的异步函数
function requestUserData() {
    return new Promise((resolve, reject) => {
        // 模拟异步请求
        setTimeout(() => {
            const mockUser = { data: { name: '张三', age: 20 } };
            resolve(mockUser);
        }, 2000);
    })
}

function getUser() {
    const res = requestUserData();
    return res.data;
}

function getUserName() {
    const user = getUser();
    return user.name;
}

// 测试
function mainTest() {
    const name = getUserName();
    console.log(name); // 输出:张三
}
mainTest()
  • 这就需要放开我们的脑回路,通过稍微离奇点的思路去实现
  • 【思路重点】:这个方案必须基于都是纯函数的前提下,若其中任何一个函数中存在副作用,则会导致多次调用而产生不同的结果或影响
  • 为了确保整条 Promise 调用链的函数都能只去掉 async await 而不用改动原有逻辑,我们需要修改发起 Promise 的根源函数 requestUserData 并另写一个启动函数,思路草图如下(与实际实现代码存在些出入,仅表示整体实现思路):
  • 从图中可以看出,在这个方案中,我们会根据请求状态调用3次后续的函数,所以必须确保都是纯函数,才能让结果不受多次调用的影响

最终代码在此!!!

js 复制代码
const REQUEST_STATUS = {
  PADDING: 'padding',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}
// 【缓存】
const cache = {
  status: '', // 'padding' 'rejected' 'fulfilled'
  response: null,
  error: ''
}
// 模拟请求用户数据的异步函数
function requestUserData() {
  if (cache.status === REQUEST_STATUS.FULFILLED) {
    return cache.response
  } else if (cache.status === REQUEST_STATUS.REJECTED) {
    throw new Error(cache.error)
  } else if (cache.status === REQUEST_STATUS.PADDING) {
    throw new Error(REQUEST_STATUS.PADDING)
  }

  console.log('发起请求...')
  return new Promise((resolve, reject) => {
    // 模拟异步请求
    setTimeout(() => {
      const mockException = false
      if (!mockException) {
        const mockUser = { data: { name: '张三', age: 20 } }
        resolve(mockUser)
      } else {
        const ex = 'request error'
        reject(ex)
      }
    }, 2000)
  })
}

function getUser() {
  const res = requestUserData()
  return res.data
}

function getUserName() {
  const user = getUser()
  return user.name
}

// 测试
function mainTest() {
  const name = getUserName()
  console.log('接收到异步数据:',name) // 输出:张三
}

function execSync(func) {
  try {
    // 【一次回调】
    requestUserData().then((res) => {
      cache.status = REQUEST_STATUS.FULFILLED
      cache.response = res
      // 【重复调用】
      func()
    }).catch((err) => {
      cache.status = REQUEST_STATUS.REJECTED
      cache.error = err
      console.error('请求失败:', err)
    })
    cache.status = REQUEST_STATUS.PADDING
  } catch (ex) {
    if (ex === REQUEST_STATUS.PADDING) {
      console.log('请求中...')
    } else {
      throw new Error(`ERROR: ${ex}`)
    }
  }
}

execSync(mainTest)

完整的测试 html 代码在此!!!

  • 可直接 ctrl cv 查看效果
html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root"></div>
  <script>
    const rootDom = document.getElementById('root')

    const REQUEST_STATUS = {
      PADDING: 'padding',
      FULFILLED: 'fulfilled',
      REJECTED: 'rejected'
    }
    const cache = {
      status: '', // 'padding' 'rejected' 'fulfilled'
      response: null,
      error: ''
    }
    // 模拟请求用户数据的异步函数
    function requestUserData() {
      if (cache.status === REQUEST_STATUS.FULFILLED) {
        return cache.response
      } else if (cache.status === REQUEST_STATUS.REJECTED) {
        throw new Error(cache.error)
      } else if (cache.status === REQUEST_STATUS.PADDING) {
        throw new Error(REQUEST_STATUS.PADDING)
      }
  
      console.log('发起请求...')
      return new Promise((resolve, reject) => {
        // 模拟异步请求
        setTimeout(() => {
          const mockException = false
          if (!mockException) {
            const mockUser = { data: { name: '张三', age: 20 } }
            resolve(mockUser)
          } else {
            const ex = 'request error'
            reject(ex)
          }
        }, 2000)
      })
    }
  
    function getUser() {
      const res = requestUserData()
      return res.data
    }
  
    function getUserName() {
      const user = getUser()
      return user.name
    }
  
    // 测试
    function mainTest() {
      const name = getUserName()
      const content = `接收到异步数据:${name}`
      console.log(content) // 输出:张三
      rootDom.innerText = content
    }
  
    function execSync(func) {
      try {
        
        rootDom.innerText = 'padding...'
        requestUserData().then((res) => {
          cache.status = REQUEST_STATUS.FULFILLED
          cache.response = res
          func()
        }).catch((err) => {
          cache.status = REQUEST_STATUS.REJECTED
          cache.error = err
          console.error('请求失败:', err)
        })
        cache.status = REQUEST_STATUS.PADDING
      } catch (ex) {
        if (ex === REQUEST_STATUS.PADDING) {
          console.log('请求中...')
        } else {
          throw new Error(`ERROR: ${ex}`)
        }
      }
    }
  
    execSync(mainTest)
  </script>
</body>

</html>

总结

  • 本篇文章仅是个简单案例,提供实现思路,根据这个案例其实可以看出,想去除 Promise + async await 带来的传染性(调用链都存在副作用),是需要根据不同的情况而编写相应的处理逻辑的(具体的实现细节由实业务际代码而定),不过实现思路目前看来几乎是固定的,即通过本文提到的【缓存 + 多次重复调用 + 一次回调】

  • 本文案例中虽说仅需2次重复调用,但是在某些场景下可能会因业务需求而需要多次重复调用才能确保结果的正确性

  • 最后,感谢阅读,若写的有问题或有什么见解,欢迎友好评论

  • 附:

    • 这里想特别提一下 react 中的 组件,该组件并没有通过 async await 也没有用到 Promise 的回调地狱,但是却能够实现异步回调的效果,即发起请求的时候加载 fallback={},当然请求成功后则加载 ,从代码上可以看出是同步的,其实本文中分享的知识点就是来源于此(当然该组件的源码是很复杂的,毕竟是框架内部,要考虑到很多因素)!
jsx 复制代码
<Suspense fallback={<Loading />}>
    <SomeComponent />  
</Suspense>
相关推荐
恋猫de小郭3 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端