1.1 什么是同步/异步任务?
-
同步任务:指的是在主线程上排队执行的任务,只有当前一个任务执行完毕后才可执行下一个任务。
-
异步任务:指的是不进入主线程,而进入任务队列的任务。只有当任务队列通知主线程某个异步任务可以执行,方可进入主线程进行执行。
1.2 为什么会出现异步问题?
-
由于JavaScript是一门
单线程
语言,即同一时间只能做一件事; -
JS本身是同步执行;
-
但在执行耗时操作时
为了避免阻塞后续代码的执行
,通常采用异步操作; -
通过事件循环(event loop)实现异步。
1.3 JS中的任务队列
Javascript 这门脚本语言诞生的使命就是为处理页面中用户的交互,以及操作 DOM 而诞生的。
多线程操作DOM == 乱套
同步任务:简单的任务
异步任务:分为宏任务和微任务
宏任务(macrotask):script, setTimeout ,setInterval ,setImmediate ,I/O,UI rendering
微任务(microtask ):process.nextTick,promise ,MutationObserver
Promise是宏任务(同步执行),但Promise 的回调函数属于异步任务,会在同步任务之后执行(比如说
then
、
catch
、
finally
只有当同步任务都执行完毕之后,才会到任务队列里面执行异步任务。
在任务队列中,也分为宏任务和微任务分别在宏任务队列和微任务队列,只有微任务队列中的任务全部执行完毕后,才会执行宏任务队列里面的任务。
console.log(1)
setTimeout(function () {
console.log(2);
process.nextTick(function () {
console.log(3);
})
})
Promise.resolve().then(function() {
console.log(4)
}).then(function() {
console.log(5)
})
结果:
1
4
5
2
3
面试题
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
script start
VM557:2 async1 start
VM557:7 async2
VM557:15 promise1
VM557:20 script end
VM557:4 async1 end
VM557:18 promise2
VM557:11 setTimeout
主线程中的任务结束之后,取出任务队列的第一个任务,推入执行栈中,重复此步骤这就叫做事件循环
1.4 浏览器的线程
浏览器的渲染进程是多线程的
-
1.GUI渲染线程
-
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
-
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
-
注意,
GUI渲染线程与JS引擎线程是互斥的
,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中
等到JS引擎空闲时
立即被执行。
2.JS引擎线程
-
也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
-
JS引擎线程负责解析Javascript脚本,运行代码。
-
JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
-
同样注意,
GUI渲染线程与JS引擎线程是互斥的
,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3.事件触发线程
-
归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
-
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
-
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
-
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.定时触发器线程
-
传说中的
setInterval
与
setTimeout
所在线程 -
浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
-
因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
-
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5.异步http请求线程
-
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
-
将检测到状态变更时,如果设置有回调函数,异步线程就
产生状态变更事件
,将这个回调再放入事件队列中。再由JavaScript引擎执行。
-
1.5 回调地狱
由多层
嵌套
的回调函数组成的代码称为回调地狱。
function callbackFn(callback){ setTimeout(function(){ callback() },1000)}callbackFn(function(){ callbackFn(function(){ callbackFn(function(){ callbackFn(function(){console.log('回调结束')}) }) })})
回调地狱就是为是实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差,后期不好维护。(强耦合)
三种方案解决回调地狱 Promise Generator async/await
1.6 Promise
ES6 将其写进了语言标准,统一了用法,原生提供了
Promise
对象。
所谓
Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
两个特点:
1.对象的状态不受外界影响。
2.一旦状态改变,就不会再变。
三个状态:
pending
(进行中)、
fulfilled
(已成功)和
rejected
(已失败)
pending—–>resolved :成功数据value
pending—–>rejected :失败原因reason
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise新建后会立即执行
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
Promise.all
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
全部变成
fulfilled
,或者其中有一个变为
rejected
,才会调用
Promise.all
方法后面的回调函数。
Promise.race
const p = Promise.race([p1, p2, p3]);
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
只要
p1
、
p2
、
p3
之中有一个实例率先改变状态,
p
的状态就跟着改变
Promise.allSettled
全部Settled(包含fulfilled、rejected)完成后回调
Promise.any
只要参数实例有一个变成
fulfilled
状态,包装实例就会变成
fulfilled
状态;如果所有参数实例都变成
rejected
状态,包装实例就会变成
rejected
状态。
Promise.resolve
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
1.7 Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
-
function
关键字和函数之间有一个星号(
*
),且内部使用yield表达式,定义不同的内部状态。 -
调用Generator函数后,该函数并不执行
,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
Generator的用途
在JavaScript中,一个函数一旦被执行,就会执行到最后或者被
return
,运行期间
不会被外部所影响打断
,而Generator的出现就
打破了这种函数运行的完整性
。
1.8 async、await
async 函数是 Generator 函数的语法糖。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
等价于
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async
函数的
await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。Promise.resolve(1)
async的返回值是Promise ,进一步说,
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而
await
命令就是内部
then
命令的语法糖。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
//指定50毫秒后 输出hello world
await
命令后面是一个
thenable
对象(即定义了
then
方法的对象),那么
await
会将其等同于 Promise 对象。
实现原理:
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
async 函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()
const a = async () => {
await b();
c();
};
b()
运行的时候,
a()
是暂停执行,上下文环境都保存着。一旦
b()
或
c()
报错,错误堆栈将包括
a()
。(方便查找报错)
async function foo() {
console.log(await Promise.resolve('foo'));
}
async function bar() {
console.log(await 'bar');
}
async function baz() {
console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo
async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。
毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别。
1.9 横向对比三种方法
Promise :
优点:
回调函数的改进。使用then方法。异步任务的多段执行更清楚。
缺点:
Promise的最大问题是代码冗余,请求任务多时,一堆的then,也使得原来的语义变得很不清楚
Generator :
优点:
分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。虽然Generator将异步操作表示得很简洁,但是流程管理却不方便
缺点:
流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)
async:
优点:
1.
方便级联调用
:即调用依次发生的场景。
2.
同步代码编写方式
: Promise使用then()函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯。
3.
多个参数传递
:Promise的then()函数只能传递一个参数,虽然可以通过包装成对象来传递多个参数,但是会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦;async/await没有这个限制,可以当做普通的局部变量来处理,用let或者const定义的块级变量想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余工作。
4.
同步代码和异步代码可以一起编写
: 使用Promise的时候最好将同步代码和异步代码放在不同的then()节点中,这样结构更加清晰;async/await整个书写习惯都是同步的,不需要纠结同步和异步的区别,当然,异步过程需要包装成一个Promise对象放在await关键字后面。
5.
sync/await是对Promise的优化
: async/await是基于Promise的,是进一步的一种优化,不过在写代码时,Promise本身的API出现得很少,很接近同步代码的写法。