异步同步化在React的实践

574 阅读3分钟

为什么要异步同步化

一般在前端编程的时候,我经常会遇到一些异步函数,比如网络请求这类的,代码很有可能是这个样子的。

   const state = {
       code:0,
       message:'',
       data:{}
   };
   
   fetch('/api/user/info?userId=123')
       .then(res=>res.json())
       .then(res=>{
          state = res; 
       });

在这段代码里面在阅读的时候,我们需要理解两个概念,Promise和回调,或者再底层一点,事件发布和订阅。往往使用这种方式的时候相比较以下的代码方式会带来两个问题。

   const state = {
       code:0,
       message:'',
       data:{}
   };
   
    let res = fetch('/api/user/info?userId=123');
    res = res.json();
    state = res;
  1. 可读性变差
  2. 心智负担提高

当然在函数式编程里面,往往会把这种网络请求的叫做"副作用",从而导致函数不纯,当然你觉得完全可以用async函数来实现上面的效果,但是asyn函数带来的另外一个问题就是async传染性的问题,好像也没有特别好的方式去解决。

因此在函数式编程里面为了消除这样的副作用实践了一个概念叫代数效应,可以隔离函数中的副作用把函数变成纯函数。在这边把异步代码同步化的操作也是实践代数效应的一种方式。

关于纯函数和副作用的概念,不在这篇文章里面展开。

代数效应的实践

比如我现在有这样的函数执行流


async function getList(){
    return await fetch('/api/good/list?page=1&limit=10').then(res=>res.json());
}

async do1(){
    return await getList();
}

async do2(){
    return await do1();
}

async function main(){
    const list = await getList();
    // 获得的list信息
    console.log(list);
}

现在我要把他改成同步执行的方式去执行,也就是这样,打印出来的结果和改造之前是一样的。


function getList(){
    return fetch('/api/good/list?page=1&limit=10');
}

do1(){
    return getList();
}

do2(){
    return do1();
}

function main(){
    const list = getList();
    // 获得的list信息
    console.log(list);
}

要实现这样的效果,核心的关键在在于对fetch这个函数的改造,整体代码大约呈这样。


function run(fn){
    const _fetch = window.fetch;
    let cache = [];
    let index = 0;
    window.fetch = function (...args){
        // 缓存命中
        if(cache[index]){
            const { data, error, status } = catch[index];
            if(status === "resolved"){
                return data;
            }else if(status === "rejected"){
                throw error;
            }
            // 在这个例子里面没有pending的情况
        }
        const result = {
            status:"pending",
            error:null,
            data:null
        };
        cache[i++] = result;
        //发送请求
        const promise = _fetch(...args).then(res=>res.json()).then((res)=>{
            result.status = "resolved";
            result.data = res;
        },(error)=>{
            result.status = "rejected";
            result.error = error;
        });
        // 报错
        throw promise;
    };
    
    try{
        fn();
    }catch(promise){
       if(promise instanceof Promise){
           function finallyRun(){
                // 说明有一个请求完成了,可以重新开始计算了。
                index = 0;
                fn();
           }
          promise.then(finallyRun,finallyRun);
       }
    }
}

经过以上的改造,我们只需要这样,就能异步转同步了。


function getList(){
    return fetch('/api/good/list?page=1&limit=10');
}

do1(){
    return getList();
}

do2(){
    return do1();
}

function main(){
    const list = getList();
    // 获得的list信息
    console.log(list);
}
// 启动流程
run(main);

整体代码里面的缺点可能就是,fn函数会执行两次,但是这个问题在纯函数里面不是问题,因为纯函数要求幂等,也就是同样参数的情况下,多次执行获得的结果是一致的,且没有"副作用"

代数效应在React的实践

当然这个的效果,在React中有很多,尤其是Suspense组件,我们可以看下面的例子,作为我们的结尾。


import { FC, Suspense } from 'react';

import './style.css';
let status = 'pending';
let value = null;
function use(promise) {
  if (status === 'resolve') {
    return value;
  }
  if (status === 'reject') {
    return value;
  }
  promise.then(
    (res) => {
      status = 'resolve';
      value = res;
    },
    (error) => {
      status = 'reject';
      value = 'error';
    }
  );
  throw promise;
}

function delay(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('this delay time');
    }, 1000);
  });
}

function Name() {
  const name = use(delay());
  return name;
}

export const App: FC<{ name: string }> = () => {
  console.log('running');
  return (
    <div>
      <h1>hello world</h1>
      <p>start</p>
      <Suspense fallback={<div>loading</div>}>
        <Name />
      </Suspense>
      <p>end</p>
    </div>
  );
};