还记得之前的一片关于栅格布局寻位的算法文章吗?
真的在项目中遇到需要大量计算的问题怎么办?
在我看来,至少有两种解法:
一、让后端提供接口,将算法逻辑写在服务端,前端请求返回计算结果即可
这种做法实际是将算法交给后端帮我们执行,因为对比起浏览器,服务器处理大数据的能力强太多了,所以这是一种可行的做法;
二、前端利用web Worker多线程处理
我们知道javascript是单线程的,但浏览器是多线程的,javascript提供的Worker类就可以用来唤起多线程,在特定的场景下,Worker确实可以提升用户体验,就拿开头文章的应用场景吧:
一个页面左侧有一块画布,右侧有一个卡片列表,点击列表某个卡片,弹出这个卡片的属性配置抽屉(drawer),配置完属性后,点击抽屉的确定按钮,将卡片添加到画布上,依次循环操作,形成栅格布局…
如果画布里已有大量的卡片,各卡片位置不确定,产品的需求是:插入卡片时,自动寻位,如果画布前面有足够的空白放下新添加的卡片,则优先添加到前面的空白,否则放置在所有卡片最后一行的行头
因此我们想出了文章开头的两个方法:addNewItem()
与getPositionForNewItem()
但使用过程中有个问题:当点击抽屉确定按钮后,开始执行寻位方法getPositionForNewItem()
,因为其中的大量算法,导致点击确定按钮后页面卡在这里很久都没处理完,需要等待几秒甚至十几秒,体验急剧下降;
于是我们又想了一个解决办法:在用户点击弹出属性抽屉后我们就立马执行寻位算法;
因为用户配置新卡片属性肯定需要不少时间,利用这个时机,我们可以抢先计算出卡片将要放置的位置值(x, y),用户配置完卡片属性后,一点击确定按钮,新卡片马上就会添加到画布,根本不需等待;
最后,还有一个问题,假如寻位算法还没跑完,而用户又点击了确定按钮怎么办呢,答案是继续等待算法执行完,因此还要用到事件监听eventbus,解决用户点击时机的问题,下面代码均有涉及;
好,清楚了问题,直接上代码:
export default {
data(){
return {
layoutData: []
workerFlag: false,
newPosition: null
}
},
methods:{
//开头文章的基础上修改
async addNewItem(item){
item.i = this.layoutData.length//i的值可以根据自己实际情况设置
const {x, y} = window.Worker?await this.awaitWorkerCalcPosition() : this.getPositionForNewCard(item)
item.x = x
item.y = y
//插入到画布
this.layoutData.push( item )
//到此完结
},
awaitWorkerCalcPostion(){
return new Promise((resolve, reject)=>{
if(this.newPosition){
this.workerFlag = false
resolve(this.newPosition)
}else{
this.workerFlag = true
this.$bus.$on("worker", pos =>{
resolve(pos)
})
}
})
},
getPositionForNewCardByWorker(row){
if(window.Worker){
this.workerFlag = false
this.newPosition = null
//文件要放在相对生产环境根目录下,vue项目则放在public目录下,这里是public/js/calcNewPositionWorker.js
const worker = new Worker("js/calcNewPositionWorker.js")
worker.postMessage({...算法需要的参数})
worker.onmessage = (e)=>{
this.newPosition = e && e.data
this.workerFlag && this.$bus.$emit("worker", this.newPosition)
worker.terminate()//关闭worker线程
}
worker.onerror = (e)=>{
worker.terminate()
}
}
},
getPositionForNewCard(newItem){
//参考开头文章
//这里写多一份是为了给没有Worker的浏览器用的
}
}
}
而路径“js/calcNewPositionWorker.js”
对应的文件内容如下:
//onmessage方法是系统自带的,self.onmessage,self可以省略,(跟window对象一个意思)
onmessage = function(e){
//用以接收发送过来的算法需要的参数
const params = e && e.data
const pos = getPositionForNewCard(params)
postMessage(pos)
self.close()
}
function getPositionForNewCard(params){
//具体参考文章开头文章
}
过程写完了,捋一下这几个方法的执行顺序:
首先,用户一点击右侧卡片列表弹出抽屉时,立马执行getPositionForNewCardByWorker()
方法(用户配置属性中…),实例worker的postMessage
方法给calcNewPositionWorker.js
文件发送了一个信息并监听,文件中的算法执行完后执行postMessage()
方法把结果(pos)发回来给worker.onmessage
,赋值给this.newPosition
;
而,用户不知道什么时候配置完属性,当用户点击确定按钮时,执行addNewItem()
方法,走有Worker
的逻辑awaitWorkerCalcPosition()
方法,假如this.newPosition
已经有值,说明算法已经执行完并返回了结果,直接resolve()
返回;没值,说明算法还没执行完,需要等待”worker”事件有返回再resolve()
以上就是利用Worker对象实现多线程的整个过程,在Worker执行过程中,打开浏览器控制台的Memory,你可以看到Javascript VM instance(虚拟机)会多出一个线程;
回顾一下,这种方法是否似曾相识?没错,跟接口的做法殊途同归,将Worker当成接口,就好理解了,所以这是浏览器给前端的“接口”,上面的第一种做法就是这样得来的;
好了,Worker的使用就讲到这里,快去试试吧