学会使用JavaScript的异步及优化(续)
文章目录
1.深入理解回调机制
回调:
特点1:函数的执行在未来的某个时刻,取决于事件函数的触发-------无法保证事件触发的顺序,js引擎只能保证触
发后函数的执行顺序,所以如何保证事件触发的顺序?(使用Promise,使用事件嵌套链式触发,使用生成器)
**特点2:**事件函数在触发之前只会通知宿主环境设置监听,引入一个延续,等待触发的执行------在此阶段产生竞争
状态,先触发的先插入事件循环队列-------只有在这个阶段才能控制事件的触发顺序!!
在触发之后,环境就将回调函数插入事件执行循环队列-----等待前面事件的执行完,才会执行!------如果引
入多线程,这个时候事件的执行顺序又可能被打乱!!
**特点3:**所以在js中,代码的执行顺序分为现在执行的代码( 严格受js引擎的顺序控制 ),事件代码(控制权交托给宿
主环境提供的事件函数执行,所以想要控制顺序,必须控制事件触发函数的触发顺序!)
**特点4:**可以看出由引擎反转到事件触发函数,交托给宿主环境的第三方函数API,称为回调函数的控制反转!
如果第三方函数不够完善就会产生信任问题,这个时候就需要自己书写安全机制代码!
**特点5:**由此可以看出回调的两大缺陷,1、无法保证执行顺序(由事件触发函数控制) 2、如果事件函数API不完善
又会产生信任问题,这个时候需要自己构建安全机制代码!! 3、事件触发也并不完全都是异步的,换而言之
事件触发可能同步,可能异步,对于这种事件触发API的不确定性,带来极大的bug追踪难度!!!所以还需保证
事件触发函数都是异步的!!
function result(data){
console.log(a)
}
var a=0
ajax("url",result)
a++
//如果ajax可能同步触发,也可能异步触发,结果是打印0还是1呢,你无法确定!!
2.异步的优化-------高级的异步模式Promise精确控制事件循环的调度执行!!
Promise:
**特点1:**由于事件触发函数将控制反转,如果我们不把控制权交给第三方,而是让第三方提供何时结束的信息(返回
未来值,或者返回错误值),然后利用这个信息执行下一步的函数执行!
function third_party(x){
return message
}
var decision=third_party(42)
decision
.then("success",function(){...})
.then("failure",function(){...})
//好处:实现第三方和己方函数的分离,不需要互相关注实现细节,己方只需要知道第三方的输入接口和输出返回
//第三方也不需要关注己方函数的执行细节,只需要返回正确的数据即可,而不需要再像使用回调时还需要维护一
//个回调函数的传入!!!
function third_party(x){
return new Promise(function(res,rej){
...//使用Promise返回第三方的数据结果!
})
}
var p=foo(42)
bar(p)
baz(p)
function bar(promise){
promise
.then("success",function(){...})
.then("failure",function(){...})
}
function baz(promise){
promise
.then("success",function(){...})
.then("failure",function(){...})
}
//鸭子类型检查,如果使用prototype将一个不是Promise的值注册了一个then函数,就会被误认为是一个
//Promise,如果一旦误用,就会出现bug!!
Promise如何解决回调的顺序性和可信任缺陷问题?
一、Promise建立信任机制
**解决1:**回调过早或者过晚------通过在第三方函数返回一个异步封装的Promise值,即使第三方立即被调用,
then()注册的函数也会被异步执行;;此外无论过晚执行,都没关系,then注册函数的执行取决于合适返回
Promise决议,都会在下一个异步点调用,也就是说,通过Promise决议和then机制,使得函数的执行权回到
了js引擎上
**解决2:**Promise永远为决议被挂住------使用Promise.race()能够将多个Promise实例包装新的Promise返回,率先决
议值便会返回给新Promise
设置超时机制,如果指定时间未执行,Promise就会变为reject状态
**解决3:**回调次数太少或者太多------由于Promise只能被决议一次,如果resolve,reject被调用多次,Promise也
只会接受第一次决议,忽略之后的调用;;但是可以对同一个Promise决议,调用多个p.then();p.then()
**解决4:**未能传入参数、环境值-------Promise决议的resolve和reject只能接受一个参数,其后的传入参数都会被忽
略,要传入多个参数,只能封装为单个值对象传入!!
**解决5:**吞掉error-------在Promise创建时出现js异常error,Promise依然会决议,但是会决议成reject状态,而非
中断,执行同步代码;;如果实在决议之后的then()
,则会直接返回一个带有reject决议的Promise,但是对于
当前的Promise就会出现吞掉错误异常的现象!!
**解决6:**返回的Promise决议本身是真的、可信任的Promise吗-----使用Promise.resolve()
对传入的值进行封装
以此来获得信任,如果是非Promise值,则使用.then就会解封得到非Promise值,如果是真正的Promise,就能如
期执行,可见使用Promise.resolve
实现一个过滤,建立信任机制的作用!!!
二、Promise模式
模式1:this-then-this-then...
链式执行模式------这种模式特点在于任意时刻只能执行一个异步任务,而且
是顺序执行
模式2:Promise.all([...])
门gate模式-------通过all设置一个门机制,只有等待多个并行/并发的异步任务执行
完成才能继续执行,但是不在乎他们的执行顺序!!
var p1=request("url1")
var p2=request("url2")
Promise.all([p1,p2])//all只有所有Promise完成才会返回所有的返回值数组消息msg
.then(function(msg){
return request("....")
})
.then(function(msg){
console.log(msg)
})
模式3:Promise.race([...])
竞态模式-------通过race,率先决议的Promise则返回给新的Promise值,如果是
空数组就会永久挂起!!
变体模式:none([]) /any([]) /first([]) /last([])
并发迭代模式:map()
并发迭代模式------对于同步的数组任务,可以使用forEach为每个任务添加额外的任务,但
是对于多个异步执行任务无法实现,这个时候必须使用异步工具map()
接受一个异步任务数组,映射完任务返回
异步任务结果数组
var p1=Promise.resolve(21)
var p2=Promise.resolve(31)
var p3=Promise.reject(41)
Promise.map([p1,p2,p3],function(pr,done){
Promise.resolve(pr)
.then(function(v){
done(v*2)
},done)
})
.then(function(vals){
console.log(vals)//[42,62,41]
})
三、Promise的局限性
单决议性、
单一值性、
吞掉错误、
一旦创建Promise无法取消、
由于回调代码一直保持活跃即使不能执行的异步任务,导致垃圾无法回收!
与裸露的回调函数相比执行效率较慢
缺点:相比回调函数的一团乱麻,Promise的this-then-this-then....
更加的表达清晰,但是仍然有大
量的重复代码,使用生成器可以提升巨大的优美模式
3.更加高级的异步模式生成器------解决js事件循环的竞争状态
**1.认识生成器:**js引擎执行函数代码时,一旦开始执行,就会运行到结束,期间无法打断!如果需要打断,可以使
用多线程机制或者ES6引入生成器
var x=1
function *foo(){//表示引入生成器
x++
yield //表示函数执行的暂停
console.log("x:",x)
}
function bar(){
x++
}
//开始构造一个迭代器it,用于控制函数生成器*foo()
var it=foo()
it.next()//启动生成器
bar()//在暂停处执行其他代码
it.next()//再次运行生成器,并调用结束!
**2.生成器的强大作用:**不仅能够接受参数,提供返回值,还能够提供强大的内建消息的输入和输出能力;;此外
还能够对同一个生成器函数创建多个迭代器实例,或者使用多个迭代器实例交替运行多个生成器(在同一个作用域)
的情况下!!记住next()
的调用次数始终比yield
多一个!!使用生成器机制可以模拟多线程执行的机制!!
function *foo(x){
var y=x*(yield)
y
return y
}
var it=foo(6)
it.next()
var res=it.next(7)//返回的是一个对象,通过使用yield和next在赋值表达式处暂停,并输入内建参数!!
console.log(res)
//第一个next执行到yield处,第二个next完成被暂停的表达式结果,并继续执行到结束,或者遇到第二个yield
**3.生产者和迭代器:**如果想要生产一系列的值,且每一个值都与前一个相关,也就是说每次生产当前值时,必须
要记住前面的最后值,而使用闭包函数的缓存机制就能完成这一项功能!
next()
迭代器返回一个对象,{value:undefined,done:true}
表示value
获得yield值或者返回值
否则为undefined
,done
表示调用是否完成,只有最后一个next()
才表示完成,前面的next()
调用均是
flase
var dosomething=(function(){
var val //由于闭包函数的特性,每次执行时,val都会保存上一次调用执行的值,而且不会被垃圾回收!
return function(){
if(val===undefined){
val=1
}else{
val=3*val+6
}
return val
}
})()
dosomething()//1
dosomething()//9
dosomething()//33
dosomething()//105
var dosomething=(function(){
var val //由于闭包函数的特性,每次执行时,val都会保存上一次调用执行的值,而且不会被垃圾回收!
return {
[Symbol.iterator]:function(){return this},//为了每次调用next返回一个此对象的迭代器
next:function(){
if(val===undefined){
val=1
}else{
val=3*val+6
}
return {value:val,done:false}//返回一个对象
}
}
})()
//使用自定义的迭代器,进行一系列的生产值的生产!!
dosomething.next()//1
dosomething.next()//9
dosomething.next()//33
dosomething.next()//105
//使用for...of迭代器循环调用对象的next()!!
for(var v of dosomething){
console.log(v)
if(v>500)
break;//必须设置便捷条件,由于迭代器默认为false,会一直循环,知道done为true
}
//除了为对象自定义迭代器,9大内建对象在ES6中也有自己的默认迭代器
var a=[1,3,5,7,8,9,10]
for( var v of a){//v是取出value值而非返回的对象
console.log(v)
}
**4.生成器和迭代器:**由于定义一个生成器,调用生成器本身就是得到一个迭代器,意味着可以直接使用此迭代器
搭配for...of
生产一系列的值,而且生成器函数的作用域会一直保持,就像相当于闭包函数,但又由于yield
中断,又不会一直锁住宿主环境,可谓一举两得!!
function *dosome(){
var val
while(true){
if(val===undefined)
val=1;
else
val=3*val+6;
yield val //通过yield拿到每次循环调用的值
}
}
for(var v of dosome()){
console.log(v)
if(v>5000)
break;
}
5.使用生成器和迭代器:实现看似同步实则异步的任务执行-----比回调和Promise更高级的模式!!
//使用看似同步的代码拿到异步的数据!!
function *foo(){
var data=yield ajax("url",function(err,data){
if(err){
it.throw(err)//还可以直接同步错误处理
}else{
it.next(data)//一旦ajax拿到响应数据,就会调用ajax的回调函数,使用it.next()迭代器
//恢复暂停的生成器的执行,并返回data,从而通过暂停的方式拿到异步数据
}
})
console.log(data)
}
var it=foo()
it.next()//启动生成器!!
var data=ajax("url",function(){...})
console.log(data)//比较二者的区别!!
//前者在执行异步任务之前直接中断,后者无法同步代码拿到异步任务的数据!!
6.使用生成器+Promise建立可信任和可组合机制
/*
1.通过在生成器中yield一个Promise,通过生成器实现同步机制,再通过Promise实现可信任和组合机制, 从而完美的解决回调的两大缺陷
*/
function *foo(){
try{
var data=yield function(){
return new Promise()
}
}catch(err){
console.log(err)
}
}
var it=foo()
var p=it.next().value//启动生成器!!
p
.then(function(data){
it.next(data)
},
function(err){
it.throw(err)
})
//使用封装运行Promise+生成器的工具函数
function run(generator){
var args=[].slice.call(arguments,1),it;
it=generator.apply(this,args)//初始化迭代器,并设置为只能接受一个参数
return Promise.resolve().then(function handleNext(value){
var next=it.next(value)
return (function handleRes(next){
if(next.done){
return next.value
}else{
return Promise.resolve(next.value)
.then(handleNext,function handleErr(err){
return Promise.resolve(it.throw(err)).then(handleRes)
})
}
})(next)
})
}
run(foo)//直接调用此工具即可!!
7.生成器模式:
**生成器+Promise并发模式:**由于yield只能在单个点设置暂停,无法控制多个异步任务的并发进行,所以必须通过
Promise的并发机制来实现!!
function *foo(){
var p1=request("url1")
var p2=request("url2")//通过Promise的决议在下一个异步点执行机制实现两个ajax请求并发进行!
var r1=yield p1
var r2=yield p2
var r3=yield request(...)
}
run(foo)
**生成器委托模式:**即在一个生成器中调用另一个生成器
function *foo(){
console.log("*foo()starting")
yield "f1"
yield "f2"
console.log("*foo()finished")
}
function *bar(){
yield "b1"
yield "b2"
yield *bar()//yield委托
yield "b3"
}
var it=bar()
console.log(
it.next(),
it.next(),
it.next(),//foo()启动
it.next(),//foo()结束
it.next(),
it.next()
)
生成器自身并发模式:…
4.try-catch
同步错误处理机制无法处理异步代码,必须通过生成器提供环境支持
function foo(){
setTimeout(function(){
baz()
},1000)
}
try{
foo()
}catch(err){
//永远无法执行到这里!!
}