平常开发过程当中,时不时会遇到要同时预加载几张图片,而且等都加载完再干活的状况,结合 Promise 和 async/await 代码会优雅不少,但也容易遇到坑,今天就来简单聊聊。javascript
先从最基本的 ES5 提及,基本思路就是作一个计数器,每次 image 触发 onload 就加一,达到次数后触发回调函数。html
var count = 0, imgs = []; function loadImgs(imgList, cb) { imgList.forEach(function(url, i) { imgs[i] = new Image(); imgs[i].onload = function() { if( ++count === imgList.length) { cb && cb() } } imgs[i].src = url; }) } 复制代码
调用方法:前端
loadImgs(["xxx/a.png","xxx/b.png"],function() { console.log("开始干活"); }) 复制代码
这样作基本功能是能知足的,可是这种回调的方式跳来跳去,代码显得比较混乱。java
俗话说,异步编程的最高境界,就是根本不用关心它是否是异步。能用同步的方式写出异步的代码,才是好的编码体验。因而乎,到 Promise 和 async/await 出场了。node
让咱们用 Promise 和 async/await 来改写一下。(注意,这个例子有个很大的问题)webpack
async function loadImgs(imgList, cb) { console.log("start") for( var i =0; i<imgList.length; i++) { await imgLoader(imgList[i], i); console.log("finish"+i) } cb(); } async function imgLoader(url, num){ return new Promise((resolve, reject) => { console.log("request"+num) setTimeout(resolve, 1000); // let img = new Image(); // img.onload = () => resolve(img); // img.onerror = reject; console.log("return"+num) }) } loadImgs(["xxx/a.png","xxx/b.png"],function() { console.log("开始干活"); }) 复制代码
为了方便在 node 环境中运行代码,这里我用 setTimeout 代替了真正的图片加载。git
运行的结果是:程序员
start
request0
return0
finish0
request1
return1
finish1
开始干活
复制代码
有没有发现问题,虽然咱们指望的是用同步代码的形式写出异步的效果,虽然咱们用了 async/await Promise 等吊炸天的东西,可是实际运行的结果倒是同步的。 request0 finish 以后,request1 才发出。es6
这样的代码虽然语义清晰,通俗易懂,但等图片一张一张顺序加载是咱们不能接受的,同时发出几个请求异步加载是咱们的目标。github
产生这种错误的缘由是 async/await 其实只是语法糖并非说加了就异步了,其本质上是为了解决回调嵌套过多的问题。
N 年前,经过分发 jQuery 武器,你们卷起袖子加入了前端大潮,然而他们遇到的一个大问题就是”回调地狱“。
好比下面这个例子,发完三个 ajax 请求以后才能开始干活。
$.ajax({ url: "xxx/xxx", data: 123, success: function () { $.ajax({ url: "xxx/xxx2", data:456, success: function () { $.ajax({ url: "xxx/xxx3", data:789, success: function () { // 终于完了能够开始干事情了 } }) } }) } }) 复制代码
这个还只是把简单的代码结构写出来,括号就多到眼花,若是再加上业务逻辑、错误处理等,那就是实实在在的”地狱“。
Promise 的出现大大改善了回调地狱,写法也更加接近同步。
简单来讲,Promise 就是一个容器,里面保存着某个已经发生将来才会结束的事件,当事件结束时,会自动调用一个统一的接口告诉你。
var promise = new Promise(function(resolve, reject) { $.ajax({ url: "xxx/xxx3", success: function () { resolve(rs) }, }) } // 调用的时候 promise.then(function(rs){ // 返回另外一个 Promise return new Promise(...) }) .then(function(rs){ // 又返回另外一个 Promise return new Promise(...) }) .then(function(rs){ // 开始干活 }) .catch(function(err){ // 出错了 }); 复制代码
Promise 的构造函数有两个参数,都是 javascript 引擎提供的,不用本身实现,分别是 resolve 和 reject。
then 方法能够接受两个函数做为参数,分别对应 resolve 和 reject 时的处理,其中 reject 是可选的。
promise.then(function(value) { // success }, function(error) { // failure }); 复制代码
Promise 至少把广大开发者从回调地狱中拯救出来,把回调变为链式调用。
注意这里只是拿 ajax 作例子,实际上 jQuery 的 ajax 已经 Promise 化,能够直接相似 Promise 的用法。
$.ajax({ url: "test.html", context: document.body }).done(function() { $( this ).addClass( "done" ); }); 复制代码
这种写法已经比回调函数的写法要直观多了,可是仍是有一些嵌套,不够直观。
Promise 和 async/await 之间其实还有一个 Generator,用的也很少,简单说下,形式是这样的:
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true } 复制代码
Generator 函数要用 * 来标识,用 yield 表示暂停,经过 yield 把函数分割出好多个部分,每调用一次 next 会返回一个对象,表示当前阶段的信息 (value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,便是否还有下一个阶段。
关于 Generator 的详细信息能够参考 www.ruanyifeng.com/blog/2015/0…
async/await 其实 Generator 的语法糖,用 async 这种更明确的标识代替 *,用 await 代替 yield。
说了这么多,咱们终于明白 async/await 是为了能用同步的方式写出异步的代码,同时解决回调地狱。
因此在多图片异步加载这个场景下,咱们指望的应该是多个异步操做都完成以后再告诉咱们。
async function loadImgs(imgList){ let proList = []; for(var i=0; i<imgList.length; i++ ){ let pro = new Promise((resolve, reject) => { console.log("request"+i) setTimeout(resolve, 2000); console.log("return"+i) }) proList.push(pro) } return Promise.all(proList) .then( ()=>{ console.log("finish all"); return Promise.resolve(); }) } async function entry(imgList, cb) { await loadImgs(imgList); cb(); } entry(["xxx/a.png","xxx/b.png"], function(){ console.log("开始干活") }) 复制代码
运行结果是:
request0
return0
request1
return1
finish all
开始干活
复制代码
会看到一开始就立马打印出
request0
return0
request1
return1
复制代码
过了两秒以后,才打印出 finish all
。
上面咱们都是在 node 命令行里面运行的,在理解整个过程以后,让咱们在浏览器里面实际试试,因为兼容性问题,咱们要借助 webpack 转换一下。
上代码:
function loadImgs(imgList){ let proList = []; for(var i=0; i<imgList.length; i++ ){ let pro = new Promise((resolve, reject) => { let img = new Image(); img.onload = function(){ resolve(img) } img.src = imgList[i]; }) proList.push(pro) } return Promise.all(proList) .then( (rs)=>{ console.log("finish all"); return Promise.resolve(rs); }) } async function entry(imgList, cb) { try { let rs = await loadImgs(imgList); cb(rs); } catch(err) { console.log(err) cb([]) } } var imgUrlList = [ "http://111.231.236.41/vipstyle/cartoon/v4/release/pic/index/recomment-single-s3.png", "http://111.231.236.41/vipstyle/cartoon/v4/release/pic/index/recomment-single-s2.png" ] entry(imgUrlList, function(rs){ console.log("开始干活") console.log(rs) }) 复制代码
注意, await 命令后的 Promise 对象是有可能 rejected 的,因此最好放到 try...catch 块中执行。
须要用 webpack 转换下,能够参考咱们 webpack.config.js:
module.exports = { entry: ['./index.js'], output: { filename: 'bundle.js' }, devtool: 'sourcemap', watch: true, module: { loaders: [{ test: /index.js/, exclude: /(node_modules)/, loader: 'babel', query: { presets: ['es2015', 'stage-3'], plugins: [ ["transform-runtime", { "polyfill":false, "regenerator":true }] ] } }] } } 复制代码
跑完以后写个页面在浏览器运行一下,打开 console,能够看到
返回的结果有两个图片对象,是咱们指望的。
再看看 network,检查下是不是并发的:
ok,搞定。
其实到上面那一步关于 async/await 异步加载图片的相关东西已经讲完了,这里咱们回过头来看下生成的文件,会发现特别的大,就那么几行代码生成的文件竟然有 80k。
把 webpack 具体打了哪些包打印出来看看:
其中,咱们原本的 index.js 只有 4.08k ,可是 webpack 为了支持 async/await 打包了一个 24k 的 runtime.js 文件,除此以外为了支持 es6 语法还打包了一大堆别的文件进去。
若是你在打包的时候使用了 babel-polyfill
最后出来的文件能够达到可怕的 200k。
因而我想起了 TypeScript。
TypeScript 具备优秀的自编译能力,不须要额外引入 babel,并且比 babel 作的更好。以我上面的代码为例,安装 TypeScript 以后,不须要任何修改,只要把后缀名改为 ts,直接就能够开始编译。
来感觉一下:
bundle-ts.js 就是用 TypeScript 编译出来的,只有 5.5k。
看一下编译出来的文件中 async/await 的实现,不到 40 行,干净利落。
TypeScript 编译出的文件跟你使用了多少特性有关系,而 bable 可能一开始就会给你打包一堆进去,即便你如今还没用到,并且一些实现上 TypeScript 也要比 bable 更好。
固然,这里并非说用 TypeScript 就必定比 bable 好,仍是要根据项目实际状况来,但 TypeScript 绝对值得你去花时间了解一下。
有时候咱们不能单从表面看问题,而要从一个事情的演化来看,好比 async/await 咋一看异步,就认为加了就异步,这样很容易走入误区。有空多想一想背后的故事,会有更深入的认识,你我共勉。
记录一些所思所想,写写科技与人文,写写生活状态,写写读书心得,主要是扯淡和感悟。 欢迎关注,交流。
微信公众号:程序员的诗和远方
公众号ID : MonkeyCoder-Life