早期的 Web 应用中,与后台进行交互时,须要进行 form
表单的提交,而后在页面刷新后给用户反馈结果。在页面刷新过程当中,后台会从新返回一段 HTML 代码,这段 HTML 中的大部份内容与以前页面基本相同,这势必形成了流量的浪费,并且一来一回也延长了页面的响应时间,老是会让人以为 Web 应用的体验感比不上客户端应用。html
2004 年,AJAX 即“Asynchronous JavaScript and XML”技术横空出世,让 Web 应用的体验获得了质的提高。再到 2006 年,jQuery 问世,将 Web 应用的开发体验也提升到了新的台阶。编程
因为 JavaScript 语言单线程的特色,不论是事件的触发仍是 AJAX 都是经过回调的方式进行异步任务的触发。若是咱们想要线性的处理多个异步任务,在代码中就会出现以下的状况:promise
getUser(token, function (user) {
getClassID(user, function (id) {
getClassName(id, function (name) {
console.log(name)
})
})
})
复制代码
咱们常常将这种代码称为:“回调地狱”。浏览器
众所周知,JavaScript 的运行时是跑在单线程上的,是基于事件模型来进行异步任务触发的,不须要考虑共享内存加锁的问题,绑定的事件会按照顺序齐齐整整的触发。要理解 JavaScript 的异步任务,首先就要理解 JavaScript 的事件模型。markdown
因为是异步任务,咱们须要组织一段代码放到将来运行(指定时间结束时或者事件触发时),这一段代码咱们一般放到一个匿名函数中,一般称为回调函数。网络
setTimeout(function () {
// 在指定时间结束时,触发的回调
}, 800)
window.addEventListener("resize", function() {
// 当浏览器视窗发生变化时,触发的回调
})
复制代码
前面说过回调函数的运行是在将来,这就说明回调中使用的变量并非在回调声明阶段就固定的。闭包
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log("i =", i)
}, 100)
}
复制代码
这里连续声明了三个异步任务,100毫秒 后会输出变量 i
的结果,按照正常的逻辑应该会输出 0、一、2
这三个结果。框架
然而,事实并不是如此,这也是咱们刚开始接触 JavaScript 的时候会遇到的问题,由于回调函数的实际运行时机是在将来,因此输出的 i
的值是循环结束时的值,三个异步任务的结果一致,会输出三个 i = 3
。koa
经历过这个问题的同窗,通常都知道,咱们能够经过闭包的方式,或者从新声明局部变量的方式解决这个问题。异步
事件绑定以后,会将全部的回调函数存储起来,而后在运行过程当中,会有另外的线程对这些异步调用的回调进行调度的处理,一旦知足“触发”条件就会将回调函数放入到对应的事件队列(这里只是简单的理解成一个队列,实际存在两个事件队列:宏任务、微任务
)中。
知足触发条件通常有如下几种状况:
上面的这些行为发生时,代码中以前指定的回调函数就会被放入一个任务队列中,主线程一旦空闲,就会将其中的任务按照先进先出的流程一一执行。当有新的事件被触发时,又会从新放入到回调中,如此循环🔄,因此 JavaScript 的这一机制一般被称为“事件循环机制”。
for (var i = 1; i <= 3; i++) {
const x = i
setTimeout(function () {
console.log(`第${x}个setTimout被执行`)
}, 100)
}
复制代码
能够看到,其运行顺序知足队列先进先出的特色,先声明的先被执行。
因为 JavaScript 单线程的特色,定时器其实并不可靠,当代码遇到阻塞的状况,即便事件到达了触发的时间,也会一直等在主线程空闲才会运行。
const start = Date.now()
setTimeout(function () {
console.log(`实际等待时间: ${Date.now() - start}ms`)
}, 300)
// while循环让线程阻塞 800ms
while(Date.now() - start < 800) {}
复制代码
上面代码中,定时器设置了 300ms
后触发回调函数,若是代码没有遇到阻塞,正常状况下会 300ms
后,会输出等待时间。
可是咱们在还没加了一个 while
循环,这个循环会在 800ms
后才结束,主线程一直被这个循环阻塞在这里,致使时间到了回调函数也没有正常运行。
事件回调的方式,在编码的过程当中,就特别容易形成回调地狱。而 Promise 提供了一种更加线性的方式编写异步代码,有点相似于管道的机制。
// 回调地狱
getUser(token, function (user) {
getClassID(user, function (id) {
getClassName(id, function (name) {
console.log(name)
})
})
})
// Promise
getUser(token).then(function (user) {
return getClassID(user)
}).then(function (id) {
return getClassName(id)
}).then(function (name) {
console.log(name)
}).catch(function (err) {
console.error('请求异常', err)
})
复制代码
Promise 在不少语言中都有相似的实现,在 JavaScript 发展过程当中,比较著名的框架 jQuery、Dojo 也都进行过相似的实现。2009 年,推出的 CommonJS 规范中,基于 Dojo.Deffered
的实现方式,提出 Promise/A
规范。也是这一年 Node.js 横空出世,Node.js 不少实现都是依照 CommonJS 规范来的,比较熟悉的就是其模块化方案。
早期的 Node.js 中也实现了 Promise 对象,可是 2010 年的时候,Ry(Node.js 做者)认为 Promise 是一种比较上层的实现,并且 Node.js 的开发原本就依赖于 V8 引擎,V8 引擎原生也没有提供 Promise 的支持,因此后来 Node.js 的模块使用了 error-first callback
的风格(cb(error, result)
)。
const fs = require('fs')
// 第一个参数为 Error 对象,若是不为空,则表示出现异常
fs.readFile('./README.txt', function (err, buffer) {
if (err !== null) {
return
}
console.log(buffer.toString())
})
复制代码
这一决定也致使后来 Node.js 中出现了各式各样的 Promise 类库,比较出名的就是 Q.js
、Bluebird
。关于 Promise 的实现,以前有写过一篇文章,感兴趣能够看看:《手把手教你实现 Promise》。
在 Node.js@8 以前,V8 原生的 Promise 实现有一些性能问题,致使原生 Promise 的性能甚至不如一些第三方的 Promise 库。
因此,低版本的 Node.js 项目中,常常会将 Promise 进行全局的替换:
const Bulebird = require('bluebird')
global.Promise = Bulebird
复制代码
Generator(生成器)
是 ES6 提供的一种新的函数类型,主要是用于定义一个能自我迭代的函数。经过 function *
的语法可以构造一个 Generator
函数,函数执行后会返回一个iteration(迭代器)
对象,该对象具备一个 next()
方法,每次调用 next()
方法就会在 yield
关键词前面暂停,直到再次调用 next()
方法。
function * forEach(array) {
const len = array.length
for (let i = 0; i < len; i ++) {
yield i;
}
}
const it = forEach([2, 4, 6])
it.next() // { value: 2, done: false }
it.next() // { value: 4, done: false }
it.next() // { value: 6, done: false }
it.next() // { value: undefined, done: true }
复制代码
next()
方法会返回一个对象,对象有两个属性 value
、done
:
value
:表示 yield
后面的值;done
:表示函数是否执行完毕;因为生成器函数具备中断执行的特色,将生成器函数当作一个异步操做的容器,再配合上 Promise 对象的 then 方法能够将交回异步逻辑的执行权,在每一个 yeild
后面都加上一个 Promise 对象,就能让迭代器不停的往下执行。
function * gen(token) {
const user = yield getUser(token)
const cId = yield getClassID(user)
const name = yield getClassName(cId)
console.log(name)
}
const g = gen('xxxx-token')
// 执行 next 方法返回的 value 为一个 Promise 对象
const { value: promise1 } = g.next()
promise1.then(user => {
// 传入第二个 next 方法的值,会被生成器中第一个 yield 关键词前面的变量接受
// 日后推也是如此,第三个 next 方法的值,会被第二个 yield 前面的变量接受
// 只有第一个 next 方法的值会被抛弃
const { value: promise2 } = gen.next(user).value
promise2.then(cId => {
const { value: promise3, done } = gen.next(cId).value
// 依次前后传递,直到 next 方法返回的 done 为 true
})
})
复制代码
咱们将上面的逻辑进行一下抽象,让每一个 Promise 对象正常返回后,就自动调用 next,让迭代器进行自执行,直到执行完毕(也就是 done
为 true
)。
function co(gen, ...args) {
const g = gen(...args)
function next(data) {
const { value: promise, done } = g.next(data)
if (done) return promise
promise.then(res => {
next(res) // 将 promise 的结果传入下一个 yield
})
}
next() // 开始自执行
}
co(gen, 'xxxx-token')
复制代码
这也就是 koa
早期的核心库 co
的实现逻辑,只是 co
进行了一些参数校验与错误处理。经过 generator 加上 co 可以让异步流程更加的简单易读,对开发者而言确定是阶段欢喜的一件事。
async/await
能够说是 JavaScript 异步编程的终极解决方案,其实本质上就是 Generator & co 的一个语法糖,只须要在异步的生成器函数前加上 async
,而后将生成器函数内的 yield
替换为 await
。
async function fun(token) {
const user = await getUser(token)
const cId = await getClassID(user)
const name = await getClassName(cId)
console.log(name)
}
fun()
复制代码
async
函数将自执行器进行了内置,同时 await
后不限制为 Promise 对象,能够为任意值,并且 async/await
在语义上比起生成器的 yield 更加清楚,一眼就能明白这是一个异步操做。