js jq 删除动态加载的元素 js异步学习笔记

本文通过一个实际问题引出JavaScript同步异步的概念,详细阐述了JS的同步、异步执行机制,包括GUI线程、JS引擎线程、定时器线程、事件触发线程等。重点讨论了浏览器和Node.js的EventLoop机制,解释了setTimeout和setImmediate的执行顺序,并介绍了process.nextTick在Node.js中的特殊性。最后,总结了JS异步执行的关键点,强调了微任务和宏任务的区别。

场景

因为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')})

在这里插入图片描述

总结

  1. JS的单线程是指主线程只有一个,并不是整个运行环境都是单线程。
  2. JS的异步靠底层的多线程实现。
  3. 不同的异步API对应不同的实现线程。
  4. 异步线程与主线程通讯靠的是Event Loop
  5. 异步线程完成任务后将其放入任务队列
  6. 主线程不断轮询任务队列,拿出任务执行
  7. 任务队列有宏任务队列和微任务队列的区别
  8. 微任务队列的优先级更高,所有微任务处理完后才会处理宏任务
  9. Promise是微任务
  10. Node.js的Event Loop跟浏览器的Event Loop不一样,他是分阶段的
  11. setImmediate和setTimeout(fn, 0)哪个回调先执行,需要看他们本身在哪个阶段注册的,如果在定时器回调或者I/O回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。
  12. process.nextTick不在Event Loop的任何阶段,他是一个特殊API,他会立即执行,然后才会继续执行Event Loop

因个人能力不足难免理解不到位,欢迎大家指点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小明和大树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值