有一个页面,要用 canvas 画出背景图片和文字,并且定时刷新页面。
这个图分三层, 第一层画红色和蓝色的矩形框,第二层画背景图,第三层写文字。

原来的代码中我直接把canvasContext.fillRect、canvasContext.drawImage、cacanvasContextnvas.fillText 放在一个函数中,循环多少次就画多少次。
这种写法直接放在页面中时,页面中的背景图片并不会闪烁。
但后来我优化了代码,把 canvas 画图部分挪到了组件中,然后在上级页面中调用 canvas 的组件页面。每秒更新数据,也就是每秒 canvas 重绘。
新版 canvas (type="2d") 获取 canvas 节点的方式变了:
或许是子页面与主页面的通讯增加了性能开销,也可能是wx.createSelectorQuery() 和 this.createSelectorQuery() 开销不同,总之这个页面开始闪烁,每次刷新都只能==随机显示一张背景图==。
分析
为了判断是哪一部分性能消耗大,我注释了 canvasContext.drawImage 部分,发现刷新时页面不闪烁了。由此判定是 ==背景图片== 的问题。
经过进一步排查,this.createSelectorQuery() 性能没有我想的大,问题还在别处。
this.createSelectorQuery()
.select(`#oil${id}`)
.node(({ node: canvas }) => {
console.log("canvas: ", canvas);
})
.exec();
进一步排查,问题的确在图片上,但不是我以为的 canvasContext.drawImage 而在加载图片资源上。
因为在 canvas 中,不能直接使用 canvasContext.drawImage(imageUrl),而要先用 canvasContext.createImage(); 创建一个图片实例,再修改 bgImg.src ,然后才能使用 canvasContext.drawImage
const bgImg = canvas.createImage();
bgImg.src = "/static/hcbgimg.png";
const canvasContext = canvas.getContext("2d");
canvasContext.drawImage(bgImg, 0, 0, bgimgWidth, bgimgHeight);
在原先代码中,每次循环都重新创建图片实例,也就是每次都调用 canvas.createImage() ,所以页面闪烁。
解决办法
:::: steps
-
创建 canvas 实例和
canvasContext.draw分开,创建 canvas 实例只运行一次,每次更新数据后 canvas 实例不重新调用。 -
把创建图片实例放在创建第一个 canvas 实例之后,也只调用一次
-
把
bgImg缓存起来,以后每一次canvasContext.drawImage(bgImg)都调用这个缓存的对象
::::
组件代码(部分)
<template ><view><view v-for= "item in tankList" ><canvas type= "2d" :id="'oil' + item.id" class= "canvas" ></canvas ></view></view> </template > <script > import { converIntToRgb, getSysInfo } from "/utils/utils.js"; export default { props: { tankList: { type: Array, default: [], },},data() { return {// 油罐图片的宽高bgimgWidth: 0,bgimgHeight: 0, context: [],canvasList: [],bgImg: null,};},mounted() {const screenWidth = getSysInfo();this.bgimgWidth = screenWidth * 0.9;this.bgimgHeight = screenWidth * 0.4;},watch: {tankList(newValue, oldValue) {// 第一次进入页面,等有数据了再画图if (oldValue.length == 0 && newValue.length != 0) {this.createContexts(this.bgimgWidth, this.bgimgHeight); //每次进来的时候先画一次 } // if 防止没有context还要画图报错if (this.context.length != 0) {this.refreshCanvas(this.bgimgWidth, this.bgimgHeight);}},},methods: {// 这个页面只运行一次 createContexts() {// ❗❗❗ 新版canvas需要消除锯齿const windowInfo = wx.getWindowInfo();const availableWidth = windowInfo.windowWidth;const dpr = windowInfo.pixelRatio; //设备像素比 // 1. 给 context[] 赋值(在这个页面只创建一次)this.tankList.forEach((tank, index) => {this.createSelectorQuery().select(`#oil${tank.id}`).node(({ node: canvas }) => {console.log("canvas: ", canvas);// 获取到第一个canvas节点时创建图片对象,以免后续加载图片太慢if (!this.bgImg) {this.bgImg = canvas.createImage();this.bgImg.src = "/static/bgimg.png";} this.context[index] = canvas.getContext("2d"); canvas.width = availableWidth * dpr;canvas.height = availableWidth * 0.4 * dpr; //按照下面css的尺寸比例this.context[index].scale(dpr, dpr); //必须有 this.canvasList[index] = canvas; //存储全局变量,其她函数中也可以使用已存储的canvas实例if (index === this.tankList.length - 1) {//全部建立成功后再画图,调用一遍,防止等待时间过长setTimeout(() => {this.refreshCanvas(this.bgimgWidth, this.bgimgHeight);}, 100);}}).exec();});},refreshCanvas(bgimgWidth, bgimgHeight) {const context = this.context; // 为了下面画canvas时少写一个 this // 2. 设置不同油罐油品的颜色let oilColors = []; //存储不同油罐油品的颜色this.tankList.forEach((tank) => {oilColors.push(converIntToRgb(tank.oilColor));}); this.tankList.forEach((tank, index) => {// // ❗❗❗ 每次画新的之前先清空画布(包括图片)context[index].clearRect(0, 0, bgimgWidth, bgimgHeight); // // 3. 画油和水的矩形块,然后画背景图片const rectStartPointX = bgimgWidth * 0.15;const rectWidth = bgimgWidth * 0.1;let yOil = bgimgHeight * (1 - (0.9 * tank.oilRatio) / 100); // 一次性数据,只在传参时用一次,不用定义数组let yWater = bgimgHeight * (1 - (0.9 * tank.waterRatio) / 100); // 一次性数据,只在传参时用一次,不用定义数组// 3.1 画油位矩形context[index].fillStyle = oilColors[index];context[index].fillRect(rectStartPointX, yOil, rectWidth, bgimgHeight);// // 3.3 新版canvas画背景图片,将图片绘制到 canvas 上this.context[index].drawImage(this.bgImg,0,0,bgimgWidth,bgimgHeight); // 4. 写详细信息(放外面会被图片挡住,因为图片加载较慢)// 4.1 设置文字颜色:在线黑色,离线红色const textColor = tank.connect ? "black" : "red";context[index].fillStyle = textColor;// 4.2 写罐号 1context[index].font = "20px sans-serif";context[index].fillText(tank.id, bgimgWidth * 0.06, bgimgHeight * 0.6);});},}, }; </script> <style scoped> /* .canvas样式不需要需改!! */ .canvas {/* border: 1rpx solid black; */width: 750rpx;height: 300rpx;margin-top: 30rpx;margin-left: 37rpx;margin-right: 37rpx; } </style>