JS为何有异步?
javascript
是单线程的语言,即一次只能完成一个任务,若有多个任务要执行,则必须按照队列排队完成任务,前一个任务执行完才能执行下一个任务。
那么如果上一个任务不结束,下一个任务就永远得不到执行,或者上一个任务执行很久,前后两个任务没有什么必然的联系,白白浪费了时间在等待。
所以需要异步。
开发中常用的异步操作
- 网络请求
IO
操作readfile readDir
- 定时函数
setTimeout setInterval
等 - 在Node.js中 还有
process.nextTick() setImmediate()
传统的异步解决方案:
- 事件的订阅/发布机制
- 回调函数
发布/订阅机制
简单示例
// 订阅
emitter.on('eventName', function(message){
console.log(message)
})
// 发布
emitter.emit('eventName', 'I am a message')
在Node.js
中 我们可以看到它的应用
var options = {
hostname: '127.0.0.1',
port: 10086,
path: '/pay/pay_callback?' + content,
method: 'GET'
};
var req = http.request(options, function (res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
res.on('end', function() {
})
});
req.on('error', function (e) {
console.log('problem with request: ' + e.message);
});
req.end();
上面是一个请求接口的过程, 作为开发者,我们只需要关注error、data、end
这些业务事件点上即可,订阅了这些事件,它在执行内部流程会自动去触发相应的事件
传统的回调函数的方式会造成回调地狱,类似这种
fs.readFile('some1.json', (err, data) => {
fs.readFile('some2.json', (err, data) => {
fs.readFile('some3.json', (err, data) => {
fs.readFile('some4.json', (err, data) => {
})
})
})
})
Promise规避了这一点,使用链式调用的形式,可读性更高
readFilePromise('some1.json').then(data => {
return readFilePromise('some2.json')
}).then(data => {
return readFilePromise('some3.json')
}).then(data => {
return readFilePromise('some4.json')
})
异步变同步
这是我们开发时经常要遇到的场景 异步代码同步执行。
写两个模拟的异步函数
var f1 = function() {
return new Promise(resolve => {
setTimeout(() => {
console.log('f1 is run');
resolve('f1 done')
}, 3000)
})
}
var f2 = function() {
return new Promise(resolve => {
setTimeout(() => {
console.log('f2 is run');
resolve('f2 done')
}, 2000)
})
}
Promise
f1().then(res => { return f2() }).then(res => { }) // 输出 3s后输出:f1 is run 再过2s后输出:f2 is run
如果异步函数很多的话,会有很多个
then
f1().then(res => { return f2() }).then(res => { return f3() }).then...
可以用更简洁的写法
var arr = [f1, f2] // 待执行promise数组 var p = Promise.resolve() for(let pro of arr) { p = p.then(res => pro(res)) }
reduce
其实跟1是一样的var arr = [f1, f2] arr.reduce((p,c) => { return p.then((res) => c(res)) }, Promise.resolve())
async await
var arr = [f1, f2] async function doFunc() { for(let p of arr) { await p() } }
Generator
function * gen() { yield f1() yield f2() } let g = gen() g.next() g.next()
以上的方式都能达到继发执行的结果.
1.2.3.4都是继发执行 然后输出。如果是并发执行 然后按顺序输出呢? 有点类似Promise.all
,其实就是并发执行,然后将执行结果先存起来,再顺序输出.这样对于两个无关联的异步函数 并发执行的效率更高
async function testFunc() {
var arr = [f1, f2]
// 并发执行
var promiseArr = arr.map((fn) => fn())
for(let result of promiseArr) {
// 同步返回结果
console.log('result', await result);
}
}
// 输出
f2 is run
f1 is run
result f1 done
result f2 done
Promise
对于Promise,我们要按要点来记忆它
- 很长的链式调用
Promise
没有被resolve
或reject
, 它将一直处于pending
的状态
- 如果是在
pending
的状态 无法知道是刚开始还是快要结束
- 如果是在
- 无法取消
Promise
一旦建立它就会立即执行,无法中途取消
- 无法取消
- 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部(所以一般建议promise
对象后面要跟着catch
方法)
- 如果不设置回调函数,
- 一旦状态改变,就永久保持该状态,不会再变了
- 如果是链式调用 原
promise
对象的状态跟新对象保持一致
- 如果是链式调用 原
reject
的作用 等同于抛出错误
catch
方法返回的也是promise
对象(后面可以继续跟then
)
- 立即
resolve()
的Promise
对象,是在本轮“事件循环”(event loop)
的结束时执行,而不是在下一轮“事件循环”的开始时
- 立即
对于第2点,举个例子:
function pend () {
return new Promise(resolve => {
// 没有resolve
console.log('aaaa');
})
}
pend().then(() => {
console.log('bbb');
})
// 输出
aaaa
Promise {<pending>}
bbb
将永远不会被打印出来
对于第9点,举个例子:
setTimeout(() => {
console.log('three');
},0)
Promise.resolve().then(() => {
console.log('two');
})
console.log('one');
// 输出
one
two
three
one
是立即输出two
是在“本轮”事件循环的末尾执行然后输出 three
是下一轮事件循环开始时被执行然后输出
写的有点笼统 后续会再补充 有问题欢迎一起讨论
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。