第一章、异步:现在与将来
1、js是单线程
浏览器的渲染进程是多线程的,如下:
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
- GUI渲染线程
而js因为防止对DOM的操作产生混乱,因此它是单线程的。单线程就是一次只能只能一个任务,有多个任务的话需要一个个的执行,为了解决异步事件,js引擎产生了Event Loop机制。
1.1 事件循环(EventLoop)
js引擎不是独立运行的,它运行在宿主环境中,我们常见的便是浏览器,但是随着发展,nodej.s已经进入了服务器的领域,js还渗透到了其他的一些领域。这些宿主环境每个人都提供了各自的事件循环机制。
那么什么是事件循环机制呢?js是单线程的,单线程就是一次只能只能一个任务,有多个任务的话需要一个个的执行,为了解决异步事件,js引擎产生了Event Loop机制。js中任务执行时会有任务队列,setTimeout是在设定的时间后加到任务队列的尾部。因此它虽然是定时器,但是在设定的时间结束时,回调函数是否执行取决于任务队列的状态。换个通俗点的话来说,setTimeout是一个“不太准确”的定时器。
直到ES6中,js中才从本质上改变了在哪里管理事件循环,ES6精确得制定了事件循环的工作细节,其中最主要的原因是Promise的引入,这使得对事件循环队列调度的运行能直接进行精细的控制,而不像上面说到的”不太准确“的定时器。
1、宏任务
- 在 JS 中,大部分的任务都是在主线程上执行,常见的任务有:
- 渲染事件
- 用户交互事件
- js脚本执行
- 网络请求、文件读写完成事件等等。
- setTimeout、setInterval
- 为了让这些事件有条不紊地进行,JS引擎需要对之执行的顺序做一定的安排,V8 其实采用的是一种队列的方式来存储这些任务, 即先进来的先执行。
2、微任务
(1)对每个宏任务而言,内部有一个都有一个微任务
(2)引入微任务的初衷是为了解决异步回调的问题
- 将异步回调进行宏任务队列的入队操作。
采用改方式,那么执行回调的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿。
- 将异步回调放到当前宏任务的末尾。
为了规避第一种方式中的这样的问题,V8 引入了第二种方式,这就是微任务的解决方式。在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务。
(3)常见的微任务有:
- MutationObserver
- Promise.then(或.reject) 以及以
- Promise 为基础开发的其他技术(比如fetch API)
- V8 的垃圾回收过程。
我们来看一个常见的面试题:
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
- 先执行同步队列的任务,因此先打印start和end
- setTimeout 作为一个宏任务放入宏任务队列
- Promise.then作为一个为微任务放入到微任务队列
- Promise.resolve()将Promise的状态变为已成功,即相当于本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行
- 接下来进入到下一个宏任务——setTimeout, 执行
再看一个例子:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
});
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0);
console.log('start');
// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
接下来从js异步发展的历史来学习异步的相关知识
第二章、回调函数
回调是js中最基础的异步模式。
2.1 回调地狱
listen("click", function handle(evt){
setTimeout(function request(){
ajax("...", function response(test){
if (text === "hello") {
handle();
} else {
request();
}
})
}, 500)
})
这种代码常常被成为回调地狱, 有时候也叫毁灭金字塔。因为多个异步操作形成了强耦合,只要有一个操作需要修改,只要有一个操作需要修改,它的上层回调函数和下层回调函数就需要跟着修改,想要理解、更新或维护这样的代码十分的困难。
2.2 信任问题
有的回调函数不是由你自己编写的,也不是在你直接的控制下的。多数情况下是第三方提供的。这种称位控制反转,就i是把自己程序的一部分执行控制交给了第三方。而你的代码和第三方工具之间没有一份明确表达的契约。会造成大量的混乱逻辑,导致信任链完全断裂。
第三章、Promise
回调函数的两个缺陷:回调地狱和缺乏可信任性。Promise解决了这两个问题。
3.1 Promise的含义
Promise简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
- Promise对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Fulfilled(已成功)、Reject(已失败)。只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态。
- 一旦状态改变就不会再变,任何时候都可以得到这个结果。Promise的状态改变有两种可能:从Pending到Fulfilled和从Pending到Rejected。状态一旦发生改变就不会再变,会一直保持这个结果。
Promise相当于购餐时的订单号,当我们付钱购买了想要的食物后,便会拿到小票。这时餐厅就在厨房后面为你准备可口的午餐,你在等待的过程中可以做点其他的事情,比如看个视频,打个游戏。当服务员喊道我们的订单时,我们就可以拿着小票去前台换我们的午餐。当然有时候,前台会跟你说你点的鸡腿没有了。这就是Promise的工作方式。
3.2 基本用法
1、Promise
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
var promise = new Promise(function(resolvem reject) {
// some code
if (/*异步操作成功*/) {
resolve(value);
} else {
reject(error);
}
})
- resolve的作用是将Promise对象的状态从“未完成”变成“成功”,在异步操作成功时调用,并将异步操作结果作为参数传递出去
- reject的作用是将Promise对象的状态从“未完成”变成“失败“,在异步操作失败时调用,并将异步操作爆出的错误作为参数传递出去
2、resolve函数、reject函数和then()方法
Promise实例生成以后,可以使用then方法分别指定Resolved状态和Rejected状态的回调函数
promise.then(function(value) {
// success
}, function(error) {
// failure
})
-
then方法接受两个参数:第一个回调函数是Promise状态变为Resolved时调用的,第二个是Promise状态变成Rejected时调用
-
第二个参数是可选的,不一定要提供
-
两个函数都接受Promise对象传出去的值做参数。
-
reject函数传递的参数一半时Error对象的实例,表示抛出错误。
-
resolve函数除了传递正常值以外,还可以传递一个Promise实例
var p1 = new Promise(function(resolve, reject) { //... }); // 这种情况下,p1的状态决定了p2的状态。p2必须等到p1的状态变为resolve或reject才会执行回调函数 var p2 = new Promise(function(resolve, reject) { //... resolve(p1); });
-
3.3 Promise.prototype.then()
then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加改变状态时的回调函数。
-
then方法接受两个参数:第一个回调函数是Promise状态变为Resolved时调用的,第二个是Promise状态变成Rejected时调用
-
then方法返回的是一个新的Promise实例。因此可以采用链式的写法。
promise((resolve, reject) => { // ... }).then(() => { // ... }).then(() => { // ... })
-
采用链式的写法可以指定一组按照次序调用的回调函数。如果前一个回调函数返回了一个Promise实例,那么后一个回调函数就会等待该Promise对象状态的变化再被调用。
promise((resolve, reject) => { // ... }).then(() => { // ... return new Promise((resolve, reject) => { // ... }) }).then((comments) => { console.log("resolved: ", comments) }, (err) => { console.log("rejected: ", err) }) // 或者可以写的更加简洁一些 promise((resolve, reject) => { // ... }) .then(() => new Promise((resolve, reject) => {...}) .then( comments => console.log("resolved: ", comments), err => console.log("rejected: ", err) )
3.4 Promise.prototype.catch()
Promise.prototype.catch()是方法.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/post.json').then((posts) => {
// ....
}).catch((error) => {
console.log("发生错误", error);
})
-
getJSON返回一个Promise对象,如果该对象变成Resolved则会调用then()方法
-
如果异步发生错误或者then方法发生错误,则会被catch捕捉
-
Promise在resolve语句后面再抛出错误不会被捕获,因为Promise的状态一旦改变就不会再改变了。
var promise = new Promise((resolve, reject) => { resolve('ok'); throw new Error('test') }) promise .then((value) => {console.log(value)}) .catch((error) => {console.log(error)})
-
Promise对象的错误具有“冒泡”的性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch捕获。一般来说不要再then中定义第二个函数,而总是用catch方法。
var promise = new Promise((resolve, reject) => { resolve('ok'); throw new Error('test') }) // 不推荐 promise .then( (value) => {console.log(value)}, (error) => {console.log(error)} ) //推荐 promise .then((value) => {console.log(value)}) .catch((error) => {console.log(error)})
-
和传统的try/catch不同,如果没有使用catch指定错误处理的回调函数,promise对象抛出的错误不会传递到外层代码,即不会有任何反应
-
catch返回的也是一个Promise对象,后面还可以跟then
3.5 done()和finally()
1. done()
无论Promise对象的回调链是以then方法结束还是以catch方法结束,只要最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。为此可以提供一个done()方法,他总是在回调链的尾部,保证抛出任何可能出现的错误。
asyncFunc ()
.then(f1)
.catch(f2)
.then(f3)
.done()
它的源码实现很简单:
Promise.prototypr.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function(reason){
// 抛出一个全局错误
setTimeout(() => {throw reason}, 0)
})
}
2. finally()
finally方法用于指定不管Promise对象最后如何都会执行的操作。他与done方法的最大区别在于它接受一个回调函数作为参数,该函数不管怎么样都会执行。来看看它的实现方式。
Promise.prototype.finally = function (callback) {
let P = this.constructor
// 巧妙的使用Promise.resolve方法,达到不管前面的Promise状态是fulfilled还是rejected,都会执行回调函数
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => throw reason)
)
}
3.6 Promise.all()
Promise.all方法用于将多个Promise实例包装成一个新的Promise实例
var p = Promise.all([p1, p2, p3])
-
p1、p2、p3都是Promise实例,如果不是,则会使用Promise.resolve方法,将参数转化为Promise实例,再进行处理
-
该方法的参数不一定是要数组,但必须要有Iterator接口,且每个组员都是Promise实例
-
p的状态由p1、p2、p3决定
- 只有p1、p2、p3的状态都变成Fulfilled,p的状态才会变成Fulfilled,此时p1、p2、p3的返回值组成一个数组传递给p的回调函数
- 只要p1、p2、p3有一个状态变成Rejected,p的状态就会变成Rejected,此时第一个Rejected的实例的返回值传递给p的回调函数
var promises = [2, 3, 4, 5, 6, 7].map((id) => { return getJSON(`/post/${id}.json`) }) Promise.all(promises).then((posts) => { //... }).catch((error) => { //... })
-
如果作为参数的Promise实例自身定义了catch方法,那么它被rejected时并不会出发Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
resolve('hello')
})
.then(result => result)
.catch(e => e)
const p2 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
.catch(e => e)
const p3 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
// p2的catch返回了一个新的Promise实例,该实例的最终状态是resolved
Promise.all([p1, p2])
.then(result => result)
.catch(e => e)
// ["hello", Error: error]
// p3没有自己的catch,所以错误被Promise.all的catch捕获倒了
Promise.all([p1, p3])
.then(result => result)
.catch(e => e)
// Error: error
3.7 Promise.race()
Promise.race方法用于将多个Promise实例包装成一个新的Promise实例
var p = Promise.race([p1, p2, p3])
-
p1、p2、p3都是Promise实例,如果不是,则会使用Promise.resolve方法,将参数转化为Promise实例,再进行处理
-
该方法的参数不一定是要数组,但必须要有Iterator接口,且每个组员都是Promise实例
-
p的状态由p1、p2、p3决定,只要p1、p2、p3有一个实例率先改变状态,p的状态就会跟着改变。率先改变状态的实例的返回值传递给p的回调函数。
3.8 Promise.resolve()
Promise.resolve方法将现有对象转换成Promise对象,分为以下四种情况:
1. 参数是一个Promise实例
Promise.resolve不做任何改变
2. 参数是一个thenable对象
thenable对象是指具有then方法的对象
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
let p1 = Promise.resolve(thenable)
p1.then(function(value) {
console.log(value) // 42
})
Promise.resolve会将这个对象转换成Promise对象,然后立即执行thenable对象的then方法
3. 参数是不具有then方法或根本不是对象
该情况下,Promise.resolve返回一个新的Promise对象,状态为Resolved
var p = Promise.resolve('hello');
p.then((s) => {
console.log(s)
})
// hello
4. 不带任何参数
此情况下,Promise.resolve方法返回一个Resolved状态的Promise对象
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
(1)先执行同步队列的任务,因此先打印start和end (2)setTimeout 作为一个宏任务放入宏任务队列 (3)Promise.then作为一个为微任务放入到微任务队列 (4)Promise.resolve()将Promise的状态变为已成功,即相当于本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行 (5)接下来进入到下一个宏任务——setTimeout, 执行
3.9 Promise.reject()
Promise.reject方法会返回一个新的Promise实例,状态为Rejected
与Promise.resolve不同,Promise.reject会原封不动的将其参数作为reject的理由传递给后续的方法,因此没有那么多的情况分类
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
//true
第四章、Gnerator
Promise解决了回调函数的回调地狱的问题,但是Promise最大的问题是代码的冗余,原来的任务被Promise包装后,无论什么操作,一眼看过去都是许多then的堆积,原来的语义变得很不清楚。
传统的编程语言中早有异步编程的解决方案,其中一个叫做协程,意思为多个线程相互作用,完成异步任务。它的运行流程如下:
- 协程A开始执行
- 协程A执行到一般暂停,执行权交到协程B中
- 一段时间后,协程B交还执行权
- 协程A恢复执行
function *asyncJob () {
// ...
var f = yield readFile(fileA);
// ...
}
它最大的优点就是,代码写法很像同步操作。
4.1 Generator封装异步任务
Generator函数是协程在ES6中最大的实现,最大的特点就是可以交出函数的执行权。
整个Generator函数就是一个封装的异步任务容器,异步操作需要用yield表明。Generator他能封装异步任务的原因如下:
- 暂停和恢复执行
- 函数体内外的数据交换
- 错误处理机制
上面代码的Generator函数的语法相关已经在上一篇博客中总结了,不能理解此处可以前往复习。
Generator函数是一个异步操作的容器,它的自动执行需要一种机制,当异步操作有了结果,这种机制需要自动交回执行权,有两种方法可以做到:
-
回调函数:将异步操作包装成Thunk函数,在回调函数里面交回执行权
-
Promise对象:将异步操作包装成Promise对象,使用then方法交回执行权
4.2 Thunk函数
参数的求值策略有两种,一种是传值调用,另一种是传名调用
- 传值调用,在参数进入函数体前就进行计算;可能会造成性能损失。
- 传名调用,在参数被调用时再进行计算。
编译器的传名调用的实现将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫Thunk函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var Thunk = function () {
return x + 5;
}
function f(thunk) () {
return thunk() * 2
}
1. js中的Thunk函数
js语言是按值调用的,它的Thunk函数含义和上述的有些不同。在js中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
(1)在js中,任何函数,只要参数有回调函数就可以写成Thunk函数的形式。
// ES5
var Thunk = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
return function (callback) {
args.push(callback);
return fn.apply(this, args)
}
}
}
}
// ES6
var Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
// 实例
function f (a, cb) {
cb(a)
}
const ft = Thunk(f);
ft(1)(console.log); // 1
(2)生产环境中使用Thunkify模块
$ npm install Thunkify
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str) {
// ...
})
2. Generator函数的流程管理
前面提到了Thunk可以用于Generator函数的自动流程管理
(1)Generator可以自动执行
function *gen() {
// ...
}
var g = gen();
var res = g.next();
while (!res.done) {
console.log(res.value);
res = g.next();
}
但是这不适合异步操作,如果必须满足上一步执行完成才能执行下一步,上面的自动执行就不可行。
(2)Thunk函数自动执行
var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile);
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen();
// 将同一个函数反复传入next方法的value属性
var r1 = g.next();
r1.value(function(err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
})
})
// Thunk函数自动化流程管理
function run (fn) {
var gen = fn();
function next (err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next)
}
next();
}
run(g)
上述的run函数就是以一个Generator函数自动执行器。有了这个执行器,不管内部有多少个异步操作,直接在将Generator函数传入run函数即可,但是要注意,每一个异步操作都是Thunk函数,也就是说yield后面必须是Thunk函数。
4.3 co模块
co模块不需要编写Generator函数的执行器
var co = require('co');
// gen函数自动执行
co(gen);
// co函数返回一个Promise对象,因此可以用then方法添加回调
co(gen).then(function () {
console.log('Generator函数执行完毕')
})
1. 基于Promise对象的自动执行
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
resolve(data);
})
})
}
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen()
// 手动执行,使用then方法层层添加回调函数
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data)
})
})
// 根据手动执行,写一个自动执行器
function run (gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function (data) {
next(data);
})
}
next();
}
run(gen)
第五章、async函数
ES2017标准引入了async函数,使得异步操作变得更加方便。async函数就是Generator函数的语法糖。
async函数就是将Generator函数的*换成async,将yield换成await。
varasyncReadFile = async function () {
var r1 = await readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = await readFileThunk('/etc/shell');
console.log(r2.toString());
}
async对于Generator的改进有三点:
- 内置执行器:不需要像Generator函数那样引入Thunk函数和co模块来解决自动执行的问题
- 适用性更广:Generator函数中yield后只能跟Thunk函数或者Promise对象,在async函数中可以是Promise对象和原始类型的值(数值、字符串和布尔值,但此之等同于同步操作)
- 返回值是Promise:比Generator函数的返回值是一个Iterator对象方便了很多
1. async函数的声明
// 函数式声明
async function foo() {}
// 函数表达式
const foo = async function() {}
// 箭头函数
const foo = async () => {}
// 对象方法
let obj = { async foo() {} }
obj.foo().then(...)
// class方法
class Storage {
constructor () { ... }
async getName() {}
}
2. 语法
(1)async函数返回一个Promise对象
- async函数内部return语句的返回值,会成为then方法回调函数的参数
async function f() {
return 'hello'
}
f().then(v => console.log(v)) // hello
- async函数内部抛出的错误会导致返回的Promise对象变成reject状态,抛出的错误对象会被catch方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v)
e => console.log(e)
)
// Error: 出错了
- async函数返回的Promise对象必须等到内部所有的await命令后面的Promise对象执行完毕才会发生状态改变,除非遇到return语句或者抛出错误。
(2)await命令
-
正常情况下await命令后面是一个Promise对象,如果不是会被resolve立即转成一个Promise对象
-
await命令后面的Promise对象如果变成reject状态,则reject的参数会被catch方法的而回调函数接收到
-
有时不希望抛出错误终止后面的步骤
- 将await放在try...catch结构里面
- 在await后面的Promise对象后添加一个catch方法
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {
}
return await Promise.resolve('hello')
}
f().then( v => console.log(v)) // hello
async function f1() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello')
}
f1().then( v => console.log(v)) // hello
-
await命令只能在async函数中使用,否则会报错
-
如果await命令后面的异步操作不是继发关系,最好让他们同步触发
let foo = getFoo();
let bar = getBar();
// 写法1
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法2
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
参考资料:
- 偶像神三元的博客
- 阮一峰老师的ES6
- 你不知道的JavaScript(中)