实践[前端] 基于Vue+Generator的排序异步可视化

众所周知JavaScript是单线程的,如果执行排序这种操作,则会造成页面渲染的阻塞,那么如何避免这种阻塞呢?我们可以使用异步渲染的方式将cpu密集的运算拆分为多个task,效果如下,我们在进行sort渲染的时候,按钮依然可以操作。红色为参加本次排序的数据项

从上面我们可以看到,每个渲染都分为多个task,而非一个task处理所有执行。

那么实现这种效果具体需要包括哪些点呢?

  1. 渲染canvas
  2. 排序程序
  3. 异步调度

首先我们定义排序使用的list,我们创建了一个数组,包含了180个数据项

ts 复制代码
const length = 180;
const randomList = Array.from({ length }).map(() => Math.random() * 250);

渲染canvas

下面代码的主要逻辑:

  1. 清除画布
  2. 开启绘制路径
  3. 循环列表,将极坐标转化为转化为笛卡尔坐标,并绘制出相应位置的点
  4. 返回一个promise,并延时一个定时器宏队列,保证每次渲染之间至少会被event_loop调度一次。
ts 复制代码
const WIDTH = 1000;
const HEIGHT = 1000;
const WIDTHCENTER = WIDTH / 2;
const HEIGHTCENTER = HEIGHT / 2;
const canvas = ref<HTMLCanvasElement>();
const context = ref<CanvasRenderingContext2D | null>();
const draw = (changeList: number[]) => {
  const ctx = context.value;
  if (!ctx) return;
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  console.log(changeList);
  randomList.forEach((node, i) => {
    const x = node * Math.cos((Math.PI / (length / 2)) * i);
    const y = node * Math.sin((Math.PI / (length / 2)) * i);
    ctx.beginPath();
    ctx.moveTo(WIDTHCENTER + x, HEIGHTCENTER + y);
    if (changeList.includes(i)) {
      ctx.fillStyle = "#f00";
      ctx.strokeStyle = "#f00";
    } else {
      ctx.fillStyle = "#000";
      ctx.strokeStyle = "#000";
    }
    ctx.arc(WIDTHCENTER + x, HEIGHTCENTER + y, 4, 0, Math.PI * 2);
    ctx.fill();
    ctx.moveTo(WIDTHCENTER, HEIGHTCENTER);
    ctx.stroke();
  });
  return new Promise<void>((res) => {
    setTimeout(() => {
      res();
    });
  });
};

排序函数

排序函数使用generator将每次循环完毕的调度权交给调用者,让调用者决定是否开启下一次循环。 下图是一个简单的冒泡排序

ts 复制代码
async function* sort() {
  for (let i = 0; i < randomList.length; i++) {
    let changeList = [];
    for (let j = 0; j < randomList.length; j++) {
      if (randomList[j] > randomList[j + 1]) {
        let temp = randomList[j];
        randomList[j] = randomList[j + 1];
        randomList[j + 1] = temp;
        changeList.push(j);
      }
    }
    await draw(changeList);
    yield true;
  }
  await draw([]);
  yield false;
}

调度函数

调度时我们使用while循环处理每个排序返回的next,并进行循环渲染,保证动画的连续性

ts 复制代码
const start = async () => {
  let next = sort();
  while (!(await next.next()).done);
};

完整代码

html 复制代码
<template>
  <div class="home">
    <canvas id="canvas" ref="canvas"></canvas>
    <div class="button" @click="() => count++">count+++ {{ count }}</div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref } from "vue";
const length = 180;
const WIDTH = 1000;
const HEIGHT = 1000;
const WIDTHCENTER = WIDTH / 2;
const HEIGHTCENTER = HEIGHT / 2;
const randomList = Array.from({ length }).map(() => Math.random() * 500);

const count = ref(0);
const canvas = ref<HTMLCanvasElement>();
const context = ref<CanvasRenderingContext2D | null>();

const draw = (changeList: number[]) => {
  const ctx = context.value;
  if (!ctx) return;
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  console.log(changeList);
  randomList.forEach((node, i) => {
    const x = node * Math.cos((Math.PI / (length / 2)) * i);
    const y = node * Math.sin((Math.PI / (length / 2)) * i);
    ctx.beginPath();
    ctx.moveTo(WIDTHCENTER + x, HEIGHTCENTER + y);
    if (changeList.includes(i)) {
      ctx.fillStyle = "#f00";
      ctx.strokeStyle = "#f00";
    } else {
      ctx.fillStyle = "#000";
      ctx.strokeStyle = "#000";
    }
    ctx.arc(WIDTHCENTER + x, HEIGHTCENTER + y, 4, 0, Math.PI * 2);
    ctx.fill();
    ctx.moveTo(WIDTHCENTER, HEIGHTCENTER);
    ctx.stroke();
  });
  return new Promise<void>((res) => {
    setTimeout(() => {
      res();
    }, 200);
  });
};

async function* sort() {
  for (let i = 0; i < randomList.length; i++) {
    let changeList = [];
    for (let j = 0; j < randomList.length; j++) {
      if (randomList[j] > randomList[j + 1]) {
        let temp = randomList[j];
        randomList[j] = randomList[j + 1];
        randomList[j + 1] = temp;
        changeList.push(j);
      }
    }
    await draw(changeList);
    yield true;
  }
  await draw([]);
  yield false;
}

const start = async () => {
  let next = sort();
  while (!(await next.next()).done);
};

onMounted(async () => {
  console.log(canvas.value);
  context.value = canvas.value?.getContext("2d");
  
  if (!canvas.value) return;
  
  canvas.value.width = WIDTH;
  canvas.value.height = HEIGHT;

  start();
});
</script>
<style lang="scss" scoped>
#canvas {
  width: 500px;
  height: 500px;
  // background-color: black;
}
.button {
  padding: 15px;
  display: inline-block;
  border-radius: 5px;
  user-select: none;
  cursor: pointer;
  border: 1px solid #cecece;
}
</style>

总结

上述代码实现了一个比较精简的异步拆分渲染拆分逻辑,如果以后遇到此类问题都可以利用这种方式将阻塞任务拆分,提高用户的体验感和互动性。

但是这种方式存在的问题是 因为每次调度会间隔15-20ms,会导致运算性能极其低下,慎用!!!

相关推荐
devincob3 小时前
js原生、vue导出、react导出、axios ( post请求方式)跨平台导出下载四种方式的demo
javascript·vue.js·react.js
木头没有瓜4 小时前
在 Windows 中清理依赖node_modules并重新安装
vue.js
不吃香菜的猪5 小时前
el-upload实现文件上传预览
前端·javascript·vue.js
dcloud_jibinbin6 小时前
【uniapp】小程序体积优化,分包异步化
前端·vue.js·webpack·性能优化·微信小程序·uni-app
qq_427506086 小时前
基于Vue 3和Element Plus实现简单的钩子函数管理各类弹窗操作
前端·javascript·vue.js
六月的可乐9 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐10 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
暴富的Tdy11 小时前
【基于 WangEditor v5 + Vue2 封装 CSDN 风格富文本组件】
vue.js·wangeditor·富文本
开发者小天11 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者12 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js