Skip to content

消除异步传染性 [2023-03-04]

视频地址

问题

异步传染性

当函数内使用 await 时, 函数上必须写上 async, 后续调用这个函数的都需要加上 async/await, 在函数式编程中都变成副作用了(异步), 所以探索怎么消除异步

ts
async function request() {
  return await fetch('/config.js').then(res => res.text())
}
async function m1() {
  // other works
  return await request()
}
async function m2() {
  // other works
  return await m1()
}
async function main() {
  const res = await m2()
  console.log(res)
}

解决方式

  • 原始流程:
graph LR A(函数开始) --- B[/fetch/] B --- E[data] E --- F(函数结束)
  • 报错流程:
    flowchart TB subgraph 第一次调用 direction LR A(函数开始) --- B[/fetch/] B --- C[data] C --- D[缓存data] D --- E[引发错误] end subgraph 第二次调用 direction LR G(函数开始) G --- H[/fetch/] H --- I[交付data] I --- J[函数继续] J --- K(函数结束) end 第一次调用 --> 第二次调用

取消异步, 然后同步调用, 利用报错实现(粗略代码)

ts

function request() {
  return fetch('/config.js')
}
function m1() {
  // other works
  return request()
}
function main() {
  const text = m1()
  console.log('[ text ]', text) // 控制台可以看到
}
function run(func: Function) {
  type Cache = {
    status: 'fulfilled' | 'pending' | 'rejected',
    data?: any,
    err?: any,
  }
  // 保留原始fetch
  const _fetch = window.fetch
  // fetch 缓存返回结果
  let cache = [] as Cache[]
  let i = 0 // 这次调用期间是第几次调用
  window.fetch = (...args) => {
    if (cache[i]) {
      // 如果有缓存
      if (cache[i].status === 'fulfilled') {
        // 如果完成,交付data
        return cache[i].data
      } else if (cache[i].status === 'rejected') {
        // 如果失败,抛出err
        throw cache[i].err
      }
    }
    // 如果没有,定义缓存
    const result: Cache = {
      status: 'pending',
      data: null,
      err: null,
    }
    cache[i++] = result
    // 发送请求
    const promise = _fetch(...args)
      .then(res => res.text())
      .then(data => {
        result.status = 'fulfilled'
        result.data = data
      }, err => {
        result.status = 'rejected'
        result.err = err
      })
    //手动抛出promise,然后在调用func中try/catch,判断是否是抛出的promise
    throw promise
  }
  try {
    func()
  } catch (error) {
    // 接收到手动抛出的promise的错误之后,重新执行函数, i归零以命中缓存, 拿到缓存结果
    if (error instanceof Promise) {
      const reRun = () => {
        i = 0
        func()
      }
      error.then(reRun, reRun)
    }
  }
}
run(main)

个人理解

TIP

通过letAsync2SyncInFn, 内部修改doSomething调用的fn函数, 使其调用时, 调用的是letAsync2SyncInFn内部创建的异步函数, 又通过 throw 和 缓存数据, 使得doSomething中可以同步调用fn函数并拿到结果

ts
type CacheI = {
  status: 'fulfilled' | 'pending' | 'rejected',
  data?: any,
  err?: any,
}

let fn = () => Promise.resolve(1)

const doSomething = () => {
  const data = fn()
  console.log('[ data ]', data) // {status: 'fulfilled', data: 1}
}

const letAsync2SyncInFn = (func: Function) => {
  const _fn = fn
  let cache!: CacheI
  fn = (...args) => {
    if (cache) {
      if (cache.status === 'fulfilled') {
        return cache
      } else if (cache.status === 'rejected') {
        throw cache
      }
    }
    cache = { status: 'pending' }
    const promise = _fn(...args).then(data => {
      cache.data = data
      cache.status = 'fulfilled'
    }, err => {
      cache.err = err
      cache.status = 'rejected'
    })
    throw promise
  }
  try {
    func()
  } catch (error) {
    if (error instanceof Promise) {
      error.then(func, func)
    }
  }
}

letAsync2SyncInFn(doSomething)