前端训练 -- 如何解决 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>
相关推荐
中微子几秒前
🔥 React Context 面试必考!从源码到实战的完整攻略 | 99%的人都不知道的性能陷阱
前端·react.js
中微子1 小时前
React 状态管理 源码深度解析
前端·react.js
加减法原则2 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele3 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4533 小时前
React移动端开发项目优化
前端·react.js·前端框架
天若有情6733 小时前
React、Vue、Angular的性能优化与源码解析概述
vue.js·react.js·angular.js
你的人类朋友3 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir3 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴3 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子3 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js