同步异步的理解

138 阅读8分钟

晚上睡不着觉,浅浅的看了几篇文章,梳理一下自己对同步异步问题的理解

问题1 为什么在开发过程中更推荐异步编程呢(如 async/await)?

下面描述一下异步编程的好处:

  1. 非阻塞性,使用async/await使代码在等待网络请求的响应过程中不会阻塞主进程,这里要解释一下,因为js是单线程的,意味着它只能同时执行一个任务,如果使用同步方式调用接口,代码会在请求完成之前完全停止执行,可能会导致页面”卡死“,用户可能会觉得应用无响应,体验感会不好,而使用异步编程可以让程序在等待接口响应导时继续执行其他操作,比如更新页面、点击按钮等其他操作。
  2. 错误处理
  • 异步:在async/await函数中,可以使用try/catch语句来捕获异常中的错误.
  • 同步:在同步代码中,错误处理可能会更复杂,尤其是在涉及多个异步操作时.
  1. 异步编程中async/await可以使代码更接近于同步代码结构,易于理解和维护,可以像写同步代码一样按照逻辑顺序书写异步代码,同时也避免了回调地狱的问题

**注:他俩最大的区别:异步请求允许多个请求同时进行,而不需要等待每个请求完成后再开始下一个,这种并发处理可以提高性能,尤其在需要同时在多个接口获取数据时。

问题2 异步编程和同步编程的使用场景分别是什么?

  1. 同步编程:小文件的读写、程序启动时的初始化操作、以及最常见的例子吧,绿-黄-红 灯的执行顺序
  2. 异步编程: 大文件的读写、网络请求、并发任务、定时任务

问题3 await可以单独使用吗?

答案是:当然不可以了,await必须和async配套使用,如果一个函数使用了async,他会返回一个promise,而await的作用就是暂停当前的async函数的执行,直到promise完成,如果单独使用await一定会报错,因为js引擎无法提供必要的异步上下文,导致语法错误,下面贴点代码举个例子:

const deleteTemplate = async (id: string) => {
    console.log("1111111");
    const result = await removeTemplate(id)
    console.log("2222222");
    if (result?.success) {
      message.success({
        content: "模板删除成功",
        class: MESSAGE_CLS.SUCCESS,
      });
      await refreshList();
    } else if (result?.message == "模板已被使用,不能删除") {
      message.warn({
        content: "模板已被使用,不能删除",
        class: MESSAGE_CLS.WARN,
      });
      await refreshList();
    } else {
      message.warn({
        content: result.message,
        class: MESSAGE_CLS.WARN,
      });
    }
    console.log("333333");
  }
};

我之前一直有个疑问,都有await了,还要等待接口响应成功才执行后续的操作,那为啥不直接用同步编程呢,现在看到这些解释,嗯,可以说的通了哈哈,那考一下,这段代码中的执行顺序是什么?

1111111
2222222
333333

问题4 什么是Promise?
Promise是js中用于处理异步操作的一种机制,它代表一个可能在未来某个时间点完成的操作的结果。Promise可以处于三种状态之一:

  1. Pending(待定):初始状态,既不是成功,也不是失败。
  2. Fulfilled(已完成):操作成功完成,Promise也会变为成功状态。
  3. Rejected(已拒绝):操作失败,Promise变为失败状态

Promise的基本用法

创建 Promise

可以使用Promise构造函数来创建一个新的Promise。构造函数接受一个执行器函数,该函数有两个参数:resolve和reject.

const myPromise = new Promise((resolve, reject) => {
//   异步操作
  const success = true;
  if (success) {
    resolve("操作成功");
  } else {
    reject("操作失败");
  }
});

使用Promise

// 第一种通过async/await(推荐!!!)
const handlePromise = async () => {
  try {
    // 等待 Promise 解析
    const result = await myPromise;
    // 处理成功的情况
    console.log("成功:", result);
  } catch (e) {
    // 处理失败的情况
    console.error("失败:", error)
  }
}
handlePromise(); // 调用 async 函数

// 第二种 使用 then 和 catch 方法来处理 Promise 的结果
const handlePromise = () => {
  myPromise.then((result) => {
    console.log(result);
  }).catch((err) => {
    console.log(err);
  })
}
handlePromise(); // 调用 函数


Promise.all的用法

Promise.all:接受一个Promise数组,只有当所有的Promise都成功时,才会返回一个成功的Promise。如果有任何一个Promise失败,则会返回失败的Promise

function fetchData(url) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`Data from ${url}`);
        }, 1000);
    });
}

const urls = ['url1', 'url2', 'url3'];

Promise.all(urls.map(fetchData)).then((results) => {
    console.log(results); // ['Data from url1', 'Data from url2', 'Data from url3']
}).catch((error) => {
    console.error('Error fetching data:', error);
});

回调地狱的问题

回调地狱(Callback Hell)是指在js 中使用回调函数处理异步操作时,代码层级嵌套过深,导致代码可读性差、维护困难的现象,通常发生在需要进行多个异步操作时,每个操作的结果都依赖于前一个操作的完成。

简单举个回调地狱的示例,假设我们需要依次执行三个异步操作,每个操作的结果都依赖于前一个操作

function asyncOperation1(callback) {
  setTimeout(() => {
    console.log("操作1完成");
    callback("结果1");
  }, 1000);
}

function asyncOperation2(resultFromOp1, callback) {
  setTimeout(() => {
    console.log("操作2完成,接收到:", resultFromOp1);
    callback("结果2");
  }, 1000);
}

function asyncOperation3(resultFromOp2, callback) {
  setTimeout(() => {
    console.log("操作3完成,接收到:", resultFromOp2);
    callback("结果3");
  }, 1000);
}

// 使用回调函数进行嵌套
asyncOperation1(result1 => {
  asyncOperation2(result1, result2 => {
    asyncOperation3(result2, result3 => {
      console.log("所有操作完成,最终结果:", result3);
    });
  });
});

在说一下解决方案

  1. 先说最推荐的,使用 async/await,使异步代码看起来更像同步代码,进一步提高了可读性
async function executeOperations() {
  try {
    const result1 = await asyncOperation1();
    const result2 = await asyncOperation2(result1);
    const result3 = await asyncOperation3(result2);
    console.log("所有操作完成,最终结果:", result3);
  } catch (error) {
    console.error("发生错误:", error);
  }
}
executeOperations();
  1. 使用 Promise:Promise 可以将异步操作的结果封装起来,避免深层嵌套。
function asyncOperation1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("操作1完成");
      resolve("结果1");
    }, 1000);
  });
}

function asyncOperation2(resultFromOp1) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("操作2完成,接收到:", resultFromOp1);
      resolve("结果2");
    }, 1000);
  });
}

function asyncOperation3(resultFromOp2) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("操作3完成,接收到:", resultFromOp2);
      resolve("结果3");
    }, 1000);
  });
}

// 使用 Promise 链式调用
asyncOperation1()
  .then(result1 => asyncOperation2(result1))
  .then(result2 => asyncOperation3(result2))
  .then(result3 => {
    console.log("所有操作完成,最终结果:", result3);
  })
  .catch(error => {
    console.error("发生错误:", error);
  });

讲个在开发过程中遇到的问题

根据模板生成正式报告:见名知意,意识就是根据文件模板生成一份正式报告,也就是说要先把模板下载到本地,然后在本地进行占位符替换,生成新文件,再把新文件上传。看一段代码:

const generateReportDraft = async (
  templateInfo: {
    id: string;
    name: string;
    templateType: number;
    type: string;
    // 新
    fileId: string;
    path: string;
  },
  replaceForTempTemplate: ReplaceData["replaceForTempTemplate"],
  projectId: string,
  projectSubitemId: string, // 子项目的 id
  sysCompanyCode: string | number,
  dataDiskBasePath: string,
  parentFolder: {
    fileId: string;
    relativePath: string;
    absolutePath: string;
  } // 初稿(临时模板)保存地址
): Promise<{
  success: boolean;
  localDir: string;
  message?: string;
  reportRecord?: Partial<ProjectReportAddOrUpdateVO>;
  editorConfig?: any;
  docService?: string;
}> => {
  // debugger;
  let localDir = "";
  if (
    !templateInfo.id ||
    !projectId ||
    !replaceForTempTemplate ||
    !projectSubitemId ||
    !parentFolder
  ) {
    failRes.message = "缺少参数";
    return failRes;
  }
  const {
    id: templateId,
    name: templateName,
    type: templateFileExt,
    path: templatePath,
  } = templateInfo;
  const templateRemotePath = dataDiskBasePath + templatePath;
  const remotePath = parentFolder.absolutePath + "/" + templateName;
  // 下载临时模版
  const downloadRes = await downloadTemplateFileByApi(
    {
      name: templateName,
      path: templateRemotePath,
      templateId: templateId,
      ext: templateFileExt
    },
    TransferChannel.DT
  );

  const templateLocalPath = downloadRes.result?.localPath || "";
  localDir = downloadRes.result?.localDir || "";
  failRes.localDir = localDir;

  if (!downloadRes?.success || !templateLocalPath || !localDir) {
    failRes.message = downloadRes.message || "下载模板失败";
    return failRes;
  }

  // 循环最多尝试10次,检查模板文件是否存在 如果文件不存在,等待100毫秒后再次检查
  // 因为接口下载文件 需要时间,会有文件不存在的情况
  for (let i = 0; i < 10; i++) {
    const {owner} = await checkExist("local", templateLocalPath);
    if (owner && owner !== void 0) {
      break;
    }
    await new Promise((resolve) => setTimeout(resolve, 100)); // 等待1秒
  }

  // 在本地下载的模版进行占位符替换
     下面代码省略
};

其中的关键就在:

// 循环最多尝试10次,检查模板文件是否存在 如果文件不存在,等待100毫秒后再次检查
  // 因为接口下载文件 需要时间,会有文件不存在的情况
  for (let i = 0; i < 10; i++) {
    const {owner} = await checkExist("local", templateLocalPath);
    if (owner && owner !== void 0) {
      break;
    }
    await new Promise((resolve) => setTimeout(resolve, 100)); // 等待1秒
  }

因为是项目改造,之前下载文件用的是sftp直连下载,下载速度很快,并没有发现在本地进行替换的时候文件不存在的问题,但是这次改成接口下载之后,下载速度慢相对慢一些,就发现了问题所在,会有文件不存在的情况,当时也挺搞笑的,怎么也想不到因为什么,然后打断点因为会有时间差,在这个时间差内文件已经下载完了,所以打断点的时候一切正常,还看不出什么,后来请教前辈,前辈提点了一下,恍然大悟哈哈

好啦,就写到这里吧,写文章真的好费时间,只是个人理解,仅供参考哦