javascript 是一门单线程的语言,在同一个时间只能做完成一件任务,如果有多个任务,就必须排队,前面一个任务完成,再去执行后面的任务。作为浏览器端的脚本语言,javascript 的主要功能是用来和用户交互以及操作 dom。假设 javascript 不是单线程语言,在一个线程里我们给某个 dom 节点增加内容的时候,另一个线程同时正在删除这个 dom 节点的内容,则会造成混乱。
由于 js 单线程的设计,假设 js 程序的执行都是同步。如果执行一些耗时较长的程序,例如 ajax 请求,在请求开始至请求响应的这段时间内,当前的工作线程一直是空闲状态, ajax 请求后面的 js 代码只能等待请求结束后执行,因此会导致 js 阻塞的问题。
javascript 单线程指的是浏览器中负责解释和执行 javascript 代码的只有一个线程,即为 js 引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:
为解决上述类似上述 js 阻塞的问题,js 引入了同步和异步的概念。
“同步”就是后一个任务等待前一个任务结束后再去执行。
“异步”与同步不同,每一个异步任务都有一个或多个回调函数。webapi 会在其相应的时机里将回调函数添加进入消息队列中,不直接执行,然后再去执行后面的任务。直至当前同步任务执行完毕后,再把消息队列中的消息添加进入执行栈进行执行。
异步任务在浏览器中一般是以下:
“栈”是一种数据结构,是一种线性表。特点为 LIFO,即先进后出 (last in, first out)。
利用数组的 push 和 shift 可以实现压栈和出栈的操作。
在代码运行的过程中,函数的调用会形成一个由若干帧组成的栈。
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7))
上面代码最终会在控制台打印42,下面梳理一下它的执行顺序。
对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
首先,stack 是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap 是没有结构的,数据可以任意存放。因此,
stack 的寻址速度要快于 heap。
其次,每个线程分配一个 stack,每个进程分配一个 heap,也就是说,stack 是线程独占的,heap 是线程共用的。
此外,stack 创建的时候,大小是确定的,数据从超过这个大小,就发生 stack overflow 错误,而 heap 的大小是不确定的,
需要的话可以不断增加。
public void Method1()
{
int i=4;
int y=2;
class1 cls1 = new class1();
}
上面代码这三个变量和一个对象实例在内存中的存放方式如下。
从上图可以看到,i、y和cls1都存放在stack,因为它们占用内存空间都是确定的,而且本身也属于局部变量。但是,cls1指向的对象实例存放在heap,因为它的大小不确定。作为一条规则可以记住,所有的对象都存放在heap。
接下来的问题是,当Method1方法运行结束,会发生什么事?
回答是整个stack被清空,i、y和cls1这三个变量消失,因为它们是局部变量,区块一旦运行结束,就没必要再存在了。而heap之中的那个对象实例继续存在,直到系统的垃圾清理机制(garbage collector)将这块内存回收。因此,一般来说,内存泄漏都发生在heap,即某些内存空间不再被使用了,却因为种种原因,没有被系统回收。
队列是一种数据结构,也是一种特殊的线性表。特点为 FIFO,即先进先出(first in, first out)
利用数组的 push 和 pop 可实现入队和出队的操作。
事件循环和事件队列的维护是由事件触发线程控制的。
事件触发线程线程同样是由浏览器渲染引擎提供的,它会维护一个事件队列。
js 引擎遇到上文所列的异步任务后,会交个相应的线程去维护异步任务,等待某个时机,然后由事件触发线程将异步任务对应的回调函数加入到事件队列中,事件队列中的函数等待被执行。
js 引擎在执行过程中,遇到同步任务,会将任务直接压入执行栈中执行,当执行栈为空(即 js 引擎线程空闲), 事件触发线程 会从事件队列中取出一个任务(即异步任务的回调函数)放入执行在栈中执行。
执行完了之后,执行栈再次为空,事件触发线程会重复上一步的操作,再从事件队列中取出一个消息,这种机制就被称为 事件循环 (Event Loop)机制。
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
例子代码:
console.log('script start')
setTimeout(() => {
console.log('timer 1 over')
}, 1000)
setTimeout(() => {
console.log('timer 2 over')
}, 0)
console.log('script end')
// script start
// script end
// timer 2 over
// timer 1 over
模拟 js 引擎对其执行过程:
此时,执行栈为空,js 引擎线程空闲。便从事件队列中读取任务,此时队列如下:
注意点:
上面,timer 2 的延时为 0ms,HTML5标准规定 setTimeout 第二个参数不得小于4(不同浏览器最小值会不一样),不足会自动增加,所以 "timer 2 over" 还是会在 "script end" 之后。
就算延时为0ms,只是 time 2 的回调函数会立即加入事件队列而已,回调的执行还是得等到执行栈为空时执行。
在 ES6 新增 Promise 处理异步后,js 执行引擎的处理过程又发生了新的变化。
看代码:
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
这里又新增了两个新的概念, macrotask (宏任务)和 microtask (微任务)。
所有的任务都划分到宏任务和微任务下:
js 引擎首先执行主代码块。
执行栈每次执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的。执行栈中的任务执行完毕后,js 引擎会从宏任务队列中去添加任务到执行栈中,即同样是事件循环的机制。
当在执行宏任务遇到微任务 Promise.then 时,会创建一个微任务,并加入到微任务队列中的队尾。
微任务是在宏任务执行的时候创建的,而在下一个宏任务执行之前,浏览器会对页面重新渲染(task >> render >> task(任务队列中读取))。 同时,在上一个宏任务执行完成后,页面渲染之前,会执行当前微任务队列中的所有微任务。
所以上述代码的执行过程就可以解释了。
js 引擎执行 promise.then 时,promise1、promise2 被认为是两个微任务按照代码的先后顺序被加入到微任务队列中,script end执行后,栈空。
此时当前宏任务(script 主代码块)执行完毕,并不从当前宏任务队列中读取任务。而是立马清空当前宏任务所产生的微任务队列。将两个微任务依次放入执行栈中执行。执行完毕,打印 promise1、promise2。栈空。 此时,第一轮事件循环结束。
紧接着,再去读取宏任务队列中的任务,time over 被打印。栈空。
因此,宏任务和微任务的执行机制如下:
因为,async 和 await 本质上还是基于 Promise 的封装,而 Promise 是属于微任务的一种。所以使用 await 关键字与 Promise.then 效果类似:
setTimeout(_ => console.log(4))
async function main() {
console.log(1)
await Promise.resolve()
console.log(3)
}
main()
console.log(2)
// 1
// 2
// 3
// 4
async 函数在 await 之前的代码都是同步执行的, 可以理解为 await 之前的代码都属于 new Promise 时传入的代码,await 之后的所有代码都是 Promise.then 中的回调,即在微任务队列中。
参考:
原文作者:大芒果哇
原文地址:https://www.cnblogs.com/shenggao/p/13799566.html
览器运行过程中会同时面对多种任务,用户交互事件(鼠标、键盘)、网络请求、页面渲染等。而这些任务不能是无序的,必须有个先来后到,浏览器内部需要一套预定的逻辑来有序处理这些任务,因此浏览器事件循环诞生了,再次强调,是浏览器事件循环,不是javascript事件循环,js只是浏览器事件循环的参与者。
浏览器把任务区分成了 宏任务 和 微任务 或者叫 外部任务 和 内部任务 ,内部任务可以理解为js内部处理的任务,外部任务可以认为是浏览器处理的任务。
也可以叫宏任务队列,浏览器中的外部事件源包含以下几种:
这些外部事件源可能很多,为了方便浏览器厂商优化,HTML标准中明确指出一个事件循环有一个或多个外部队列,而每一个外部事件源都有一个对应的外部队列。不同的时间源之间可以有不同的优先级(例如在网络时间和用户交互之间,浏览器可以优先处理鼠标行为,从而让用户感觉更加流畅)。
也可以叫微任务队列,指的就是javascript语言内部的事件队列,在HTML标准中,并没有明确规定这个队列的事件源,通常认为有以下几种:
以上三种除了第一个,其他两个可以认为没有,实际上我们js中能够使用的就只有promise。
先来一张事件循环处理模型的截图:
可以看出,每一个事件循环,从外部任务队列中拿出一个来执行,执行完一个外部任务后立即执行内部任务队列中所有内部任务(清空),然后浏览器执行一次渲染,然后再次循环。
了解了两种队列和事件循环的执行模型,下面来一段经典代码:
// 以下代码会得到什么样的输出结果?
console.log('1');
setTimeout(function() {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
});
}, 0);
Promise.resolve().then(function() {
console.log('4');
}).then(function() {
console.log('5');
});
console.log('6');
答案是:164523
执行顺序如下:
对于两者的区别,来张瞟来的截图:
这个例子的代码如下:
setTimeout(()=>{
console.log('1');
Promise.resolve().then(function() {
console.log('2');
});
});
setTimeout(()=>{
console.log('3');
Promise.resolve().then(function() {
console.log('4');
});
});
这段代码在浏览器和nodejs中的输出结果分别是什么呢?
通过前面对浏览事件循环的了解,你应该很容易得出在浏览器中的输出结果是: 1234
那在nodejs中的输出结果是什么呢?结果是在nodejs的 v11.x 之前输出1324。这之间的原因是浏览器有非常多的用户交互事件,为了用户体验更加流畅,必须均匀的处理宏任务和微任务,而在nodejs中由于并没有用户交互事件,为了保证异步事件能够被均等的执行,因此设计的初衷就是先清空宏任务队列再清空微任务队列。
不过你应该注意到,我上面只说了在 nodejs的 v11.x 之前输出1324,但是nodejs这个特性在社区经历了一波开发者的吐槽之后,node官方在 v11 这个版本紧急修复了这个问题。所以在 v11.x 以上版本执行以上代码会得到在浏览器中一样的结果。
先来张瞟来的截图:
我们再来一个例子:
setTimeout(()=>{
console.log('1');
Promise.resolve().then(() => console.log('2'));
});
setTimeout(()=>{
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
setImmediate(() => {
console.log('5');
Promise.resolve().then(() => console.log('6'));
});
setImmediate(() => {
console.log('7');
Promise.resolve().then(() => console.log('8'));
});
以上代码在nodejsV13.x中的执行结果是12345678,接下来我们把顺序调换一下,在第二个位置插入setImmediate
setTimeout(()=>{
console.log('1');
Promise.resolve().then(() => console.log('2'));
});
setImmediate(() => {
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
setTimeout(()=>{
console.log('5');
Promise.resolve().then(() => console.log('6'));
});
setImmediate(() => {
console.log('7');
Promise.resolve().then(() => console.log('8'));
});
执行结果有一定的概率是12347856,也有一定的概率是 12563478
为啥不同的顺序会得到不同的结果呢?这是由于setTImeout的精度问题导致的,到了这个级别的时间精度,代码执行的时间可能都会导致结果的不同。下面这张截图是nodejs官方文档对于事件循环顺序的展示:
其中timers阶段是用于执行setTimeout事件的,check阶段是用于执行setImmediate事件的。Nodejs官方这个所谓事件循环过程,其实只是完整的事件循环中Node.js的多个外部队列相互之间的优先级。setTimeout是由event loop检测系统时间是否到点然后向时间队列插入一个事件,然后调用事件的回调方法。而setImmediate是监控UI线程的调用栈,一旦调用栈为空则将回调压栈。
讲了这么多,其实对于上面setTimeout和setImmediate的对比结果还是有点模糊
推测:对于setImmediate的延时有时比setTimeout的要长,由于setImmediate要先监控调用栈,若调用栈为空才压栈,那么在压栈之前event loop已经将setTimeout事件的回调函数压栈了。
好了,以上是这次分享的所有内容,对于后面setTimeout和setImmedate的对比没有的出一个明确的结果,有兴趣的可以一起讨论。
编写程序时,我们经常需要重复执行某些操作,这时候循环结构就显得非常有用。JavaScript 提供了多种循环结构,以适应不同的编程场景。以下是 JavaScript 中常见的循环结构:
for 循环是最常见的循环结构之一,它允许我们指定循环开始前的初始化代码、循环继续的条件以及每次循环结束时要执行的代码。
for (初始化表达式; 循环条件; 循环后的操作表达式) {
// 循环体代码
}
for (var i = 1; i <= 10; i++) {
console.log(i);
}
while 循环在给定条件为真时将不断循环执行代码块。与 for 循环不同,while 循环只有循环条件,没有初始化和迭代表达式。
while (条件) {
// 循环体代码
}
var i = 1;
while (i <= 10) {
console.log(i);
i++;
}
do...while 循环和 while 循环类似,但它至少会执行一次循环体,无论条件是否为真。
do {
// 循环体代码
} while (条件);
var i = 1;
do {
console.log(i);
i++;
} while (i <= 10);
for...in 循环用于遍历对象的属性。
for (var key in 对象) {
// 使用 key 访问对象属性
}
var person = {
name: "张三",
age: 30,
job: "软件工程师"
};
for (var key in person) {
console.log(key + ": " + person[key]);
}
for...of 循环用于遍历可迭代对象(如数组、字符串等)的元素。
for (var item of 可迭代对象) {
// 使用 item 访问元素
}
var fruits = ["苹果", "香蕉", "橘子"];
for (var fruit of fruits) {
console.log(fruit);
}
JavaScript 的循环结构为我们提供了强大的工具来处理重复任务。for 循环适合于当我们知道循环次数时使用;while 和 do...while 循环适合于循环次数未知,但是循环条件明确的情况;for...in 和 for...of 循环则让对象和数组的遍历变得更加简洁。掌握这些循环结构有助于我们编写更加高效和可读性更强的代码。
*请认真填写需求信息,我们会在24小时内与您取得联系。