采用单线程模式工作的原因
众所周知,JavaScript 是以单线程去执行代码。这与 JavaScript 的设计初衷有关,最早 JavaScript 是运行于浏览器端脚本语言,用于实现页面的动态交互。
而实现页面交互的核心是 DOM 操作,这就决定 JavaScript 必需使用单线程模式模型,否则会出现复杂的线程同步问题。
假定 JavaScript 能在多线程模式下工作,就会出现一个线程修改一个 DOM 元素的操作,另一个线程又删除这个 DOM 元素,这时浏览器就无法明确该以哪个线程的工作结果为准。
所以为了避免这种线程同步的问题,从一开始就设计为单线程式作。
单线程
JS 执行环境中负责执行代码的线程只有一个。同一时间只能做一个任务,当出现多个任务时,只能进行排队依次完成。
优点
- 安全、简单
缺点
- 遇到特别耗时的任务,会阻塞后面的任务。会出现假死的情况。
:::info 解决0耗时任务阻塞任务的问题,JavaScript 将任务的执行模式分为两种:
- 同步
- 异步
:::
同步与异步
是指 运行环境提供的 API 是以同步或异步模式的方式工作
同步 API 如 console.log
异步 API 如 setTimeout同步行为 Synchronous
对应内存顺序执行的处理器指令,每条指令都会严格按照它们出现的顺序来执行。
程序的执行顺序与代码的编写顺序是一致的。
单线程中大多数代码都会以同步模式来执行,这里的同步是意思不是同时执行,而是排队执行。
console.log('global begin');
function bar() {
console.log('bar task');
}
function foo() {
console.log('foo task');
bar();
}
foo();
console.log('global end');
调用栈 ( JavaScript 执行的工作表 )
把所有的代码加载后,在调用栈中压入一个匿名的调用(可以理解为把所有的代码放入匿名的函数中执行)
Synchronous.pptx
异步行为 Asynchronous
相对同步行业,类似于系统中断,即当前进程外部的实体可以触发代码执行。
不会去等待这个任务的结束才开始下一个任务,对于耗时操作都是开启过后就立即往后执行下一个任务
后续逻辑一般会通过回调函数的方式定义
异步模式的缺点:代码的执行顺序混乱
console.log('global begin');
setTimeout(function timer1() {
console.log('timer1 invoke');
}, 1800);
setTimeout(function timer2() {
console.log('timer2 invoke');
setTimeout(function inner() {
console.log('inner invoke');
}, 1000);
}, 1000);
console.log('global end');
Asynchronous & Event loop.pptx
如果调用栈是一个正在执行的工作表,那么消息队列就是待办的工作表。当 JS 执行完成 调用栈 的任务后,就会通过 事件轮询 从 消息队列 中取任务出来继续执行,以此类推。在这过程中随时可以在消息队列中放入一些任务,这些任务会在消息队列当中排队等待 事件轮询。
以住的异步编程模式
回调函数
可以理解为一件你想要做的事情,但你并不知道这件事情依赖的任务什么时候完成。所以最好的办法,把这个事的步骤写到一个函数中交给任务的执行者。
这个任务的执行者知道这个任务是什么时候完成的,那么当任务结束时帮你执行你想做的事。
异步行为是 JavaScript 的基础,但以前实现不理想。早期 JavaScript 只支持定义回调函数来表明异步操作完成。
串联多个异步操作是一个常见的问题,通常需要深度嵌套回调函数(俗称“回调地狱”)来解决。
- 异步返回值
给异步操作提供 一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));
- 失败处理
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调。
- 嵌套异步回调
随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”的代码 维护起来就是噩梦。