0.前言
开发过程中遇到js线程和ui渲染线程互斥问题。导致ui无法正常更新等问题。这些问题的根源就是因为浏览器的多线程和js的单线程引起的。
看本篇博客之前,应该充分理解消息队列,事件循环,同步异步任务等概念。
这些概念以前都知道,也了解多线程的概念。但是当遇到问题的时候,这些东西都被抛到脑后,值得深思。
1.知识点补充
js单线程
js运作在浏览器中,是单线程的,js代码始终在一个线程上执行,此线程被称为js引擎线程。
ps:web worker也只是允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。
但是如果单线程,任务都需要排队。排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
浏览器多线程
1.js引擎线程(js引擎有多个线程,一个主线程,其它的后台配合主线程)
作用:执行js任务(执行js代码,用户输入,网络请求)
2.ui渲染线程
作用:渲染页面(js可以操作dom,影响渲染,所以js引擎线程和UI线程是互斥的。js执行时会阻塞页面的渲染。)
3.浏览器事件触发线程
作用:控制交互,响应用户
4.http请求线程
作用:ajax请求等
5.定时触发器线程
作用:setTimeout和setInteval
6.事件轮询处理线程
作用:轮询消息队列,event loop
所以异步是浏览器的两个或者两个以上线程共同完成的。比如ajax异步请求和setTimeout
同步任务和异步任务
同步任务:在主线程排队支持的任务,前一个任务执行完毕后,执行后一个任务,形成一个执行栈,线程执行时在内存形成的空间为栈,进程形成堆结构,这是内存的结构。执行栈可以实现函数的层层调用。注意不要理解成同步代码进入栈中,按栈的出栈顺序来执行。
异步任务会被主线程挂起,不会进入主线程,而是进入消息队列,而且必须指定回调函数,只有消息队列通知主线程,并且执行栈为空时,该消息对应的任务才会进入执行栈获得执行的机会。
主线程执行的说明: 【js的运行机制】
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
事件循环
主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。
2.js引擎线程(ajax)和UI线程互斥
前提:导出数据,每次从后台请求100条数据,最后拼接成文件。同时,才下载之前修改dom,显示loading效果。导出之后,取消loading效果。
1.考虑每次都要用到响应结果,则使用同步ajax,但dom就被阻塞。
function getData1(){$('.loadingicon').show();$.ajax({url : '',async : false,success: function(data){$('.loadingicon').hide();alert(data);}});}
2.阻塞后,则考虑是js和ui互斥。想用settimeout,则将ajax重开线程,放入事件循环队列。
如果是一个简单的dom则可以,但如果loading是个动图之类的,则无效。因为从循环队列取出,又一次阻塞。
$('').click(function(){$('.loadingicon').show();setTimeout(function(){$.ajax({url : '',async : false,success: function(data){$('.loadingicon').hide();alert(data);}});}, 0);});
3.使用异步和递归==有顺序,不会出现dom阻塞
getDataByChunk(writeToCSV);function getDataByChunk(done){if(firstTime){offsetValue = 0;firstTime = false;} else{offsetValue = get_talent_query.offset + get_talent_query.count;}$.ajax({url: '',type: 'GET',dataType: 'JSON',data: $.extend(true, get_talent_query, {count: 100,offset: offsetValue}),}).done(function (re) {result = result.concat(re.data.users);if(offsetValue + get_talent_query.count >= re.data.total){$(".btn-export-talent").html('导出数据');done(result);}else{getDataByChunk(done)}}).fail(function (e) {});
4.但时间较长,使用promise
// 给每个数据请求都转成promise对象generaterPromise(count, offset){let data = {count: count,offset: offset}return **.then( res => {this.exportData.splice( data.offset, 0, ...res.data.users );});},//下载文件,使用了promise,下载相对的比较快exportCSV(){this.exportData = [];this.exportString = EXPORT_STRING.DOWNLOADING;let count = 100;let times = Math.ceil(this.pageTotal / count );let arr = [];for(let i=0; i< times; i++){arr.push(i);}// 这样使用可以保证generaterPromise函数生成的offset是需要的,如果使用for循环只会得到最后一次的变量let promises = arr.map( ( index )=>{return this.generaterPromise( count, count * index);});Promise.all( promises ).then( res => {this.exportString = EXPORT_STRING.DOWNLOAD;this.writeToCSV( this.exportData )}).catch( error => {})},