likes
comments
collection
share

WEB前端奇淫巧计-消除异步的传染性

作者站长头像
站长
· 阅读数 8

简介

大家好今天给大家介绍一个关于异步的比较恶心的东西也许大家在开发中也曾遇到过只不过解决起来比较棘手废话不多说直接上代码

async function getUser() {
  return await fetch('https://my-json-server.typicode.com/typicode/demo/profile').then((resp) => resp.json())
}

async function m1(){
  //other  works
  return await getUser()
}

async function m2(){
  //other  works
  return await m1()
}

async function m3(){
  //other  works
  return await m2()
}

async function main() {
  const res = await m3()
  console.log('res', res)
}
main()

经过观察上述代码有没有发现一旦一个函数使用 async await其他函数调用这个函数进行异步操作时,也要加上async await突然有没有觉得有那么一丝丝小恶心我们今天的目的就是把以上的async await去掉也能达到同样的效果

function getUser() {
  return fetch('https://my-json-server.typicode.com/typicode/demo/profile')
}

function m1() {
  //other  works
  return getUser()
}

function m2() {
  //other  works
  return m1()
}

function m3() {
  //other  works
  return m2()
}

function main() {
  const res = m3()
  console.log('res', res)
}
main()

就像以上代码调用,也能实现同样的效果是不是一下子有点懵懵的这其实是一个大厂的内部晋升题,还是有点小难度的这个问题在一些框架的底层也会常遇到我来带你逐步探讨

解决问题

不难发现通过以上直接去掉async await是无法得到原来的结果的因为它会返回一个promise 对象,无法使res得到真实的数据这里我先说一下大概思路首先fetch会返回一个promise,但是在请求时就想对结果进行操作,显然是不可能的这时候我们需要在fetch没返回我们想要的数据前先终止函数运行,等拿到正确的数据后我们再运行函数是不是听到这个过程也是一头雾水呀先别着急继续往下看如果想要函数终止运行有个办法那就是抛出异常 throw error然后等fetch返回数据data后,对数据进行缓存缓存后开始函数的运行,最后交付data看一下流程图WEB前端奇淫巧计-消除异步的传染性整体流程大概就是这样为了方便理解,我化简一下上述代码

function main() {
  const res = fetch('https://my-json-server.typicode.com/typicode/demo/profile')
  console.log('res', res)//res要得到一个data数据而不是一个promise对象
}
main()

我们都知道fetch实际返回一个promise对象此时返回的是一个promiseWEB前端奇淫巧计-消除异步的传染性在不改变main函数体的情况下使得res是我们想要的数据而不是promise下面是我们想要的数据WEB前端奇淫巧计-消除异步的传染性那我们就得想办法更改main的调用方式

function main() {
  const res = fetch('https://my-json-server.typicode.com/typicode/demo/profile')
  console.log('res', res)//res要得到一个data数据而不是一个promise对象
}
function run(func){
  //瓜瓜一顿操作,使得fetch返回真实的数据而不是promise
}
run(main)

根据上述讲的流程,我们来看一下run函数的具体过程注释我已经写的很详细了大家认真看哦

function run(func) {
  let cache = []//缓存的列表,由于可能不止一个fetch,所以要用一个list
  let i = 0;//缓存列表的下标
  const _originalFetch = window.fetch//储存原先的fetch
  window.fetch = (...args) => {//重写fetch函数,这个fetch要么抛出异常,要么返回真实的数据
    if (cache[i]) {//判断一下缓存是否存在,如果存在就返回真实的数据或抛出异常
      if (cache[i].status === 'fulfilled') {
        return cache[i].data
      } else if (cache[i].status === 'rejected') {
        throw cache[i].err
      }
    }
    const result = {
      status: 'pending',
      data: null,
      err: null
    }
    cache[i++] = result//添加缓存
    //发送请求
    //真实的fetch调用
    const prom = _originalFetch(...args).then(resp => resp.json()).then(resp => {
      //等待返回结果,然后修改缓存
      result.status = 'fulfilled'
      result.data = resp
    }, err => {
      result.status = 'rejected'
      result.data = err
    })
    //如果没有缓存,就添加缓存和抛出异常
    throw prom
    //这里为什么会抛出真实fetch返回的promise,主要是因为外面会用到这个promise然后等待拿到最终结果
  }
  try {
    //在try里调用func也就是上述的main函数
    //由于main里面有fetch,且第一次没有缓存,所以会抛出一个异常
    func()

  } catch (err) {
    //从这里捕获到异常
    //这里的err就是上述fetch返回的promise

    if (err instanceof Promise) {//验证一下是不是promise
      const reRun = () => {
        i = 0//重置一下下标
        func()
      }
      err.then(reRun, reRun)//待promise返回结果后重新执行func,也就是重新执行main
      //这次执行已经有缓存了,并且返回中有了正确的结果,所以重写的fetch会返回真实的数据
    }
  }
}

通过这么一个函数调用main,就可以使得在不改变main函数体的情况下使得fetch返回真实的数据而不是promise对象是不是感到很神奇我们来看下完整代码

function getUser() {
  return fetch('https://my-json-server.typicode.com/typicode/demo/profile')
}

function m1() {
  //other  works
  return getUser()
}

function m2() {
  //other  works
  return m1()
}

function m3() {
  //other  works
  return m2()
}

function main() {
  const res = m3()
  console.log('res', res)
}

function run(func) {
  let cache = []//缓存的列表
  let i = 0;//缓存下标
  const _originalFetch = window.fetch//储存原先的fetch
  window.fetch = (...args) => {//重写fetch函数
    if (cache[i]) {
      if (cache[i].status === 'fulfilled') {
        return cache[i].data
      } else if (cache[i].status === 'rejected') {
        throw cache[i].err
      }
    }
    const result = {
      status: 'pending',
      data: null,
      err: null
    }
    cache[i++] = result
    //发送请求
    const prom = _originalFetch(...args).then(resp => resp.json()).then(resp => {
      result.status = 'fulfilled'
      result.data = resp
    }, err => {
      result.status = 'rejected'
      result.data = err
    })
    throw prom
  }
  try {
    func()
  } catch (err) {
    //什么时候引发重新执行function
    if (err instanceof Promise) {
      const reRun = () => {
        i = 0
        func()
      }
      err.then(reRun, reRun)
    }
  }
}
run(main)

此时执行的结果,就是我们想要的结果WEB前端奇淫巧计-消除异步的传染性没错就是这样,nice

在框架中的应用

其实在react这个应用很常见我们先来看一段代码

const userResource = getUserResource()
function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
    </Suspense>
  )
}
function ProfileDetails(){
  const user = userResource.read();
  return <h1>{user.name}</h1>
}
ReactDOM.render(<ProfilePage/>, document.getElementById("root"));

别急别急我来稍微翻译下这段代码的意思是在ProfileDetails没加载到数据前先显示Loading profile...待ProfileDetails加载到数据就渲染 {user.name}他是怎么实现的呢如果放在vue里面ProfileDetails必须为一个异步函数而在这里的实现方案与我上述讲述的类似我们来验证一下在ProfileDetails打印1

function ProfileDetails(){
  console.log(1)//在这里输出一个1
  const user = userResource.read();
  return <h1>{user.name}</h1>
}

输出结果是这样的WEB前端奇淫巧计-消除异步的传染性为什么会输出两个1呢原因就和我们上述代码类似在userResource.read()第一次执行它会抛出一个错误第二次是已经拿到数据所以它执行了两遍,最终拿到了数据我们在函数里手动抛出一个promise

function ProfileDetails(){
  throw new Promise((resolve)=>{})//我们在这里抛出一个promise,且函数体里没有执行resolve()
  const user = userResource.read();
  return <h1>{user.name}</h1>
}

你会发现页面一直展示Loading profile...WEB前端奇淫巧计-消除异步的传染性因为我们抛出的promise,一直没有resolve,也就是等待不了结果返回,所以它只会渲染Loading profile...保持不变肿么样,神奇吧,你学费了嘛有兴趣的可以一起学习交流,有什么问题也可以联系我小编微信:buouyupro