通过angular/vue/react知识的理解, 结合着浏览器多线程, event loop, 让我们来一起聊聊如何监听dom更新, 在vue中,相信大家对nextTick一定不会陌生,在react中也一定会setState的第二个参数callback陌生,他们的共同点都是页面渲染之后执行, 那么我们如果不在vue和react中使用这2种方式怎么做呢?不用担心,我们还可以使用h5 Mutation Observer,此方式具体用法, 我就不在此赘述啦, 那么如果在h5之前呢? 接下来就是我们今天要说的主题: setTimeout和setInterval(以下通过angularjs代码来解析, 因为早期angularjs框架中,并没有提供类似于vue/react中的那2种方式, 所以比较有代表性)
1.浏览器多线程
2.js引擎线程
3.异步任务
4.回调函数队列(macrotask queue) (event队列,我为什么叫回调函数队列, 是因为想通过这个名字更好的去理解形成这个队列先后顺序的原因)
5.微任务
6.微任务函数队列(microtask queue)
7.js function stack
8.event loop
首先对于js是单线程还是多线程,在这里我就不做解释了,有兴趣的大家可以去了解一下h5 webworker,相信大家就会对js是单线程还是多线程就会有自己的认识了,在这里我们没有用h5 webworker,就只讨论js是单线程处理, js 单线程就是我们平常口中所说的js引擎线程,执行我们所写js的线程;那么我们再谈谈浏览器多线程,浏览器多线程通常我们接触到的有以下几个线程:
1.js引擎线程
2.gui渲染线程 (用来渲染页面UI的线程,与js引擎线程互斥)
3.网络请求线程 (比如我们进行ajax请求的时候运行的线程)
4.定时器线程 (比如运行setTimeout setInterval的线程)
5.浏览器事件线程 (比如我们进行浏览器事件点击的线程)
接着为大家介绍异步任务,引入异步任务的原因就是因为当初js是单线程而不是多线程导致的,因为js是单线程,同步执行代码的,你想假设你进行一个http请求,你必须等这个请求结束,你才可以继续执行后面的代码,这样性能很差;所以在引入异步任务后,达到异步的效果,你进行网络请求,运行在网络请求线程,并不会影响后面的代码执行,达到异步并发的效果,所以我认为异步任务,可以来说是模拟js多线程的产物;
回调函数队列就是基于异步任务产生的,假设我们进行多个异步任务,那么怎么确定哪个异步任务回调函数执行的顺序呢?所以就有了回调函数队列,我们定义这些异步任务的时候,基本上都会带着回调函数,这些异步任务会在各自的线程中执行,那么他们执行好了,就会把回调函数加入到回调函数队列中,注意他们是谁先执行好,谁排到队列的前面,按照先进先出的原则,去从回调函数队列中去取,然后在js引擎线程中去执行,
这个时候就需要引入event loop,浏览器是基于事件驱动的,他会进行event loop,不断的去从回调函数队列中去取,直到回调函数队列取完;接着我们谈谈微任务,比如说浏览器原生的 Promise, object.observe等,而通过这些微任务的回调函数形成了一个微任务回调函数队列,同样按照先进先出的原则去执行。
注意微任务队列和回调函数队列是2个不同的概念,event loop是基于回调函数队列而不是微任务队列。
js function stack 由若干个function形成的一个栈,按照先入后出来执行,你可以理解为js引擎线程执行代码都是按这个原则来执行的,而如何形成这个function栈就要通过下面的解释来看。
首先根据我们的script标签会加入到回调函数队列,进行第一次的event loop,然后会执行js function stack里的代码,执行完js function stack 里面的代码后,会从微任务队列里面去取排在队列前面的微任务回调函数,加入到js function stack, 然后去执行,然后执行完再去从微任务队列中去取这个时候排在队列前面的函数,直到微任务队列里回调函数去玩,再进行下一次event loop,同上,直到回调函数队列取完。
第一次event loop (根据script标签包裹的js代码触发执行)
回调函数队列:setTimeout callbackFunc, ajax callbackFunc
微任务队列:Promise callbackFunc
js function stack:定义的同步代码
||
||定义的同步代码执行完
||
回调函数队列:setTimeout callbackFun, ajax callbackFunc
微任务队列:暂无
js function stack: Promise callbackFunc
||
||回调函数中定义的同步代码执行完
||
回调函数队列:setTimeout callbackFunc, ajax callbackFunc
微任务队列: (本次event loop中,所有微任务会被取完,微任务变空)
js function stack:
第二次event loop
回调函数队列:ajax callbackFun
微任务队列:process.nextTick callbackFunc, Promise callbackFunc(本次event loop中加入新的微任务)
js function stack:setTimeout callbackFunc
||
||回调函数中定义的同步代码执行完
||
回调函数队列:ajax callbackFunc
微任务队列: Promise callbackFunc
js function stack:process.nextTick callbackFunc
||
||回调函数中定义的同步代码执行完
||
回调函数队列:ajax callbackFunc
微任务队列:
js function stack: Promise callbackFunc
||
||回调函数中定义的同步代码执行完
||
回调函数队列:ajax callbackFunc
微任务队列: (本次event loop中所有定义的微任务都已经执行完,微任务队列为空)
js function stack:
第三次event loop 同上
回调函数队列:
微任务队列:
js function stack: ajax callbackFunc
||
||回调函数中定义的同步代码执行完
||
回调函数队列:
微任务队列: (本次event loop中所有定义的微任务都已经执行完,微任务队列为空,并且回调函数队列同样为空,不再进行event loop,除非再次有回调函数加入到回调函数队列中)
js function stack:
至此我们通过下面的例子来理解setTimeout和setInterval:
有时候我们在项目中通过js去动态操作dom,可能dom渲染花的时间有点长,然后我们通过js立刻就要获取这个dom元素,这个时候我们就会获取不到,平常我们的做法是把获取这个dom元素的代码放在setTimeout中,然后就可以正常获取成功,为什么这样可以成功呢?基于我们上文的理解,可以简化为如下过程:
html:
<div id="oee-login" class="login-bg" ng-if="config.showLoginStatus">
<span>login</span>
</div>
js:
$scope.config={
showLoginStatus:false
}
$scope.config.showLoginStatus=true;
var loginElement=document.getElementById('oee-login');
if(loginElement)
{
console.log("dom渲染后";
}else{
console.log("dom未渲染");
}
$timeout(function () {
loginElement=document.getElementById('oee-login')
if(loginElement)
{
console.log("dom渲染后" );
}else{
console.log("dom未渲染");
}
},0)
打印如下:
dom未渲染
dom渲染后
虽然我们设置timeout后再去获取,由上文可得,
此时加入了第二次event loop,而第一次event loop后,gui线程进行了UI更新,此时dom节点就有了,所以我们这个时候再去获取就会获取成功了。
在这里我需要额外提一点,记得当时在做项目的时候,有一个表格是通过js配置完数据后动态生成的,设置完数据后,我就获取dom节点这个时候就同上,第一次event loop没有获取到,然而我设置了$timeout的第二个参数为0,在第二次event loop中
还是没有获取到,根据我的理解认为因为是这个表格dom太复杂,导致在第二次event loop的时候dom还是没有渲染好,然后我就把第二个参数设置的比较大一点,然后在第二次event loop中就获取到了dom节点。
通过上面的例子,我发现了一个问题,就是$timeout的第二个参数,设置多少才能成功获取,可能这次dom复杂度为10(这里便于理解dom复杂度,抽象出来的),你设置延时参数为100ms,下次dom复杂度为30,你该如何设置延时参数呢?
当时开始想法是用for循环如下:
js:
var isCreate=false;
do{
var loginElement=document.getElementById('oee-login');
if(loginElement)
{
isCreate=true;
}
}while(!isCreate);
不过很快就被我否定,根据上文的理解,gui线程与js引擎线程是互斥的,所以在同一次event loop中,dom节点永远都不会有,isCreate永远都是false,所以我们要把获取dom节点放在下一次event loop中,前文我们是通过$timeout来让加入到下一次event loop中,又因为延时参数不好设置,所以有了以下2种解决方案:
一:通过递归的方式:
js:
var isCreate=true;
var loginElement=null;
var timeoutEr=null;
judgeElementStatus();
function judgeElementStatus(){
loginElement=document.getElementById('oee-login');
if(loginElement)
{
if(timeoutEr){
timeout(function(){
judgeElementStatus();
},30);
}
}
二:通过setInterval在angularjs中用interval具体用法这里就不赘述,如下:
js:
var intervalEr=null;
var loginElement=null;
intervalEr=$interval(function(){
loginElement=document.getElementById('oee-login');
if(loginElement)
{
if(intervalEr){
$interval.cancel(intervalEr); //取消隐藏的定时器 ps:在angular开发的时候最好在监听destroy时候取消隐藏的定时器
}
console.log("dom渲染后" );
}
},30);
通过以上得到最终核心点,1.gui线程与js引擎线程是互斥的;2.通过js操作dom元素,我们需要在至少下一次event loop中才可以等到dom渲染成功
以上就是我的理解,欢迎大家一起讨论。