众所周知JavaScript是单线程的,如果执行排序这种操作,则会造成页面渲染的阻塞,那么如何避免这种阻塞呢?我们可以使用异步渲染的方式将cpu密集的运算拆分为多个task,效果如下,我们在进行sort渲染的时候,按钮依然可以操作。红色为参加本次排序的数据项
从上面我们可以看到,每个渲染都分为多个task,而非一个task处理所有执行。
那么实现这种效果具体需要包括哪些点呢?
- 渲染canvas
- 排序程序
- 异步调度
首先我们定义排序使用的list,我们创建了一个数组,包含了180个数据项
ts
const length = 180;
const randomList = Array.from({ length }).map(() => Math.random() * 250);
渲染canvas
下面代码的主要逻辑:
- 清除画布
- 开启绘制路径
- 循环列表,将极坐标转化为转化为笛卡尔坐标,并绘制出相应位置的点
- 返回一个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,会导致运算性能极其低下,慎用!!!