开篇记
这是我的第一篇文章,也是我工作一年后的新征程。作者是2019年刚刚毕业的,出身贫寒(普通二本)。亲眼目睹校招神仙打架,不幸流落凡尘(我不配)。现在以外包的形式,在一家金融公司工作。
场景
前端项目为vue技术栈,
业务中遇到这样一个情景,有一个输入框,可以键入或者复制粘贴进一大段带有某种格式的文本,根据格式符号对文本进行分割处理(例如根据‘;’分割对象,根据‘,’分割属性),最终将他们处理成某种格式的对象集合,同时生成预览。
效果大概是这个样子
代码如下
// index.vue
import { sectionSplice, contentSplice } from '@/utils/handleInput';
...
onInput() {
this.loading = true;
const temp = sectionSplice(this.text);
this.cardList = contentSplice(temp).data;
this.loading = false;
},
// @/utils/handleInput
export function sectionSplice(val) {
const breakSymbol = '\n';
let cards = val.split(breakSymbol);
return cards.filter((item) => item != '');
}
export function contentSplice(dataArr, cardId) {
const splitSymbol = ',';
const length = dataArr.length;
const result = {
data: [],
cardId,
};
let item = null;
let time = new Date().getTime();
function maxLength(text) {
if (text && text.length > 1000) return text.substring(0, 1000);
return text;
}
for (let i = 0; i < length; i++) {
item = dataArr[i].split(splitSymbol);
if (item != '') {
result.data.push({
title: maxLength(item[0]),
desc: maxLength(item.slice(1).join(splitSymbol)),
key: time + i,
keydef: time + i + 'keydef',
});
}
}
return result;
}
性能瓶颈
但随着输入内容的增多,以及操作的频繁,很快会遇到性能问题,导致页面卡死。
这是一段2082080字数键入后执行的情况
这是当输入内容比较多是的执行情况,因为再多就卡死了,可以看到整个input回调执行相当耗时,造成性能低下,同时频繁触发vue 更新让原本就就已经低效的性能雪上加霜。
如何优化
引入web-worker
既然input回调高耗时,阻塞后续事件的执行,那我们就引用web-worker开辟新的线程,来执行这部分耗时操作就好了。在这个过程中,因为web-worker的加载方式使得在webpack工程化的项目中造成了困难。我尝试使用worker-loader等方式,但是太多坑了。最终使用了vue-worker,之所以使用this.$worker.run()方法是因为这种方式执行完成后worker会自行销毁。这里附带上
// main.js
import VueWorker from 'vue-worker';
Vue.use(VueWorker);
// index.js
onInput() {
this.loading = true;
const option = [this.text];
this.workerInput = this.$worker
.run(sectionSplice, option)
.then((res) => {
this.handleCards(res);
})
.catch((e) => console.log(e));
},
handleCards(data) {
this.workerCards = this.$worker
.run(contentSplice, [data])
.then((res) => {
this.cardList = res.data;
this.loading = false;
})
.catch((e) => console.log(e));
},
一个线程不够用
但是现实非常残酷的开辟1个新线程之后,这一套处理过程还是非常繁重,只不过阻塞的位置从页面渲染线程换到了新线程。 与是我想到了React Fiber的理念,我也去搞个分片吧。于是将原有的逻辑拆分成两步。
- 开辟1个线程,将整体文本分割成数组
- 将分割好的数组按50的长度分片,为每一个分片开辟线程执行,并将返回结果汇总 一切大功告成之后,又遇到了新的问题,由于分片过程异步,执行中不可终止(vue-worker没有终止功能),分片返回结果时,就可能是过时的内容了。
使用代理
想了一下我想起了代理模式 设计一个Cards类,有4个属性
- SL记录此次任务的分片个数
- count当前已经完成的分片个数
- CardId当前操作id
- list合并后的结果 每次更新操作时,实例一个cards,并传入自增的操作id。当分片任务完成时,调用addCards方法,比对分片id与当前cards实例的CardId如果相同,数组合并,count自增当所有分片全部完成,返回最终结果list。 这样我们解决了不同步的问题。
export default class Cards {
constructor(id, length) {
this.SL = length;
this.count = 0;
this.CardId = id;
}
list = [];
addCards(sid, section) {
if (this.CardId == sid) {
this.count++;
this.list = this.list.concat(section);
}
if (this.count == this.SL) {
return this.list;
} else {
return [];
}
}
empty() {
this.list = [];
}
get() {
return this.list;
}
}
web-worker这么好,可以无限开新线程么?
这个问题非常重要,但是我并不是科班出身,我百度了好久都没有找到相关说明的文章,只能试着说明了。
这就设计到计算机基础了,最早cpu只有一个核心,一个线程,同时只能同时完成一件事情,一心不可二用。但是随着技术的发展,现在的消费级cpu都有16核32线程了,可以理解为三头六臂,同时可以做很多事情。
但是并非有多少线程,就只能开多少线程。以今年热销的英特尔i5 10400为例,这颗cup是6核12线程,12线程指的是最大并行执行的线程数量。其实是可以开辟多余12的线程数,这时cpu就有一个类似js eventloop的调度机制,用于切换任务在空闲线程执行。在这个过程中要消耗物理资源的,如果线程过多,在线程间来回切换的损耗会非常巨大。因此线程开辟,不超过cpu线程数为宜。
并且为什使用了vue-worker就可以绕过那么多在vue环境下使用web worker的坑呢?于是我去看了一下vue-worker的源码。
// https://github.com/israelss/vue-worker/blob/master/index.js
import SimpleWebWorker from 'simple-web-worker'
export default {
install: function(Vue, name) {
name = name || '$worker'
Object.defineProperty(Vue.prototype, name, { value: SimpleWebWorker })
}
}
这。。。。竟然只是把SimpleWebWorker注册成vue插件,好吧,看来vue-worker也大可不必了。于是我基于SimpleWebWorker写了一个worker的执行队列,通过 window.navigator.hardwareConcurrency获取cpu线程信息限制开放线程数不超过cpu线程数,如果获取不到就默认上线是4个线程,毕竟现在都2020年了,在老的机器也都是2核4线程以上的配置了。 但是这种线程的限制方式并不严谨,因为是还有很多其他应用程序在占用线程,但是相对合理不会多度开辟新线程.
import SimpleWebWorker from "simple-web-worker";
export default class WorkerQueue {
constructor() {
try {
this.hardwareConcurrency = window.navigator.hardwareConcurrency;
} catch (error) {
console.log(
"Set 4 Concurrency,because can`t get your hardwareConcurrency."
);
this.concurrency = 4;
}
this.concurrency = 4;
this._worker = SimpleWebWorker;
this.workerCont = 0;
this.queue = [];
}
push(fn, callback, ...args) {
this.queue.push({ fn, callback, args });
this.run();
}
run() {
while (this.queue.length && this.concurrency > this.workerCont) {
this.workerCont++;
const { fn, callback, args } = this.queue.shift();
this._worker
.run(fn, args)
.then((res) => {
callback(res);
this.workerCont--;
this.run();
})
.catch((e) => {
throw e;
});
}
}
}
防抖
虽然引入了worker开辟线程,一定程度上减轻耗阻塞的问题,但是频繁触发Input回调,以及频繁的vue更新还是会影响性能,因此这里引入防抖控制回调执行的频率。给cup一点喘息的时间,让他能够一直跑起来。
if (this.timer) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
clearTimeout(this.timer);
this.timer = null;
}, 2000);
return
}
最终效果
极端情况这是一次性键入的1278531字数的内容,当一次性输入这么多内容时,即便是浏览器的textInput都吃不消了,反而成为了最耗时的事件,而我们的处理过程并未造成卡顿。也就是说理论上当内容足够多,浏览器都吃不消时,我们的事件处理也不会造成卡顿,已经能够满足我们的需求了。
正常大数据量情况,还是使用开头2082080字数文字键入后的执行情况,与优化前进行对比。
优化前
优化后
结尾
附上demo地址github.com/liubon/vue-…
第一次尝试写文章,不足之处请见谅,存在问题欢迎指正