异步回调 基于回调的异步风格 一个简单的demo
1 2 3 4 5 6 7 8 9 function loadScript (src, callback ) { let script = document .createElement ('script' ); script.src = src; script.onload = () => callback (script); document .head .append (script) }
之后函数调用方,将函数的回调方式作为参数传递
1 2 3 loadScript ('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js' , script => { alert (`脚本:${script.src} 加载完成` ) });
上面的demo就是采用JS实现的基于回调的异步编程风格
异步执行某项功能的函数应该提供一个 callback
参数用于在相应事件完成时调用
Error优先回调风格 1 2 3 4 5 6 7 8 9 10 11 function loadScript (src, callback ) { let script = document .createElement ('script' ); script.src = src; script.onload = () => callback (null , script); script.onError = () => callback (new Error (`Script load error for script ${src} ` )); document .head .append (script) }
之后在调用方
1 2 3 4 5 6 7 loadScript ('./foo.js' , function (err, script ) { if (err){ } else { alert (`脚本:${script.src} 加载完成` ) } })
上面的方式则是Error优先的回调风格
Promise基础介绍 引入Promise 现在对于上述加载某个脚本的代码,我想要实现加载完某个脚本之后,再按顺序加载第二个脚本,那么在调用方,对应的代码就会变成如下的样子
1 2 3 4 5 6 7 8 9 loadScript ('./foo.js' , function (err, script ) { if (error) { handleError (error); } else { loadScript ('./bar.js' , function (err, scirpt ) { }); } });
所以如果有这样的需求,原先的代码编写方式将会很臃肿而且可读性依托
JS 提供了 promise 的方式来进行优化
声明promise 1 2 3 let promise = new Promise ((resolve, reject ) => { });
这里的 resolve 和 reject 是 JS 自带的函数,不需要关注,直接当接口使用
resolve/reject
只需要一个参数(或不包含任何参数),并且将忽略额外的参数。
promise对象属性 由 new Promise
构造器返回的 promise
对象具有以下内部属性:
state
—— 最初是 "pending"
,然后在 resolve
被调用时变为 "fulfilled"
,或者在 reject
被调用时变为 "rejected"
。
result
—— 最初是 undefined
,然后在 resolve(value)
被调用时变为 value
,或者在 reject(error)
被调用时变为 error
。
因此,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolve
或 reject
来改变对应的 promise 对象的状态。
Promise核心组成 executor生产者定义 1 2 3 4 5 6 7 let promise = new Promise ((resolve, reject ) => { foo (); bar (); reject (new Error ('Failed' )); });
如上代码,如果在 exector 中执行出了问题,executor 应该调用 reject
。这可以使用任何类型的参数来完成(就像 resolve
一样)。但建议使用 Error
对象(或继承自 Error
的对象),因为可以给 catch 在下游进行捕获(下面会介绍)
并且从生产——消费的模型角度来看,我们定义的promise和executor其实是定义了一个生产者,promise作为”消息队列”进行连接
处理程序 then、catch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let promise = new Promise ((resolve, reject ) => { foo (); bar (); reject (new Error ('Failed' )); }); promise.then ( result => { alert (result); }, error => { alert (error); }, );
then中定义了当resolve和reject的时候分别需要做什么
例如如下的代码
1 2 3 4 5 6 7 8 let promise = new Promise ((resolve, reject ) => { setTimeout (()=> resolve ("done" ), 1000 ); }) promise.then ( result => alert (result), error => altet (error) );
如果我们只对成功的情况感兴趣,可以只调用then()
的时候只传入一个参数
1 2 3 4 5 let promise = new Promise ((resolve, reject ) => { setTimeout (()=> resolve ("done" ), 1000 ); }) promise.then (alert);
如果只对失败的情况感兴趣,可以直接调用catch
1 2 3 4 5 6 let promise = new Promise ((resolve, reject ) => { setTimeout (() => reject (new Error ()), 1000 ); }); promise.catch (alert);
finally finally(f) 类似于 then(f,f)
,表示同时将函数f运用到resolve和reject场景下
但是需要注意的是他的核心功能是设置一个处理程序在前面的操作完成之后,执行对应的不包含任何非异常返回值的操作
常见的场景有:
1 2 3 4 5 6 new Promise ((resolve, reject ) => { foo (); bar (); }) .finally (() => stop loading indicator) .then (result => show result, error => show error);
可以看到,使用finally的时候:
不接受参数,这就意味着finally不会得到前一个Promise处理程序的结果,反倒是会将结果或 error “传递” 给下一个合适的处理程序
也不会返回任何内容,除了在处理这一步的的时候出现Error
1 2 3 4 5 6 7 8 new Promise ((resolve, reject ) => { setTimeout (() => { resolve ('test' ); }, 2000 , ); }) .finally (() => alert ('Promise finally ready' )) .then (result => alert (result));
Promise&异步回调对比 回到之前加载脚本的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function loadScript (src, callback ) { let script = document .createElement ('script' ); script.src = src; script.onload = () => callback (null , script); script.onError = () => callback (new Error (`Script load error for script ${src} ` )); document .head .append (script) }loadScript ('./foo.js' , function (err, script ) { if (err){ } else { alert (`脚本:${script.src} 加载完成` ) } });
我们发现,在调用loadscript的时候,需要指定callback,也就是在调用之前,我们必须知道应该如何处理结果,对应上面的匿名函数function(err, script)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function loadScript (src ) { return new Promise ((resolve, reject ) => { let script = document .createElement ('script' ); script.src = src; script.onload = () => resolve ('Script loaded successfully' ); script.onerror = () => reject (new Error (`Script ${src} loaded error` )); document .head .append (script) }); }let promise = loadScript ("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js" ); promise.then ( result => alert (result), error => alert (error) );
我们可以按照自然方式进行编码,运行loadscript
之后,通过then来进行处理,也就是将异步任务发布,和任务消费进行解耦
我们可以根据需要,在 promise 上多次调用 .then
。每次调用,我们都会在“订阅列表”中添加一个新的“粉丝”,一个新的订阅函数。
Promise链 处理程序链式调用 通过上述的语法不难发现,catch finally then
都是支持链式调用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 new Promise ((resolve, reject ) => { setTimeout (() => { resolve (1 ); }, 1000 ); }) .then (result => { alert (result); return result * 2 ; }) .then (result => { alert (result); return result * 2 ; }) .then (result => { alert (result); return result * 2 ; });
这样之所以是可行的,是因为每个对 .then
的调用都会返回了一个新的 promise ,因此我们可以在其之上调用下一个 .then
下面这种不是promise链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let promise = new Promise (function (resolve, reject ) { setTimeout (() => resolve (1 ), 1000 ); }); promise.then (function (result ) { alert (result); return result * 2 ; }); promise.then (function (result ) { alert (result); return result * 2 ; }); promise.then (function (result ) { alert (result); return result * 2 ; });
在处理程序中返回”Promise” 上面我们的处理程序只是简单计算返回结果
我们也可以在处理程序中再次返回promise,这种方式下,后续所有的其他处理程序都会阻塞等待,直到返回的promise状态为settled后再执行对应逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 new Promise ((resolve, reject ) => { setTimeout (() => { resolve (1 ); }, 1000 ); }) .then (result => { alert (result); return new Promise (resolve => { setTimeout (() => { resolve (result * 2 ); }, 1000 ); }); }) .then (result => { alert (result); return new Promise (resolve => { setTimeout (() => { resolve (result * 2 ); }, 1000 ); }); }) .then (result => { alert (result); });
其实从严格角度来说,then中返回的不是Promise对象,而是thenable
对象,这个对象也具有.then
处理程序,它会被当做一个 promise 来对待
下面是一种高级用法,自定义Thenable对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Thenable { constructor (num ) { this .num = num; } then (resolve, reject ) { alert (this .num ); alert (resolve); setTimeout (() => resolve (this .num * 2 ), 1000 ); } }new Promise (resolve => resolve (1 )) .then (result => { return new Thenable (result); }) .then (alert);
当Promise的第一个then运行的时候,return new Thenable
会自动调用对象的 then
方法(类似Promise的executor)
实现按顺序加载脚本 所以回到我们最初的问题,我们想要实现按顺序来加载脚本,同时不希望出现error金字塔这种史山代码,就可以使用Promise链来进行简化
这里综合使用了Promise链以及处理程序中返回thenable的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function loadScript (src ) { return new Promise ((resolve, reject ) => { let script = document .createElement ('script' ); script.src = src; script.onload = () => resolve ('Script loaded successfully' ); script.onerror = () => reject (new Error (`Script ${src} loaded error` )); document .head .append (script) }); }loadScript ('./script1' ) .then (() => loadScript ('./script2' )) .then (() => loadScript ('./script3' )) .then (() => { alert ('All scripts are loaded!' ); });
最佳实践 在前端编程中,promise 通常被用于网络请求
1 let promise = fetch ("http://bing.com" );
fetch函数向指定 url
发出网络请求并返回一个 promise,当远程服务器返回 header(是在 全部响应加载完成前 )时,该 promise 使用一个 response
对象来进行 resolve(也就是说fetch promise resolve的是一个response对象)
然后我们还可以调用response对象的text()
来获取所有的响应内容,这个时候也会返回一个promise, 以刚刚下载完成的这个文本作为 result 进行 resolve;或者调用response对象的json()
来将结果转为JSON对象作为promise resolve的结果
1 2 3 fetch ('./foo.json' ) .then (response => response.json ()) .then (user => alert (user.name ));
异步行为应当始终返回一个Promise 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function showImage ( ) { fetch ('./foo.json' ) .then (response => response.json ()) .then (user => fetch (`https://avatars.githubusercontent.com/${user.name} ` )) .then (response => response.json ()) .then (githubUser => { let img = document .createElement ('img' ); img.src = githubUser.avatar_url ; img.className = 'promise-avatar-example' ; document .body .append (img); setTimeout (() => img.remove (), 3000 ); }); }
上述的实现确实是没什么大问题,但是如果我们还想再下游继续消费处理(例如显示一个用于编辑该用户或者其他内容的表单)则没有办法,如果需要实现,还需要调用then
所以一个合理的最佳实践是:为了保证Promise链的可扩展,异步行为应当始终返回一个Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function showImage ( ) { fetch ('./foo.json' ) .then ((response ) => response.json ()) .then ((user ) => fetch (`https://avatars.githubusercontent.com/${user.name} ` )) .then ((response ) => response.json ()) .then ( (githubUser ) => new Promise ((resolve ) => { let img = document .createElement ('img' ); img.src = githubUser.avatar_url ; img.className = 'promise-avatar-example' ; document .body .append (img); setTimeout (() => { img.remove (); resolve (githubUser); }, 3000 ); }), ); }