Why异步
保证浏览器正常渲染不堵塞,不影响用户使用
总体介绍
浏览器运行是多进程的,包括主进程、渲染进行、网络进程和GPU进程等等
解析HTML时(渲染进程)用到两大引擎,一个是渲染引擎(用于渲染页面)、一个是JS引擎用于解析JS代码。又JS引擎运行是单线程的,所以渲染和 js引擎是互斥的。这就要求提高JS引擎解析时间,防止长时间堵塞主线程,以提高渲染效率。异步就可用来解决此问题。
渲染引擎的线程运行
浏览器运行渲染进程先利用渲染引擎渲染页面,渲染过程如下:
JS引擎的线程运行
主线程中的任务先进入任务队列中给JS引擎解析,其他线程的任务(优先级如下图)的以回调任务进行队列待JS引擎解析。若JS代码改变了DOM树,渲染任务又进入任务队列中待渲染引擎执行。
主线程就监控检查任务队列中是否任务待执行,有就在主线程中执行,部分任务给其他线程运行(异步任务)【这些任务根据逻辑(如定时3s)将回调函数传到任务队列中,待主线程执行】
异步编程实现
- 回调函数
回调函数是最早也是最基本的异步编程方式。它将需要等待的函数作为参数传递给另一个函数,当等待的操作完成时,再调用这个回调函数。但这种方式可能会导致“回调地狱”(Callback Hell),即多层嵌套的回调函数使得代码难以阅读和维护。
function doSomething(callback) {
setTimeout(() => {
console.log('Doing something...');
callback();
}, 1000);
}
doSomething(() => {
console.log('Done!');
});
- 优点:简单、容易理解
- 缺点:不利于维护,代码耦合高
- 事件监听(采用时间驱动模式,取决于某个事件是否发生):
在JavaScript中,事件监听是一种强大的机制,允许你监听和响应网页上发生的各种事件,如用户点击按钮、输入文本、页面加载完成等。通过为元素添加事件监听器,你可以指定当特定事件发生时应该执行的函数。
基本语法
向元素添加事件监听器的基本语法是使用addEventListener()
方法。这个方法接受至少两个参数:要监听的事件类型(如click
、load
、mouseover
等)和一个当事件发生时会被调用的函数(事件处理函数)。
- 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
- 缺点:事件驱区动型,流程不够清晰
- 发布/订阅(观察者模式)
- 类似于事件监听,但是可以通过‘消息中心',了解现在有多少发布者,多少订阅者
- Promise对象
Promises是ES6中引入的,用于处理异步操作的对象。它代表了一个最终可能完成或失败的操作及其结果值。Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
function doSomething() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Done!');
// 或者在某些情况下使用 reject('Error!');
}, 1000);
});
}
doSomething().then(result => {
console.log(result);
}).catch(error => {
console.error(error);
});
- 优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
- 缺点:编写和理解,相对比较难
- Generator函数
Generators是ES6中引入的,提供了一种执行暂停和恢复代码的机制。虽然它们本身不直接处理异步操作,但可以通过与Promises结合使用(例如通过co库),来实现异步编程。然而,由于async/await
的引入,使得Generators在异步编程中的使用逐渐减少。
Generator 函数是 ES6 引入的一种异步编程解决方案,它允许你编写一个可以暂停执行和恢复执行的函数。这种函数可以通过 function*
声明(注意星号),并且可以使用 yield
关键字来暂停和恢复函数的执行。Generator 函数返回一个遍历器对象(Iterator),因此你可以使用 next()
方法来遍历 Generator 函数内部的每一个状态。
function* generatorFunction() {
yield 'Hello';
yield 'World';
return 'Ending';
}
const generator = generatorFunction();
console.log(generator.next().value); // 'Hello'
console.log(generator.next().value); // 'World'
console.log(generator.next().value); // 'Ending'
console.log(generator.next().done); // true
yield 关键字
yield
表达式是暂停 Generator 函数执行的点。- 每次调用
next()
方法时,Generator 函数会从上次yield
表达式停止的地方开始执行,直到遇到下一个yield
表达式(或return
语句)或函数末尾。 yield
可以返回任何值给next()
方法的调用者。- 如果
yield
表达式后面没有跟值,则next()
方法的value
属性为undefined
。
next() 方法
next()
方法可以接受一个参数,这个参数会被当作上一个yield
表达式的返回值。next()
方法返回一个对象,该对象包含两个属性:value
和done
。value
属性表示yield
表达式的值,done
属性是一个布尔值,表示 Generator 函数是否已经执行完毕。
应用场景
Generator 函数的主要用途之一是处理异步操作,尽管在现代 JavaScript 开发中,async/await
语法已经成为处理异步操作的首选方式。然而,Generator 函数仍然有其用武之地,特别是在需要手动控制异步流程或需要与旧代码库集成
function* fetchData() {
const data1 = yield fetch('https://api.example.com/data1');
const json1 = yield data1.json();
console.log(json1);
const data2 = yield fetch('https://api.example.com/data2');
const json2 = yield data2.json();
console.log(json2);
}
// 辅助函数,用于处理 Generator 函数中的 Promise
function run(gen) {
const it = gen();
function handle(result) {
if (result.done) return result.value;
return result.value.then(data => handle(it.next(data)));
}
return handle(it.next());
}
run(fetchData);
- 优点:函数体内外的数据交换、错误处理机制
- 缺点:流程管理不方便
- async函数
async/await
是ES2017(ES8)中引入的,建立在Promises之上,使得异步代码看起来和同步代码一样。async
函数会隐式返回一个Promise,而await
关键字则用于等待Promise的解决(resolve)或拒绝(reject),并可以从中检索解决的值。
async function doSomething() {
try {
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve('Done!');
}, 1000);
});
console.log(result);
} catch (error) {
console.error(error);
}
}
doSomething();
- 优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
- 缺点:错误处理机制