欢迎大佬们点开阅读原文,点个 star~
这是我的第一篇文章,也是我工作一年后的新征程。作者是 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 更新让原本就就已经低效的性能雪上加霜。
既然 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 的理念,我也去搞个分片吧。于是将原有的逻辑拆分成两步。
想了一下我想起了代理模式 设计一个 Cards 类,有 4 个属性
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;
}
}
这个问题非常重要,但是我并不是科班出身,我百度了好久都没有找到相关说明的文章,只能试着说明了。 这就设计到计算机基础了,最早 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 地址https://github.com/liubon/vue-worker-demo)
第一次尝试写文章,不足之处请见谅,存在问题欢迎指正~