js-异步编程、事件环

js 是把一个单线程压榨到极致的好语言,谁赞成,谁反对

*gpt4:我反对!

在早期的异步编程中,使用 setTimeout 的方法进行异步。

setTimeout(() => {console.log("兄弟你好")}, 3000);
console.log("你会立刻看到我");

但这种 setTimeout 是嵌套的形式且不可避免,会导致回调地狱。

在 Promise 代替后,对于 Promise 类型,成功则走 then 语句,并处理成功的结果。

fetch(".../posts/1").then((response) => {
	// ...
});

Promise 较于 setTimeout 的好处是,匿名函数返回的 Promise 类型可作为 then 的返回值,意味着可以链式调用:

fetch(".../posts/1").then((response) => response.json());
// .json() 也是 Promise 类型

.then() 方法接受的第一个函数参数是用于处理 fulfilled 状态的,第二个函数参数(可以忽略)是用于处理 rejected 状态的。比如:

codewrappedPromise.then(
    value => console.log('Fulfilled with:', value),
    reason => console.log('Rejected with:', reason)
);

当我们谈论"resolve"一个 Promise,我们指的是将 Promise 从挂起状态转变为已完成状态,无论它是 fulfilled 还是 rejected。至于 resolve 包装任何非 Promise.reject 的值都会执行 fulfill,这是必然的,毕竟不是潜在的会返回 reject Promise。resolve 只是打包工具。

let p = Promise.resolve(42);
p.then(value => console.log(value)); // 输出: 42

使用 catch 来处理错误,以及 finally(()=>{}):

fetch(".../posts/1").then((response) => response.json()).then((json) => {console.log(json);}).catch((error) => {console.error(error);});

async 标记的函数,返回的是 promise。

async function myAsyncFunction() {
  // 没有 return 语句
}

myAsyncFunction().then(value => {
  console.log(value);  // 输出:undefined
});

这里拿一些例子强化 async。

async function bb() {
 return Promise.resolve('别bb, 专心学习');
}

bb().then(value => {
 console.log(value); //打印出 promise 类型
});

await 后面一定是 promise 对象

image-20231031171450310

下面这个异步没问题。fetch 是顺序调用,但是网络可并不一定能顺序返回。

image-20231031171919810

对于下面这段代码

async function asyncFunction() {
 console.log("Start async function");

 let result = await new Promise((resolve) => setTimeout(() => resolve("Resolved after 2 seconds"), 2000));
 console.log(result);

 console.log("End async function");
}

console.log("Before calling async function");

asyncFunction();

console.log("After calling async function");

输出会是:

Before calling async function
Start async function
After calling async function
Resolved after 2 seconds
End async function

尽管 asyncFunction 内的代码因为 await 而“暂停”了,主线程仍然没有被阻塞,因此 "After calling async function" 能在 Promise 解析之前被打印出来。

背后这种"神奇"的工作原理是基于事件循环和回调队列的。当你使用 await.then() 来处理异步操作时,你实际上是在告诉 JavaScript:“当这个异步操作完成时,执行这个回调”。而在此期间,主线程可以继续执行其他任务。

准确的说,async 函数内部并没有“阻塞”(在传统的意义上)。当我们说代码“阻塞”时,我们通常意味着代码会暂停执行,直到某个操作完成。但在JavaScript的异步模型中,使用 await 关键字并不会“阻塞”整个线程。相反,它只是“暂停”了 async 函数的进一步执行,直到 Promise 完成。

其实就是 async A() 函数中的 await 暂停了当前的 A,并等待 A 中的 await 后的 promise 执行。但是不妨碍主线程,比如调用 A() 后的其他同步任务

当你在 async 函数 A() 中使用 await 关键字时,这里是发生的事情的简化描述:

  1. await 会使 A() 函数“暂停”在那个点,等待后面的 Promise 解析。
  2. 但这种“暂停”并不会阻止或影响外部的主线程或其他函数。
  3. 主线程(和其他任何在调用 A() 之后的代码)会继续运行。
  4. 一旦 await 后面的Promise被解析,A() 会继续从它“暂停”的地方运行。

最后的最后,当使用 await 关键字等待的 Promise 解决时,随后的代码(即 await 之后的代码)会被置入微任务队列,而不是立即执行。这就符合事件环的运作逻辑了。

js-事件环

下面基本都来自一个演说。放 reference 里了。主持人的开场白。这个就是调换后的。实际跟调换前没差别。

QQ截图20231029014936

简单来说,这个是在 js 块下的一段命令。一个 js 块是一个宏任务,这里既没有 requestAnimationFrame(rAF),也没有微任务。所以执行完宏任务,才会去渲染。具体可以看后面。

关于基本的 DOM 操作(大概性能很差)介绍如下:

<!DOCTYPE html>
<html>
<body>

<p id="demo"></p>

<script>
var parser, xmlDoc;
var text = "<bookstore><book>"+
 "<title>Everyday Italian<!-- This is a comment --></title>"
+
"<author>Giada De Laurentiis</author>" +"<year>2085</year>" +
"</book></bookstore>";
parser = new DOMParser();
xmlDoc = parser.parseFromString(text, "text/xml");
document.getElementById("demo").innerHTML =
xmlDoc.getElementsByTagName("title")[0].childNodes[0].nodeValue;
</script>

</body>
</html>

上面是 html 文件。首先用 DOMParser 将 xmlDOM 建立起来并解析。

xmlDoc.getElementsByTagName("title")[0] // 获取所有 <title> 从左到右第 0 个
.childNodes[0] // title 下的第 0 个子节点元素。文本(Everyday Italian)、注释(<!-- This is a comment -->)都是一个节点
.nodeValue // 获取节点的值,此时取到了 Everyday Italian
document.getElementById("demo").innerHTML = ... // 将该值写到 html DOM 中

常见的 setTimeout 回调的函数,js 块内(非微任务和非 rAF 的指令集合)都是宏任务。

setTimeout(()=>{
               },// 我是宏任务!
   1000       );

根据这个圈,宏任务一旦卡死,整个系统都会卡死。

QQ截图20231029021947

比如来个:

image-20231031124410000

然后就到了完整的事件环。右边是 Styles calculation、Layout、Paint。

QQ截图20231029085808
image-20231031132059877

然后是 rAF 的使用。注意,rAF 的栈跟宏任务不太一样,完成当前栈所有才开始 SLP 渲染。(但是新的不算,可以理解成当前栈消耗完,新产生的 rAF 才会入栈)。

QQ截图20231029092946

像这样(黄色部分)。

QQ截图20231029095314

而宏观任务是下面这样。

QQ截图20231029095410

所以下面这个,就一宏任务,一次渲染,开销为 1 次渲染。

QQ截图20231029100042

所以下面这个只会从 0 到 500,用 1s 过渡。

QQ截图20231029100248

解决方案是下方。click 事件触发回调函数,box1.style.transform = 'translatex(400px)'; 构成了该回调函数的全部。借由第一次 rAF 帮助渲染,到了 400px 后,第二次 rAF 渲染了 200px 和 1s 的移动,完成预定任务。

const box1 = document.getElementById('box1');
const button = document.getElementById('button');
button.addEventListener('click', () => {
	box1.style.transform = 'translateX(400px)';
  
  
  requestAnimationFrame(() => {
  	requestAnimationFrame(() => {
      box1.style.transform = 'translateX(200px)';
      box1.style.transition = '1s';
    })
  })
})

另一种方式是 getComputedStyle(box).transform,可以强刷,但是会涉及重排什么损耗性能的事情。

image-20231031143148335

然后到了微任务。下面这个会卡死。

QQ截图20231029133707

因为微任务,只要栈不空,就会一直执行。

QQ截图20231029143631

下面的宏任务:

Promise.resolve();
comsole.log('Yo!');

微任务:

console.log('Hey!');
QQ截图20231029130835

再来一个综合训练:

QQ截图20231029144813

再来一个综合训练:

QQ截图20231029144905

结尾彩蛋

*gpt4:那么,是否可以说 JavaScript 是单线程设计中的最佳语言呢?

广泛使用: JavaScript 是唯一可以在所有主流浏览器中运行的脚本语言,因此它成为了 Web 开发的标准。

非阻塞异步编程: 通过事件循环,JavaScript 可以在单线程中执行 I/O 操作而不被阻塞。

社区与生态: JavaScript 拥有一个庞大的开发者社区和丰富的库/框架选择。

然而,从编程语言的设计和特性上讲,JavaScript 也存在一些缺陷:

历史包袱: JavaScript 的某些部分(如类型转换)在设计时并不完美,可能导致开发者混淆。
单线程限制: 对于计算密集型任务,单线程模型可能不是最佳选择。虽然有了 Web Workers,但它们并不共享内存,因此并不等同于传统意义上的多线程。
不是唯一选择: 对于单线程环境,还有其他语言和运行时也很合适。例如,Lua 在嵌入式系统和游戏开发中很受欢迎。
总的来说,JavaScript 在单线程浏览器环境中表现得相当出色,并且随着 Node.js 的出现,它也被用于服务器端开发。但是否将其视为“最佳”取决于具体的应用场景和需求。其他语言在特定环境和用途中可能更适合。

reference

https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=1592s