手记

关于JQuery的$.Deferred的两个使用例子

最近在观摩大神项目的时候接触到了JQuery的$.Deferred对象,网上搜搜看看了几天的$.Deferred对象以及JS单线程的知识,觉得颇有收获,所以写篇手记记录一下这段时间学习的收获。


先总结一下对应的知识点:

一、什么是deferred对象?

开发网站的过程中,我们经常遇到某些耗时很长的javascript操作。其中,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。

通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。

但是,在回调函数方面,jQuery的功能非常弱。为了改变这一点,jQuery开发团队就设计了deferred对象。

简单说,deferred对象就是jQuery的回调函数解决方案。在英语中,defer的意思是"延迟",所以deferred对象的含义就是"延迟"到未来某个点再执行。

二、$.Deferred如何使用?

关于$.Deferred的用法,网上有很多教程了。也可以直接去JQuery的文档那里查阅。

三、什么是同步和异步?

虽然JS运行在浏览器中,是单线程的,但浏览器不是单线程的。浏览器中很多异步行为都是由浏览器新开一个线程去完成。javascript引擎线程是浏览器多个线程中的一个,它本身是单线程的。浏览器还包括很多其他线程,如界面渲染线程,浏览器事件触发线程,Http请求线程等。所以,所谓的javascript是单线程的,是指javascript运行在浏览器中是单线程的,叫做javascript引擎线程。

我们不妨把负责解释和执行JavaScript代码的线程叫作主线程

把处理AJAX请求、处理DOM事件、定时器、读写文件等线程,叫作工作线程。这些线程可能存在于JS引擎之内,也可能存在于浏览器。

Javascript语言将任务的执行模式分为同步和异步两种。

同步:当有多个任务的时候,主线程会将任务排队按顺序执行,一次只能执行一个任务。在等待任务完成的过程中,当前执行中的任务会阻塞接下来的一系列任务。

异步:当一个任务执行的时间较长并且需要等待时,主线程会将任务挂起,放进“任务队列”里,把任务队列里的任务交给浏览器新开的线程去执行,然后继续执行主线程的其他任务,当主线程的全部任务执行完毕,并且“任务队列” 通知主线程,某个异步任务可以执行了,主线程才会执行“任务队列”里的异步任务。


最后分享一下最近遇到的两个用到$.Deferred对象的例子:

情景1:当用户按下删除弹窗的确定或取消后,把弹窗隐藏,并执行对应的操作(删除或不执行),因为我们不知道用户什么时候会点击按钮,所以不能让弹窗阻塞其他任务的执行。

关键代码如下:

function pop(arg) {
    if (!arg) {
        console.error('pop title is empty');
    }

    var dfd = $.Deferred() //实例化一个延迟对象
           ,confirmed //记录按下确定或取消
           ,$confirm = $content.find('button.confirm') //确认按钮
           ,$cancel = $content.find('button.cancel'); //取消按钮

    //定时器轮询,当按下确定或取消时触发删除或取消操作
    timer = setInterval(function() {
        if (confirmed !== undifined) {
            dfd.resolve(confirmed);
            clearInterval(timer);
            dismiss_pop();
        }
    },50);

    //点击确定时更改confirmed状态
    $confirm.on('click', function() {
        confirmed = true;
    });

    //点击取消时更改confirmed状态
    $cancel.on('click', function() {
        confirmed = false;
    });

    //返回dfd对象
    return dfd.promise();
}

$('.delete').click(function() {
    var $this = $(this);
    var index = $this.data('index'); //当前的id
    //确定删除
    pop('确定删除?').then(function(res){
        res ? delete_task(index) : null;
    })
})

情景2:最近在看项目代码的时候,发现有这么一个需求:把html转成图片,再压缩成zip文件保存到后台供用户下载。

技术难点1:如何把html转成图片?

答:把HTML转成图片的方式其实挺多的。前端html2canvas和dom-to-image,后端有phantomjs.exe等都可以实现浏览器截图。这里大神调用了dom-to-image.jsJSZip.js分别实现html转成图片和压缩成zip文件的功能。其中如果HTML信息量很大的时候,生成图片是一个耗时的操作,所以用到延迟对象。

技术难点2:那么如何用ajax上传文件呢?

答:通过传统的表单form上传文件会刷新页面,而且当前也没有表单可以上传,所以使用html5的FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,使用FormData的最大优点就是我们可以异步上传一个二进制文件,使用 ajax 上传文件时要将 contentType = false。

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo</title>
    <script src="__STATIC__/js/jquery-1.9.1.min.js"></script>
    <script src="__STATIC__/js/dom-to-image.js"></script>
    <script src="__STATIC__/js/jszip.min.js"></script>
    <script src="__STATIC__/js/FileSaver.js"></script>
</head>
<body>
    <button id="run" type="button">run</button>
    <br/>
    <img src="__STATIC__/img/pic1.jpg" >
    <img src="__STATIC__/img/pic2.jpg" >
    <img src="__STATIC__/img/pic3.jpg" >
    <img src="__STATIC__/img/pic4.jpg" >

</body>
<script>
        $(function() {
        function toImage(num,node,zip) {
            var dfd = $.Deferred();
            //console.log('weak'+num);
            domtoimage.toJpeg(node)
                .then(function (dataUrl) { //domtoimage.toJpeg成功后的回调
                    var data = dataUrl.split('base64,'); //处理domtoimage返回的base64数据
                    if (data[1]) {
                        zip.file('pic' + num + '.jpg', data[1], {base64: true}); //文件保存到zip,此时文件在内存中
                        dfd.resolve(num);
                    } else {
                        dfd.reject('Generate pic' + num + ' fail');
                    }
                });
            return dfd;
        }

        function toZip() {
            //生成zip文件
            zip.generateAsync({type: "blob"}).then(function (content) {
                //saveAs(content,'test.zip'); //调用FileSaver.js的saveAs方法可以把zip文件下载下来
                var formData = new FormData(); //实例化一个html5的formData对象
                var file = new File([content], 'test.zip'); //实例化一个html5的File对象
                formData.append("file", file);
                formData.append("flieName", 'test.zip');
                $.ajax({
                    url: "{:url('index/upload')}",
                    type: "POST",
                    data: formData,
                    processData: false, //必须将processData标志设置为false,否则,jQuery会尝试将FormData转换为字符串,否则将失败。
                    contentType: false, //false才会避开jQuery对 formdata 的默认处理
                    cache: false //禁止使用缓存的结果
                })
                .done(function (res) {
                    if (res.status == 1) {
                        console.log('压缩文件上传成功');
                        console.log('压缩文件上传时间:'+Date.parse(new Date()));
                    } else if (res.status == 0) {
                        alert(res.msg);
                    }
                })
                .fail(function () {
                    console.log('error');
                })
            })
        }

        function start() {
            console.log('Begin to generate picture');
            console.log('Begin time:'+Date.parse(new Date()));
            var imgs = $('img');
            imgs.each(function(k,v) {
                console.log(k); //打印执行图片的序号
                var res = toImage(k, v, zip);
                $.when(res).done(function(num) {
                    console.log('Pic' + num + ' Generate time:'+Date.parse(new Date())); //打印生成图片的时间
                    count++;
                    if (count == total) { //当执行了total次时,生成zip文件,zip文件存在于内存中,直到调用ajax把文件保存到后台服务器中
                        toZip();
                    }
                }).fail(function (data) {
                    console.log(data);
                });
            })
        }

        var imgs = $('img');
        var total = imgs.length;
        var count = 0;
        var dfds = [];
        var zip = new JSZip();
        imgs.each(function (k, v) {
            // 图片较大时需要等待加载完毕
            var dfd = $.Deferred();
            v.onload = function () {
                console.log('Pic' + k + ' Loaded time:' + Date.parse(new Date()));
                dfd.resolve();
            };
            dfds.push(dfd);
        });

        $('#run').click(function() {
            // 所有图片加载完就去生成图片
            $.when.apply(null, dfds).done(function () { //apply继承$.when并把数组参数传进去
                start();
            });
        });
    });
</script>
</html>

后端代码:

<?php
namespace app\index\controller;

use think\Controller;
class Index extends Controller
{

    public function index()
    {
        return $this->fetch();
    }

    public function upload()
    {
        $file = request()->file('file');
        if ($file) {
            $file->move(ROOT_PATH . 'public/');
        } else {
            return json(['status'=>0,'msg'=>'no file']);
        }
        return json(['status'=>1,'msg'=>'success']);
    }

}

之前由于图片没加载完就用dom-to-image去生成图片了,导致图片都是黑的,所以加了个延迟,判断图片都加载完毕的时候才去执行生成图片的操作start()。

图片加载完毕,进入生成图片的主线程(start)。对imgs进行遍历(一共4张图片),console.log(k)之后,就调用了toImage()函数进入“任务队列”异步生成图片,当图片生成之后,会返回dfd.resolve()给$.when,当$.when接收到dfd时,打印生成图片的时间。

由于toImage()是一个耗时的函数,所以主线程把这个耗时的操作丢给“任务队列”之后,马上就进行下一张图片的遍历了,所以看到控制台打印了0,1,2,3之后,由于没有其他的任务要执行,所以主线程就在等待“任务队列”返回的异步任务(打印图片生成的时间)了。
借用一张图:

参考链接:
jQuery的deferred对象详解
JavaScript:彻底理解同步、异步和事件循环(Event Loop)
如何证明JavaScript是单线程的?
浅谈contentType = falseJQuery/Ajax Form提交

2人推荐
随时随地看视频
慕课网APP