导入个Excel页面直接卡死,看我如何处理T0生产事故~

作者: jie 分类: 前端 发布时间: 2023-11-10 18:00

背景

我们都知道JavaScript是单线程的语言,也就是说同步代码都需要排队去执行,这样就会造成很多问题,就比如今天讲的:数据量大的excel文件的导入导出,会造成整个页面出现“假卡死”的现象。

什么是“假卡死”呢?意思就是页面其实并没有卡死,但是用户做一些操作时,页面并没有及时给到反馈,这就会让用户觉得页面卡死了~

数据量大的excel

数据量大的excel文件的导入导出,就会造成这个问题,原因是当我们使用库去解析数据量大的excel时,是非常耗时的,这就导致了后面的代码执行不了,所以出现“假卡死”现象

我们可以通过一个小案例来体现出这个问题,我们先准备一个 excel 文件,它差不多有 1w 行数据

接着我们在项目中去装xlsx这个插件,用于解析待会导入的excel文件

pnpm i xlsx

接着准备一个上传的input,和一个按钮button,并准备他们对应的事件

<input @change="handleImport" type="file" />
<button type="button" @click="handleClick">按钮</button>
const handleImport = (ev: Event) => {
  const file = (ev.target as HTMLInputElement).files![0];

  // 使用FileReader读取文件内容
  const reader = new FileReader();

  reader.onload = function (e: any) {
    const data = new Uint8Array(e.target.result);
    console.time('解析excel');
    // 使用SheetJS的XLSX库解析文件数据
    const workbook = XLSX.read(data, { type: 'array' });
    console.timeEnd('解析excel');
    // 获取工作表对象
    const firstSheet = workbook.Sheets[workbook.SheetNames[0]];

    // 解析工作表数据
    const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });

    // 处理解析后的数据
    // console.log(jsonData);
  };

  // 读取文件
  reader.readAsArrayBuffer(file);
};
const handleClick = () => {
  console.log('打印');
};

这个时候,我们去导入excel文件,然后马上去点击按钮,期望的输出顺序肯定是

-> 打印
-> 解析excel

因为解析excel是需要时间的,差不多要2-3s,在这2-3s中去点击按钮,应该先让按钮点击的事件先执行,但是事实上效果不是这样的,我这里其实在上传过程中点了很多次按钮了,但是就是不打印,而是等到excel解析完之后,才会打印,这就像卡死了一样,明明我点了按钮,但就是没反应

WebWorker解决导入

这个问题可以用 WebWorker 来解决,WebWorker 能开启一个子线程,来做一些我们需要让它做的事情,并把结果返回到主线程,这样就可以不阻塞主线程的代码执行了~

WebWorker VS setTimeout

有人就问了,为啥要使用 WebWorker,而不是 setTimout 呢?不是照样也可以吗?

  • 其实在单核电脑上,它两确实没啥区别,但是在多核电脑上,WebWorker 可以同时使用多个cpu来计算, 速度比 setTimeout快~
  • setTimeout 还是在主线程上执行,还是会占主线程的资源影响主线程的执行,但是 WebWorker 跟主线程是隔离开的

代码 & 效果

const handleImport = (ev: Event) => {
  const file = (ev.target as HTMLInputElement).files![0];
  const worker = new Worker(
    URL.createObjectURL(
      new Blob([
        `
        importScripts('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.4/xlsx.full.min.js');
        onmessage = function(e) {
          const fileData = e.data;
          const workbook = XLSX.read(fileData, { type: 'array' });
          const sheetName = workbook.SheetNames[0];
          const sheet = workbook.Sheets[sheetName];
          const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
          postMessage(data);
        };
        `,
      ]),
    ),
  );

  // 使用FileReader读取文件内容
  const reader = new FileReader();

  reader.onload = function (e: any) {
    const data = new Uint8Array(e.target.result);
    worker.postMessage(data);
  };

  // 读取文件
  reader.readAsArrayBuffer(file);

  // 监听Web Worker返回的消息
  worker.onmessage = function (e) {
    console.log('解析完成', e.data);
    worker.terminate(); // 当任务完成后,终止Web Worker
  };
};

我们可以看到,在解析excel的过程中,点击按钮的事件并不会被阻塞到了~

WebWorker解决导出

道理都是一样的,就是利用 WebWorker 来帮我们做一些比较耗时的事情

const handleExport = () => {
  const worker = new Worker(
    URL.createObjectURL(
      new Blob([
        `
        importScripts('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.4/xlsx.full.min.js');
        onmessage = function(e) {
          const data = e.data;
          const workbook = XLSX.utils.book_new();
          const sheetData = e.data
          const worksheet = XLSX.utils.aoa_to_sheet(sheetData);
          XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
          const excelData = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' });
          postMessage(excelData, [excelData.buffer]);
        };
        `,
      ]),
    ),
  );

  worker.onmessage = function (e) {
    const excelData = e.data;

    // 创建 Blob 对象,用于保存 Excel 文件数据
    const blob = new Blob([excelData], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });

    // 创建下载链接并触发下载
    const downloadLink = document.createElement('a');
    downloadLink.href = URL.createObjectURL(blob);
    downloadLink.download = 'export.xlsx';
    downloadLink.click();

    // 终止 Web Worker
    worker.terminate();
  };

  worker.postMessage([...你的数据]);
};

发表回复