前言
编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续提出的新概念是否解决了问题。
将回调包装成promise
观察者模式与事件监听
内容来源于《ES6入门-阮一峰》《你不知道的JS》《MDN web文档》《N
ode.js》、‘网上的诸多博客’、‘自己以前的代码’。
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。
异步与多线程
先说结论:JS是单线程的但是宿主环境:浏览器或node 不是。
简单的看JS的代码是从上至下执行的,如果中间遇到了特别耗时的任务。那此任务下面的代码就会一直等待等待。也就是阻塞
。想解决这个问题,也就是解决耗时任务的问题,一种可以开启多个线程
,将任务移到子线程去不占用主线程,比如安卓app就是在主线程更新UI,在子线程种执行耗时操作譬如网络请求。(html5新增了web worker
也可以开个子线程执行其他操作了)。一种就是现在js主流采用的异步编程
:先执行所有的同步代码块,然后再执行异步代码块。
一个比较有趣的点是,刚开始听说异步的时候,总是会想起来JS是单线程的。就好像是只有一个工人一样,分给他的任务即使先做简单的,那复杂的不一样的得慢慢做吗?很显然我是错的,JS是单线程的,但是浏览器是多线程的。
具体请看这篇博客,讲的很好,这篇
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
-
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,执行回调。
在node种也有类似的Event loop
1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
2、主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
4、主线程不断重复上面的第三步。
至于具体实现以及与浏览器之间的差异就不写了,我也没搞懂。
回调
wiki定义:In computer programming, a callback, also known as a "call-after" function, is any executable code that is passed as an argument to other code that is expected to call back (execute) the argument at a given time.This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback
简单的说就是做好了叫我。根据上文的浏览器或node异步事件处理机制叙述,可以很轻松的看出,怎么用回调写异步代码。将耗时操作与需要得到结果后执行的操作用回调函数写就。例如(还有事件监听或订阅模式与本篇内容关系不大,不写。)
function foo() {
console.log('执行完了')
}
function bar1() {
console.log('同步函数1')
}
function bar2() {
console.log('同步函数2')
}
bar1()
setTimeout(foo, 1000)
bar2()
/**
同步函数1
同步函数2
执行完了
*/
回调是异步的解决方案,但是回调的问题又是什么?
回调地狱
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
谁都写过类似的代码,一个套一个。所以回调的问题一是不清晰。
顺序
如果想组织两个或多个异步函数的顺序用回调函数就会变得复杂,比如上面回调地狱那种,按照顺序一一执行,又或要等两个异步函数都出结果才会回调,或一个出了结果立刻回调另一个作废。这些操作都需要在每一个回调函数里书写判断逻辑。
控制倒转
假设
bar1()
setTimeout(foo, 1000)
bar2()
我们的目的是先执行bar1、bar2稍后执行一次foo。这个代码用了最熟悉的setTimeout,所以可以保证按照设计执行,但是如果这个异步函数是第三方提供的,那么就失去了foo的执行控制权。称之为控制倒转。
这种情况可能会导致回调不调用、调用过早、调用多次、没有得到必要参数、吞掉了异常等等。
可以利用错误优先模式解决部分问题
function response(err,data) {
// 有错?
if (err) {
console.error( err );
}
// 否则,认为成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
总结
又不是不能用,应该可以很准确的形容异步回调。组织代码不清晰缺乏顺序性,对很多场景要加判断逻辑缺乏可靠性。现在轮到promise解决这些问题了。
Promise
回调缺乏可靠性的原因就是我们不知道它何时会完成也不知道是否成功。Promise就解决了这个问题。
Promise中保存着未来才会结束的事件,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)一旦状态改变,就不会再变。这时就称为 resolved(已定型)。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
==============
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
虽然依然是基于回调的,但是却是通过固定的形式将回调的形式固定下来,并且会传递异步得到的数据或错误给回调函数。解决了信任问题。
回调包装成promise
假设是包装某个特定的函数,可以直接构造并设定好resolve与reject。
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
一种是调用个方法可以随时将回调promise化,这个可以下载第三方库,比如bluebird
解决回调问题
- 回调过早调用
如果对一个 Promise 调用 then(..) 的时候,即使这个 Promise是立即resolve的函数(即Promise内部没有ajax等异步操作,只有同步操作), 提供给then(..) 的回调也是会被异步调用的
Promise.resolve()
可以将现有对象转化为Promise对象。根据参数不同而结果不同
- 参数Promise实例
不做任何修改返回这个实例。 - 参数是个thenable对象
将这个对象转为Promise对象,并立刻执行thenable对象的then方法。thenable对象指的是具有then方法的对象 - 参数不是具有then方法的对象,或根本就不是对象,比如基本类型值
返回一个新的 Promise 对象,状态为resolved。 - 不带有任何参数
直接返回一个resolved状态的 Promise 对象。
- 回调调用次数过多
Promise 的内部机制决定了调用单个Promise的then方法, 回调只会被执行一次,因为Promise的状态变化是单向不可逆的,当这个Promise第一次调用resolve方法, 使得它的状态从pending(正在进行)变成fullfilled(已成功)或者rejected(被拒绝)后, 它的状态就再也不能变化了 - 回调中的报错被吞掉
Promise中的then方法中的error回调被调用的时机有两种情况:- Promise中主动调用了reject (有意识地使得Promise的状态被拒绝), 这时error
回调能够接收到reject方法传来的参数(reject(error)) - 在定义的Promise中, 运行时候报错(未预料到的错误), 也会使得Promise的
状态被拒绝,从而使得error回调能够接收到捕捉到的错误
- Promise中主动调用了reject (有意识地使得Promise的状态被拒绝), 这时error
4.回调没有调用
设置一个超时用Promise.race组合。Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例,只要其中一个实例改变状态新的大实例就会改变状态。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
组织问题
- 链式
将回调地狱改成链式,每一个then里面的异步操作都可以返回一个值,传递给下一个异步操作当作参数。- 每次你在一个Promise上调用then(..)的时候,它都创建并返回一个新的Promise,我们可以在它上面进行 链接。
- 无论你从then(..)调用的完成回调中(第一个参数)返回什么值,它都做为被链接的Promise的完成。
Promise.then(
// 第一个异步操作
).then(
// 第二个异步操作
).then(
// 第三个异步操作
)
- 门
Promise.all
方法
all方法接收一个Promise数组,并且返回一个新的“大Promise”, 只有数组里的全部Promise的状态都转为Fulfilled(成功),这个“大Promise”的状态才会转为Fulfilled(成功), 这时候, then方法里的成功的回调接收的参数也是数组,分别和数组里的子Promise一一对应 - 竞态
Promise.race
方法
Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例,只要其中一个实例改变状态新的大实例就会改变状态。
错误处理
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
promise
.then(function(data) {
// success
})
.catch(function(err) {
// error
});
总结
Promise是一个用可靠语义来增强回调的模式,所以它的行为更合理更可靠。通过将回调的 控制倒转 反置过来,我们将控制交给一个可靠的系统(Promise),它是为了将你的异步处理进行清晰的表达而特意设计的。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Genarator
遍历器(Intertor)
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
有些数据结构原生部署了Iterator接口,可以直接调用原生Array、Map、Set、NodeList 对象等等。对象是没有此接口的,如果需要可以用Map代替。或者用Generator 函数。
这些数据结构有此接口的原因是它们有Symbol.iterator
属性, 因为是Symbol
值要用[]
调用,此属性为一个函数,叫遍历器生成函数,调用后返回遍历器。
原生数组可以直接这样遍历。
let arr = [1,2,3]
let arrIter = arr[Symbol.iterator]()
arrIter.next() //? { value: 1, done: false }
arrIter.next() //? { value: 2, done: false }
arrIter.next() //? { value: 3, done: false }
arrIter.next() //? { value: undefined, done: true }
================================
Generator 函数顺便测试一下
let obj = {
bar: {
b : 2
},
[Symbol.iterator]: function *() {
yield 1
yield 2+3
yield 'hello'
yield (function () {
return '立即执行函数的返回值'
})()
yield foo = {
a : 1
}
yield this.bar
}
}
let ObjIter = obj[Symbol.iterator]()
ObjIter.next() //? { value: 1, done: false }
ObjIter.next() //? { value: 5, done: false }
ObjIter.next() //? { value: 'hello', done: false }
ObjIter.next() //? { value: '立即执行函数的返回值', done: false }
ObjIter.next() //? { value: { a: 1 }, done: false }
ObjIter.next() //? { value: { b: 2 }, done: false }
let arr = [...obj] //? [ 1, 5, 'hello', '立即执行函数的返回值', { a: 1 },{ b: 2 } ]
for (const x of obj) {
console.log(x) //? 1, 5, 'hello', '立即执行函数的返回值', { a: 1 } ,{ b: 2 }
}
for (const key in obj) {
const element = obj[key]
console.log(element) //?{ b: 2 }
}
Reflect.ownKeys(obj) //? [ 'bar', Symbol(Symbol.iterator) ]
for (const x of Reflect.ownKeys(obj)) {
console.log(obj[x]) //? { b: 2 },[λ: [Symbol.iterator]]
}
想让对象有遍历器接口只要给它一个Symbol.iterator
属性就行,这个属性也得是个函数。上面的做法,将一个generator
函数赋予给对象的遍历器属性,当遍历这个对象时就会调用此函数,但是遍历结果是此函数内部的表达式结果,与对象属性无关。
下面两种方法可以遍历对象属性的值,一种就是不给它赋予遍历器属性而是借其他数据结构之手,另一种在Symbol.iterator
属性函数里写逻辑将对象属性遍历后返回属性值。
for (let key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}
==========================
function* entries() {
for (let key of Reflect.ownKeys(this)) {
yield [key, this[key]];
}
}
let obj1 = {
a : 1,
b: 2
}
obj1[Symbol.iterator] = entries
for (const [key, value] of obj1) {
console.log(`${key}: ${value}`)
}
Generator
generator
函数是遍历器对象生成函数,执行后返回遍历器对象。*
号注明函数为生成器函数,在函数内部使用yield
表达式产出不同的内部状态
function* foo() {
yield 1
yield 2
return 3
}
let bar = foo()
bar.next() //? { value: 1, done: false }
bar.next() //? { value: 2, done: false }
bar.next() //? { value: 3, done: true }
当调用next
方法的时候,函数内部语句会运行到第一个yield
停止并将yield
后的表达式计算出结果后返回,与return
类似,不同的是,yiled后的语句会在调用下一个next()方法后继续执行知道遇到第二个yield
。
状态机与协程
这个和此篇无关,所以只写个标题吧。
Generator内外部通信
生成器函数不仅可以一步一步的运行下去输出结果,还可以传入不同数据来操控下面的步骤。
next方法的参数
yield
意为产出,其后跟的表达式的结果在计算后会直接产出,函数内部并不能得到
function* foo(x) {
let y = 2*(yield x+1)
yield y
}
let bar = foo(3)
bar.next() //? { value: 4, done: false }
bar.next() //? { value: NaN, done: false }
bar.next() //? { value: undefined, done: true }
第一步运行的是x+1
得出的结果为4产出了,外部可以看到。当内部需要它再参与计算的时候就发现根本不存在变成2*undefined
结果就是NaN
。
如果yield算产出,那next
方法的参数算投入。参数会被当作上个yield
表达式的结果值,如果没有上个自然无效。所以第一个next()算是启动遍历器的不能加参数。
上例改一下
function* foo(x) {
let y = x*(yield x+1)
console.log(x) //?3
yield y
}
let bar = foo(3)
bar.next() //? { value: 4, done: false }
bar.next(5) //? { value: 15, done: false }
return和throw
return
方法,可以返回给定的值,并且终结遍历 Generator 函数。
function *foo() {
yield 1
yield 2
yield 3
}
let bar = foo()
bar.next() //? { value: 1, done: false }
bar.return('结束了') //? { value: '结束了', done: true }
bar.next() //? { value: undefined, done: true }
如果函数内部有finally
就会在调用return后立刻执行,finally内代码,最后执行return
function *foo() {
try {
yield 1
} finally {
yield 2
}
yield 3
}
let bar = foo()
bar.next() //? { value: 1, done: false }
bar.return('结束了') //? { value: 2, done: false }
bar.next() //? { value: '结束了', done: true }
throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
function *foo() {
try {
yield 1
yield 2
} catch (e) {
yield 3
console.log(e) //?出错了
}
yield 4
}
let bar = foo()
bar.next() //? { value: 1, done: false }
bar.throw('出错了') //? { value: 3, done: false }
bar.next() //? { value: 4, done: false }
throw
方法被调用后,会结束调用下面的代码,并从catch后继续,注意throw自带next(),传入的错误值会被捕获成为catch的参数,并传出下一个yield的值。
总结
next
、return
、throw
可以粗略的理解为替换yield表达式,
next
是换为值
return
是换为return语句
throw
是换为throw语句
不同点在于,生成器函数内如果存在try catch或finally即使return或throw了还是可以继续运行。
Generator与Promise
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。具体的就是上文写的,
下面的代码用Fetch
实现,它是基于promise
的,在node环境中使用要装个插件
npm install node-fetch --save
简单的用下
const fetch = require('node-fetch');
let url = 'http://api.apiopen.top/singlePoetry' //下文省略上面两句
fetch(url)
.then(res => res.json())
.then(json => console.log(json));
会随机出个诗句,网上随便找的接口
{ code: 200,
message: '成功!',
result:
{ author: '陆游',
origin: '夜泊水村',
category: '古诗文-人生-青春',
content: '一身报国有万死,双鬓向人无再青。' } }
异步
回到起点,为什么需要回调?因为如果把耗时的代码写成同步的形式,那代码就会卡在那里也就是发生了阻塞
,所以需要先执行完同步代码,最后执行耗时代码得出结果后再执行处理结果的代码,这个过程叫做用回调完成异步。
在generator
函数中的yield
表达式有两个特性,一是向函数外传出消息、二是暂停函数。所以generator函数天生就具有异步的特征,如果手动执行generator函数,那此函数是不会执行的,自然会在同步代码后执行,当我们执行函数时,在yeild后放异步操作,那函数就会暂停于此直到执行完毕,当传出结果就可以继续next()执行下一步,这个下一步就和回调函数一个含义。
function *foo() {
try {
let result = yield fetch(url)
console.log(result)
} catch (error) {
console.log(error)
}
}
===================================
let it = foo()
let result = it.next().value
result
.then(data => data.json())
.then(data => it.next(data))
.catch(e => it.throw(e))
可以看到let result = yield fetch(url) console.log(result)
将异步回调的形式,写成了同步的方式,没有回调地狱也没有无限的then。
这个函数问题是执行起来复杂,虽然函数内部包装异步操作将异步改同步,但是真正运行的时候要多几步,首先要先启动第一步,其次yield
是产出值,所以代码中的result
其实是等于undefined
,需要在bar.next(data)
下一次执行时传入上次的结果值 并赋值。
如果生成器函数里包含着一个异步操作序列,很多步的promise,那generator
内的复杂度其实不高,写成同步形式反而更好分清执行顺序。但是运行起来的时候就要在外面写一长串的promise链了,因为要将函数分步运行并且将yield传出的异步结果值再传进去。
基于Promise的自动执行
(还有基于回调的自动执行 Thunk函数,不写了,所有下面yield后跟的异步操作必须是基于promise的,如果不是要先用new Promise包装。)
上文的问题就是写generator函数简单,执行起来麻烦,如果自动执行的话就完美了。
function run(fn) {
let it = fn()
function next(data) {
let result = it.next(data)
if (result.done) return result.value
result.value
.then(res => res.json())
.then((data) => next(data))
.catch(e => it.throw(e))
}
next()
}
一个利用递归简单的实现。运行一下
function *foo() {
try {
let result = yield fetch(url)
console.log(result)
let result2 = yield fetch(url)
console.log(result2)
} catch (error) {
console.log(error)
}
}
run(foo) //结果是吟了两句诗
只是简单的实现,下面复制下《你不知道的js》与es6入门中的实现
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// 在当前的上下文环境中初始化generator
it = gen.apply( this, args );
// 为generator的完成返回一个promise
return Promise.resolve()
.then( function handleNext(value){
// 运行至下一个让出的值
var next = it.next( value );
return (function handleResult(next){
// generator已经完成运行了?
if (next.done) {
return next.value;
}
// 否则继续执行
else {
return Promise.resolve( next.value )
.then(
// 在成功的情况下继续异步循环,将解析的值送回generator
handleNext,
// 如果`value`是一个拒绝的promise,就将错误传播回generator自己的错误处理g
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})(next);
} );
}
--------------------------------------------------------------------------------
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var 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
语言内置的,不需要执行器的generator异步实现。
async function foo() {
try {
let result = await fetch(url)
let data = await result.json()
console.log(data)
} catch (error) {
console.log(error)
}
}
foo()
await
yeild后面只能跟promise对象,且结果值会传出函数外还要再传进来一次,await后面不仅可以跟promise对象还可以跟原生类型,更重要的是await的结果可以直接赋值,不需要传来传去。当yield
两个特性:产出值与自我阻塞,去掉了产出那就剩下await
了
async
async返回的是Promise,所以可以在后面用then添加回调函数
总结
写了generator之后async确实没什么好写的了。因为这个用起来确实太方便了,将异步改为同步写法,流程明了,错误处理也很清晰。
总结
回调,promise,async,其实相当于一步一步的封装。promise通过固定的形式封装了回调,在then里添加回调函数在catch里捕捉错误,并且通过自身特性固化了异步操作的完成与否。相当于好用版的回调,async通过进行了进一步的封装,将promise表达式跟在await后面等待解析,并将结果赋值下个语句处理,达成了用同步代码的写法组织异步代码。所以如果有原生异步回调的方法或第三方库,想要方便的使用async就需要一步步封装了。
下一篇写分别封装好三者,并在复杂的异步环境下比较。