文章目录
场景
因为bug的原因,需要删除单击后弹窗的第一行,因为是动态加载,代码比较复杂,找不到第一行的源代码,所以决定要在单击事件后删除第一行。

解决
function removePDFclick(){
exportPdf('report1');
$("#report1_pdf_dlg td[style='FONT-SIZE: 13px; COLOR: forestgreen;']:first").remove();
}
图中为单击事件的方法,第一行代码为原始单击事件代码。
在单击事件后面添加,删除。
发现第一次单击,没有效果。之后每次单击,都会减少一行。
通过在删除方法前后获取表格的行数
function removePDFclick(){
exportPdf('report1');
console.log($("#report1_pdf_dlg table tr").length);
$("#report1_pdf_dlg td[style='FONT-SIZE: 13px; COLOR: forestgreen;']:first").remove();
console.log($("#report1_pdf_dlg table tr").length);
}
发现第一次单击时,table并没有加载出来,至于删除其他行的问题,在前面加个判断就好了。
因此现在要让他执行第一行代码后,渲染一下,然后再执行删除。
尝试
function a(参){
原单击事件
b();
}
失败。因为这是一个同步方法,一个宏任务一并执行,并不会在中间进行渲染。
此外网上另一种方法
$(function () {
$.getJSON("url", function (res) {
$("#myDiv").append("<div id = \"divId\">"+res.name+"</div>");
}
});
$(function(){
var a = document.getElementById("divId");
console.log(a.innerHTML);
});
改为
$(function () {
$.getJSON("url", function (res) {
$("#myDiv").append("<div id = \"divId\">"+res.name+"</div>");
printA();
}
});
function printA(){
var a = document.getElementById("divId");
console.log(a.innerHTML);
}
这个方法是将浏览器加载就调用的类改为了同步方法,顺序执行了。
在此处行不通,因为当前环境都是响应方法,只有调用时才会执行。而不是匿名函数,在浏览器加载完成后就执行的。
正解
通过学习js的同步异步,通过setTimeout进行延时,让他第一行代码执行后就结束一次事件循环,第二次执行宏任务再删除。代码如下:
function removePDFclick(){
exportPdf('report1');
setTimeout(function(){
if($("#report1_pdf_dlg table tr").length==3){$("#report1_pdf_dlg table tr:first").remove();}
},100);
}
js的同步和异步学习笔记
参考js执行完一件事 之后再去执行下一件事_setTimeout和setImmediate到底谁先执行
同步
const syncFunc = () => {
const time = new Date().getTime();
while(true) { if(new Date().getTime() - time > 2000) {
break;
}}
console.log(2);}
console.log(1);
syncFunc();
console.log(3);

异步
const asyncFunc = () => {
setTimeout(() => { console.log(2); }, 2000);
}
console.log(1);
asyncFunc();
console.log(3);

异步不会影响主线程事件的执行
JS异步的实现
js是单线程的,即js主执行线程只有一条。
js的运行环境浏览器如谷歌内核,是多线程/多进程的

每个页面都有各自的线程。
GUI线程
GUI线程就是渲染页面的,他解析HTML和CSS,然后将他们构建成DOM树和渲染树就是这个线程负责的。
JS引擎线程
负责执行js的主线程,这个线程和GUI线程互斥,因为GUI和js线程都可以操作dom,同时进行会混乱,因为js线程不要做长时间的操作,否则会导致页面卡死。
定时器线程
setTimeout和setInterval所在的线程,也是单线程的js能实现异步的原因。
事件触发线程
定时器线程只是计时,真正的执行不在他,当时间到时,他会把事件交给js主线程,事件触发线程将事件加入处理队列,最后拿出来处理。其他满足条件的事件也是在这个线程中执行。
异步HTTP请求线程
这个线程负责处理异步的ajax请求,当请求完成后将事件交给事件触发线程,js主线程调用。
js的异步执行靠的就是浏览器的多线程,当主线程遇到异步线程,他会把异步线程交给对应的线程,当条件满足时,交给事件触发线程的队列,然后由js主线程执行。这个队列就是EventLoop
Event Loop
事件循环,js执行事件的流程。
js执行循环的环境有浏览器和node.js。
浏览器的Event Loop
事件循环就是各种异步线程通信和协作的循环机制。他们还有公共的数据区,就是事件队列。
异步线程执行完后,会将回调事件由事件触发线程放入js主线程,当主线程忙完手上的活后,会看一眼事件队列是否有事件需要执行。

主线程执行先看是同步任务还是异步任务,同步直接执行,异步交给对应线程,对应线程执行完成后将回调函数放入事件队列。
当主线程执行完同步线程后,看队列中是否有异步的回调函数事件,有则执行。
定时器不准
Event Loop的这个流程有问题,最典型的问题就是总是先执行同步任务,然后再执行事件队列中的回调。所以再次强调,写代码时一定不要长时间占用主线程。
引入微任务
事件队列事件分为两类,一类是宏任务,一类是微任务。微任务优先级更高,执行宏任务时会检查微任务,有会全部执行。

一个事件循环可以有多个事件队列但是只有一个微任务队列。
微任务全部执行完成/宏任务完成一个会重新渲染一次。
requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列。
常见宏任务:
- script (可以理解为外层同步代码)
- setTimeout
- setInterval
- setImmediate(Node.js)
- I/OUI事件postMessage
常见微任务 - Promiseprocess
- .nextTick(Node.js)
- Object.observe
- MutaionObserver
console.log('1');
setTimeout(() => { console.log('2');},0);
Promise.resolve().then(() => { console.log('5');})
new Promise((resolve) => { console.log('3'); resolve();}).then(() => { console.log('4');})
1,3,5,4,2
2因为setTimeout进入了宏队列
3因为promise进入微任务队列
在promise构造函数中同步执行4
在then中会进入微任务
Node.js的Event Loop
Node.js是运行在服务端的js,虽然他也用到了V8引擎,但是他的服务目的和环境不同,导致了他API与原生JS有些区别,他的Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop也是不一样的。Node的Event Loop是分阶段的,如下图所示:

timers: 执行setTimeout和setInterval的回调pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调idle, prepare: 仅系统内部使用poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。check: setImmediate在这里执行close callbacks: 一些关闭的回调函数,如:socket.on(‘close’, …)
每个阶段都有一个自己的先进先出的队列,只有当这个队列的事件执行完或者达到该阶段的上限时,才会进入下一个阶段。
在每次事件循环之间,Node.js都会检查它是否在等待任何一个I/O或者定时器,如果没有的话,程序就关闭退出了。
我们的直观感受就是,如果一个Node程序只有同步代码,你在控制台运行完后,他就自己退出了。
还有个需要注意的是poll阶段,他后面并不一定每次都是check阶段,poll队列执行完后,如果没有setImmediate但是有定时器到期,他会绕回去执行定时器阶段:

setImmediate和setTimeout
上面的这个流程说简单点就是在一个异步流程里,setImmediate会比定时器先执行,我们写点代码来试试:
console.log('outer');
setTimeout(() => {
setTimeout(() => { console.log('setTimeout'); }, 0);
setImmediate(() => { console.log('setImmediate'); });
}, 0);
外层是一个setTimeout,所以执行他的回调的时候已经在timers阶段了处理里面的setTimeout,因为本次循环的timers正在执行,所以他的回调其实加到了下个timers阶段处理
里面的setImmediate,将它的回调加入check阶段的队列外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下到了check阶段,发现了setImmediate的回调,拿出来执行然后是close callbacks,队列时空的,跳过又是timers阶段,执行我们的console
上面console.log(‘setTimeout’)和console.log(‘setImmediate’)都包在了一个setTimeout里面,如果直接写在最外层会怎么样
console.log('outer');
setTimeout(() => { console.log('setTimeout');}, 0);
setImmediate(() => { console.log('setImmediate');});

再来一次

node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)
外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段。
遇到setImmediate塞入check阶段。同步代码执行完毕,进入Event Loop先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过跳过空的阶段,进入check阶段,执行setImmediate回调。
通过上述流程的梳理,我们发现关键就在这个1毫秒,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout执行,如果1毫秒还没到,就先执行了setImmediate。每次我们运行脚本时,机器状态可能不一样,导致运行时有1毫秒的差距,一会儿setTimeout先执行,一会儿setImmediate先执行。但是这种情况只会发生在还没进入timers阶段的时候。像我们第一个例子那样,因为已经在timers阶段,所以里面的setTimeout只能等下个循环了,所以setImmediate肯定先执行。同理的还有其他poll阶段的API也是这样的,比如:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => { console.log('setTimeout');}, 0);
setImmediate(() => {console.log('setImmediate');});
});
这里setTimeout和setImmediate在readFile的回调里,由于readFile是I/O操作,它本身在poll阶段,
所以他的定时器只能进入下一个timer阶段,但是setImmediate可以在下面的check阶段进行。
因此setImmediate比setTimeout先执行。
process.nextTick()
process.nextTick()是一个特殊的异步API,他不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。我们写个例子来看下:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => { console.log('setTimeout'); }, 0);
setImmediate(() => { console.log('setImmediate');
process.nextTick(() => { console.log('nextTick 2'); });
});
process.nextTick(() => { console.log('nextTick 1'); });});
我们代码基本都在readFile回调里面,他自己执行时,已经在poll阶段遇到setTimeout(fn, 0),其实是setTimeout(fn, 1),塞入后面的timers阶段遇到setImmediate,塞入后面的check阶段遇到nextTick,立马执行,输出’nextTick 1’到了check阶段,输出’setImmediate’,又遇到个nextTick,立马输出’nextTick 2’到了下个timers阶段,输出’setTimeout’。
这种机制其实类似于我们前面讲的微任务,但是并不完全一样,比如同时有nextTick和Promise的时候,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。来看个例子:
const promise = Promise.resolve()
setImmediate(() => { console.log('setImmediate');});
promise.then(()=>{ console.log('promise')})
process.nextTick(()=>{ console.log('nextTick')})

总结
- JS的单线程是指主线程只有一个,并不是整个运行环境都是单线程。
- JS的异步靠底层的多线程实现。
- 不同的异步API对应不同的实现线程。
- 异步线程与主线程通讯靠的是Event Loop
- 异步线程完成任务后将其放入任务队列
- 主线程不断轮询任务队列,拿出任务执行
- 任务队列有宏任务队列和微任务队列的区别
- 微任务队列的优先级更高,所有微任务处理完后才会处理宏任务
- Promise是微任务
- Node.js的Event Loop跟浏览器的Event Loop不一样,他是分阶段的
- setImmediate和setTimeout(fn, 0)哪个回调先执行,需要看他们本身在哪个阶段注册的,如果在定时器回调或者I/O回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。
- process.nextTick不在Event Loop的任何阶段,他是一个特殊API,他会立即执行,然后才会继续执行Event Loop
因个人能力不足难免理解不到位,欢迎大家指点。
本文通过一个实际问题引出JavaScript同步异步的概念,详细阐述了JS的同步、异步执行机制,包括GUI线程、JS引擎线程、定时器线程、事件触发线程等。重点讨论了浏览器和Node.js的EventLoop机制,解释了setTimeout和setImmediate的执行顺序,并介绍了process.nextTick在Node.js中的特殊性。最后,总结了JS异步执行的关键点,强调了微任务和宏任务的区别。
1707

被折叠的 条评论
为什么被折叠?



