浏览器多线程和js单线程

来源:互联网 发布:飞机工业设计软件 编辑:程序博客网 时间:2024/06/05 13:24

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 => {    })},