前言
本文首先介绍了异步操作的前置知识及存在的问题。第二、三节分别介绍了ES6引入的两种异步编程解决方案Promise和Generator。
一、异步操作前置知识
详细解释参看:JS运行机制详解 动图详解Event Loop
1. JS是单线程的
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
2. 同步任务 异步任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步执行的运行机制:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
下图是主线程和任务队列的示意图:
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
const a = 2
const b = 3
console.log(a + b) // 同步任务
// 异步任务
setTimeout(() => { // 延迟1s执行
console.log(a + b)
}, 1000)
// 前后端数据分离 前端 <-> 后端 ajax
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)
// 1 3 2
console.log(1)
//不管延迟时间是多少,它都是异步任务,必须等主线程任务执行完成之后再执行
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
//1 3 2
// 伪代码
setTimeout(()=>{
task() // 表示一个任务
}, 2000) // 异步任务,首先进入Event table,等2s之后再进入Event Queue
// 虽然异步任务2s就准备好了,但是还必须等到同步任务执行完成
sleep(5000) // 表示一个很复杂的同步任务,会先于异步任务执行
3. Ajax原理
Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。
Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。
XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
在实现的时候,要考虑兼容性问题
// 封装成一个函数
// ajax的第二个参数是一个回调函数
function ajax(url, callback) {
// 1、创建XMLHttpRequest对象
var xmlhttp
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest()
} else { // 兼容早期浏览器
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
}
// 2、发送请求
xmlhttp.open('GET', url, true) // 指定发送请求的操作
xmlhttp.send() // 发送
// 3、服务端响应
// 监听onreadystatechange 方法
xmlhttp.onreadystatechange = function () {
// 事件处理函数
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText)
// console.log(obj)
callback(obj) // 将响应得到的数据传给回调,回调函数是自己定义并传入的
}
}
}
var url = 'http://musicapi.xiecheng.live/personalized'
ajax(url, res => {
console.log(res)
})
4. Callback Hell
回调函数可规范调用的顺序,但是当代码层层嵌套越写越深,代码的可维护性、可读性都会降低,就会造成Callback Hell,下面将介绍ES6对异步操作的解决方案Promise来解决这类问题。
// 1 -> 2 -> 3
// callback hell
ajax('static/a.json', res => {
console.log(res)
ajax('static/b.json', res => {
console.log(res)
ajax('static/c.json', res => {
console.log(res)
})
})
})
二、Promise (※面试必考)
1. 原理
所谓 Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可 以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 对象有以下两个特点。
- 对象的状态不受外界影响。
Promise 对象代表一个异步操作,有三种状态: pending (进行中)、 fulfilled (已成功)和 rejected (已失败)。 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是“承诺”,表 示其他手段无法改变。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending变为 rejected 。
只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外, Promise 对象提供统一的接口,使得控制异步操作更加容易。 - Promise 也有一些缺点。
① 无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
② 如果不设置回调函数, Promise 内部抛出的错误, 不会反应到外部。
③ 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署 Promise 更好的选择。
2. Promise的用法
Promise的精髓在于对于异步的状态管理
// 状态管理
// resolve 成功
// reject 失败
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('kakaDorothy')
// resolve()
// reject()
// if(){
// resolve()
// }else{
// reject()
// }
// resolve('成功') // 传参到then里面
reject('失败')
}, 1000)
}).then(res => { // then可传入两个函数作为参数,第一个为必须得参数,第二个可省略
// 异步操作成功之后进行的操作
console.log(res)
}, err => {
console.log(err)
})
3. Promise执行顺序
注意,如果Promise内部没有写任何异步操作,那么它是会立即执行的。then方法相当于promise的回调函数(它的微任务),待promise内的函数执行完成便执行。
let p = new Promise((resolve, reject) => {
console.log(1)
resolve() // 成功->可调用then方法进一步操作
})
console.log(2)
p.then(res => { // 想要调用then方法,在promise对象中的resolve或者reject方法是一定要写的
console.log(3)
})
//1 2 3
4. Promise的三种状态
let p1 = new Promise((resolve, reject) => {
resolve(1)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2) // 模拟成功的状态
}, 1000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(3) // 模拟失败的状态
}, 1000)
})
console.log(p1) // resolved
console.log(p2) // pending
console.log(p3) // pending
// 待状态转换完成之后再输出
setTimeout(() => {
console.log(p2)
}, 2000) // resolved
setTimeout(() => {
console.log(p3)
}, 2000) // rejected
p1.then(res => {
console.log(res) // 1
})
p2.then(res => {
console.log(res) // 2
})
p3.catch(err => {
console.log(err) // 3
})
Promise状态形成之后就无法再改变
let p = new Promise((resolve, reject) => {
reject(2)
resolve(1)
})
p.then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// 只输出2,promise状态形成之后就无法再改变
5. 改造回调深渊Callback Hell
封装Ajax请求
// 传入成功回调与失败回调
function ajax(url, successCallback, failCallback) {
// 1、创建XMLHttpRequest对象
var xmlhttp
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest()
} else { // 兼容早期浏览器
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
}
// 2、发送请求
xmlhttp.open('GET', url, true)
xmlhttp.send()
// 3、服务端响应
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText)
// console.log(obj)
successCallback && successCallback(obj)
} else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
failCallback && failCallback(xmlhttp.statusText)
}
}
}
对于重复的方法,将它封装成一个函数,每次调用此方法时,返回一个新的Promise对象。
function getPromise(url) {
return new Promise((resolve, reject) => {
// 在Promise内部调用ajax函数
ajax(url, res => {
resolve(res)
}, err => {
reject(err)
})
})
}
调用getPromise方法(注意需要使用return,否则链式操作可能不起效)
// resolved成功示例
getPromise('static/a.json')
.then(res => {
console.log(res)
return getPromise('static/b.json')
}).then(res => {
console.log(res)
return getPromise('static/c.json')
}).then(res => {
console.log(res)
})
// static文件夹下不存在aa.json,会进入rejected状态
getPromise('static/aa.json')
.then(res => {
console.log(res)
return getPromise('static/b.json')// aa.json读取成功时向b.json发出请求
}, err => {
console.log(err) // Not Found
return getPromise('static/b.json') // aa.json读取失败时也向b.json发出请求
}).then(res => {
console.log(res) // 输出b.json的内容
return getPromise('static/c.json')
}).then(res => {
console.log(res) // 输出c.json的内容
})
// 在最后加上catch方法,如出现错误便会抛出异常,而不会进入内部继续输出
getPromise('static/aa.json')
.then(res => {
console.log(res)
return getPromise('static/b.json')
}).then(res => {
console.log(res)
return getPromise('static/c.json')
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err) // Not Found
})
6. Promise静态方法
- Promise.resolve() 表示成功的状态
- Promise.reject() 表示失败的状态
- Promise.all() 传入一个数组作为参数,数组内的每一个内容都对应一个Promise对象
- Promise.race() 将多个 Promise 实例,包装成一个新的 Promise 实例
6.1 Promise.resolve()、Promise.reject()
// Promise.resolve()
let p1 = Promise.resolve('success')
// console.log(p1)
p1.then(res => {
console.log(res)
})
// Promise.reject()
let p2 = Promise.reject('fail')
console.log(p2)
p2.catch(err => {
console.log(err)
})
function foo(flag) {
if (flag) {
return new Promise(resolve => {
// 异步操作
resolve('success')
})
} else {
// return 'fail' // 因为下面是在then方法中调用此返回字符串方法 所以报错表示not a function
return Promise.reject('fail') // 能够返回一个Promise对象
}
}
foo(false).then(res => {
console.log(res)
}, err => {
console.log(err)
})
6.2 Promise.all()
Promise.all 方法接受一个数组作为参数, p1 、 p2 、 p3 都是 Promise 实例,如果不是,就会先调用 Promise.resolve 方法, 将参数转为 Promise 实例,再进一步处理。( Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p 的状态由 p1 、 p2 、 p3 决定,分成两种情况:
- 只有 p1 、 p2 、 p3 的状态都变成 fulfilled , p 的状态才会变成 fulfilled ,此时 p1 、 p2 、 p3 的返回值组成一个数组,传递给 p 的回调函数。
- 只要 p1 、 p2 、 p3 之中有一个被 rejected , p 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
let p1 = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
console.log(1)
resolve('1成功')
}, 2000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2)
// resolve('2成功')
reject('2失败')
}, 1000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3)
resolve('3成功')
}, 3000)
})
// 传入数组作为参数, 数组内放入Promise对象
Promise.all([p1, p2, p3]).then(res => {
// 需要等数组内的Promise对象都执行完成之后再执行此时的then方法
console.log(res)
}, err => {
// 如果出现错误 便直接进入err错误函数 而不会执行前面的res
console.log(err)
})
6.3 Promise.race()
只要 p1 、 p2 、 p3 之中有一个实例率先改变状态, p 的状态就跟着改变,即只要其中一个promise对象状态转换完成了(无论是成功or失败),便认为整体的p完成了。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
Promise.race 方法的参数与 Promise.all 方法一样,如果不是 Promise 实例,就会先调用Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
Promise.race([p1, p2, p3]).then(res => {
console.log(res)
}, err => {
console.log(err)
})
// 结果为 2失败
6.4 应用场景
const imgArr = ['1.jpg', '2.jpg', '3.jpg']
let promiseArr = []
imgArr.forEach(item => {
promiseArr.push(new Promise((resolve, reject) => {
// 图片上传的操作
resolve()
}))
})
// 控制图片全部上传成功
Promise.all(promiseArr).then(res => {
// 插入数据库的操作
console.log('图片全部上传完成')
})
// 加载图片
function getImg() {
return new Promise((resolve, reject) => {
let img = new Image()
img.onload = function () {
resolve(img)
}
img.src = 'http://www.xxx.com/xx.jpg'
})
}
// 定义定时器,判断两秒内是否成功
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('图片请求超时')
}, 2000)
})
}
// 如果图片为加载成功而定时器超时了->返回超时信息
// 如果两秒内图片加载成功-> 返回图片信息
// 这两个Promise对象中任意一个完成了 整个对象便算完成了
Promise.race([getImg(), timeout()]).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
三、Generator
1. 原理
-
从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 -
形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式, 定义不同的内部状态( yield 在英语里的意思就是“产出”)。
-
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
-
必须调用遍历器对象的 next 方法(next方法可以传递参数),使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
2. Generator的用法
Generator是可以暂停的,需要调用next方法手动执行, yield指令只能在生成器内部使用。
// 普通函数
function foo() {
for (let i = 0; i < 3; i++) {
console.log(i)
}
}
foo()
// Generator
function* foo() {
for (let i = 0; i < 3; i++) {
yield i
}
}
console.log(foo()) // 输出一个Generator对象 但是并不会输出 因为它需要我们手动输出
let f = foo()
console.log(f.next())
console.log(f.next())
console.log(f.next())
console.log(f.next())
// yield指令只能在生成器内部使用
function* gen(args) {
args.forEach(item => {
yield item + 1
})
}
3. next()、yield()
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。 yield 表达式就是暂停标志。
遍历器对象的 next 方法的运行逻辑:
- 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值(暂停时的返回值)。
- 下一次调用 next 方法时(next传递的参数会作为暂停时yield处的返回值。),再继续往下执行,直到遇到下一个 yield 表达式。
- 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value属性值(最后的返回值)。
- 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined 。 需要注意的是, yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行 ,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
yield 表达式与 return 语句既有相似之处,也有区别。
- 相似之处:都能返回紧跟在语句后面的那个表达式的值。
- 区别之处:每次遇到 yield ,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return 语句,但是可以执行多次(或者说多个)yield 表达式。正常函数只能返回一个值,因为只能执行一次 return ;Generator 函数可以返回一系列的值,因为可以有任意多 个 yield 。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(“生成器”)。 Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数。
function* gen(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return x + y + z
}
let g = gen(5)
console.log(g.next()) // 6
console.log(g.next()) // NaN
console.log(g.next()) // NaN
let g = gen(5)
console.log(g.next()) // 6
console.log(g.next(12)) // y=24 8
console.log(g.next(13)) // z=13 x=5 42
// 计数器
function* count(x = 1) {
while (true) {
// x为7的倍数时才暂停
if (x % 7 === 0) {
yield x
}
x++
}
}
let n = count()
console.log(n.next().value) // 7
console.log(n.next().value) // 14
console.log(n.next().value) // 21
console.log(n.next().value) // 28
console.log(n.next().value) // 35
4. Generator异步状态管理
同样以ajax为例:
function ajax(url, callback) {
// 1、创建XMLHttpRequest对象
var xmlhttp
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest()
} else { // 兼容早期浏览器
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
}
// 2、发送请求
xmlhttp.open('GET', url, true)
xmlhttp.send()
// 3、服务端响应
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText)
// console.log(obj)
callback(obj)
}
}
}
// 封装请求方法,调用ajax
function request(url) {
ajax(url, res => {
getData.next(res) // 调用next,使Generator对象继续执行
})
}
//Generator函数
function* gen() {
let res1 = yield request('static/a.json')
console.log(res1) // 返回第一次请求的结果
let res2 = yield request('static/b.json')
console.log(res2) // 返回第二次请求的结果
let res3 = yield request('static/c.json')
console.log(res3) // 返回第三次请求的结果
}
let getData = gen()
getData.next()
参考链接:
http://es6.ruanyifeng.com/#docs/generator
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://juejin.cn/post/6969028296893792286