异步多图加载这件小事儿(Promise与async)

日常开发过程中,时不时会遇到要同时预加载几张图片,并且等都加载完再干活的情况,结合 Promise 和 async/await 代码会优雅很多,但也容易遇到坑,今天就来简单聊聊。

ES5

先从最基本的 ES5 说起,基本思路就是做一个计数器,每次 image 触发 onload 就加一,达到次数后触发回调函数。

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("开始干活");
})

这样做基本功能是能满足的,但是这种回调的方式跳来跳去,代码显得比较混乱。

俗话说,异步编程的最高境界,就是根本不用关心它是不是异步。能用同步的方式写出异步的代码,才是好的编码体验。于是乎,到 Promise 和 async/await 出场了。

ES6

让我们用 Promise 和 async/await 来改写一下。(注意,这个例子有个很大的问题)

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 代替了真正的图片加载。

运行的结果是:

start
request0
return0
finish0
request1
return1
finish1
开始干活

有没有发现问题,虽然我们期望的是用同步代码的形式写出异步的效果,虽然我们用了 async/await Promise 等吊炸天的东西,但是实际运行的结果却是同步的。 request0 finish 之后,request1 才发出。

这样的代码虽然语义清晰,通俗易懂,但等图片一张一张顺序加载是我们不能接受的,同时发出几个请求异步加载是我们的目标。

产生这种错误的原因是 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 的出现大大改善了回调地狱,写法也更加接近同步。

简单来说,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。

  • resolve 的作用是将 Promise 的状态从“未完成”变成“解决了”,即异步操作完成,可以将结果作为参数传递给下一步。
  • reject 的作用是将 Promise 的状态从“未完成”变成“失败”,即异步操作失败,并将错误传递出去。

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" );
});

这种写法已经比回调函数的写法要直观多了,但是还是有一些嵌套,不够直观。

async/await 降临

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 的详细信息可以参考 http://www.ruanyifeng.com/blog/2015/04/generator.html

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,搞定。

one more thing

其实到上面那一步关于 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 咋一看异步,就认为加了就异步,这样很容易走入误区。有空多想想背后的故事,会有更深刻的认识,你我共勉。

相关代码

https://github.com/bob-chen/demos/tree/master/async-await-loadimgs

参考文章

Generator 函数的含义与用法

async 函数的含义和用法

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JetpropelledSnake

Linux学习笔记之Redis中5种数据结构的使用场景介绍

原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码。目前目标是吃透 redis 的数据结构。我们都知...

13010
来自专栏我的技术专栏

C++设计模式:Template Method

11930
来自专栏青玉伏案

关于Simple_html_dom的小应用

  今天一同学给我推荐了本书,说是刚出不久,内容还不错,是心灵鸡汤类的书,于是按捺不住就像在网上下一本,可是木有资源肿么办。只有在线看的,作为一个准码农,所以甭...

23070
来自专栏编程

java基础思维图解

Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。J...

22790
来自专栏数据和云

巧用SQL:Oracle中实现split相关方法总结

尚世波 从事数据库方面工作多年,专注于pl/sql开发、数据库设计、优化方面的研究,喜欢挑战 前文回顾:巧用SQL:oracle pl/sql split函...

41250
来自专栏量化投资与机器学习

【干货】Matlab的内存问题讨论

谢谢大家支持,可以让有兴趣的人关注这个公众号。让知识传播的更加富有活力,谢谢各位读者。 很多人问我为什么每次的头像是奥黛丽赫本,我只能说她是我女神,每天看看女神...

22980
来自专栏较真的前端

互动交流——第2期

小编:MDN是一个供Web开发者学习的网站,里面的知识点够全够深,个人觉得比W3cSchool强很多。。。

11330
来自专栏青玉伏案

设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习"模板方法模式"(Template Method Pattern)

今天是五.四青年节,祝大家节日快乐。看着今天这标题就有食欲,夏天到了,醋溜土豆丝和清炒苦瓜适合夏天吃,好吃不上火。这两道菜大部分人都应该吃过,特别是醋溜土豆丝,...

18390
来自专栏信数据得永生

JavaScript 编程精解 中文第三版 八、Bug 和错误

321100
来自专栏Java3y

【Java】几道让你拿offer的面试题

之前在刷博客的时候,发现一些写得比较好的博客都会默默收藏起来。最近在查阅补漏,有的知识点比较重要的,但是在之前的博客中还没有写到,于是趁着闲整理一下。

51100

扫码关注云+社区

领取腾讯云代金券