美文网首页
JavaScript异步和事件循环机制

JavaScript异步和事件循环机制

作者: 优质胡萝北 | 来源:发表于2021-01-06 18:24 被阅读0次

    JavaScript(以下简称js)的语言执行环境是单线程(single thread)的,这是其用途而决定的,作为浏览器脚本语言,js的主要用途是与用户互动,以及操作DOM,在iOS开发上的理解就是操作UI(只有主线程才能对UI进行操作)。所以为了避免语言的复杂性,单线程成为了js的核心特质,将来也不会轻易改变。

    但异步编程又非常重要,没有异步操作在进行耗时任务卡顿感会非常严重。所以目前JavaScript主要提供了三种异步方式:回调函数、事件监听、Promise对象。

    这篇文章我们将使用三种方式实现同一个登录网络请求的Mock方法,来模拟js的异步执行。

    回调函数

    在iOS开发中,我们的一般做法就是在参数中指定回调函数,直观并且容易理解。在js中做法可能更加自由,例如微信小程序OpenApi,直接指定参数为一个对象,对象分别指定相关参数和回调:

    wx.request({
      url: 'test.php', //仅为示例,并非真实的接口地址
      data: {
        x: '',
        y: ''
      },
      header: {
        'content-type': 'application/json' // 默认值
      },
      success (res) {
        console.log(res.data)
      }
    })
    

    我们使用回调函数的方式实现异步回调,代码片段如下:

    //声明
    function request_callback(url, param, success, fail){
        console.log('请求:' + url + '中...');
        setTimeout(() => {
            if (!error) {
                success(response, param);   
            }else{
                fail(error);
            }
        }, 2000);
    }
    
    //执行
    request_callback(url, params, 
        (response, params) => {
            console.log(response);
            console.log('用户名:' + params.name);
            console.log('密码:' + params.sec);
        },
        (error) => {
            console.log(error);
        }
    );
    

    事件监听

    该实现借鉴jQuery的trigger写法,类似于iOS开发中的消息通知(一对多)

    var listener;
    function request_trigger(url, params){
        console.log('请求:' + url + '中...');
        setTimeout(() => {
            if (!error) {
                listener.trigger('success', response);
            }else{
                listener.trigger('fail', error);
            }
        }, 2000);
    }
    
    listener.on('success', (response, params) => {
       console.log(response);
       console.log('用户名:' + params.name);
       console.log('密码:' + params.sec);
    });
    
    listener.on('fail', (error) => {
       console.log(error);
    });
    

    Promise

    比较官方的解释:Promise对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。

    个人理解,Promise就是对一个异步流程的封装,ES6为这种流程的封装提供了一种标准的方法,并取了一个好听的名字。Promise译为承诺,承诺那么就要有反馈,Promise就是规范了这种反馈的方式,由程序员决定反馈的时机。

    //声明
    function request_promise(url, params) {
        console.log('请求:' + url + '中...');
        //Promise 新建后就会立即执行
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (!error) {
                    resolve(response);
                }else{
                    reject(error);
                }
            }, 2000);
        });
    }
    
    //执行
    request_promise(url, params)
        .then((response) => {
            console.log(response);
            console.log('用户名:' + params.name);
            console.log('密码:' + params.sec);
        })
        .catch((error) => {
            console.log(error);
        });
    

    阮一峰大神在ES6入门中,举了一个使用Promise封装图片加载的例子,通过这个例子我们可以更深刻理解Promise对异步<流程>的封装,这种方式非常美妙:

    function loadImageAsync(url) {
        return new Promise(function(resolve, reject) {
            const image = new Image();
            image.onload = function() {
                resolve(image);
            };
            image.onerror = function() {
                reject(new Error('Could not load image at ' + url));
            };
            image.src = url;
        });
    }
    
    

    Promise实现异步流程控制

    解决异步回调,可是后台接口并不如我们前端程序员想的那么好用。如果我们要在请求登录之前,需要先请求init接口获取初始化数据,那代码会变成什么样?看下面这段代码:

    /**
     * 1. 请求init获取初始化数据
     * 2. 请求登录
     */
    request_callback(initUrl, params, 
        (response, params) => {
            //嵌套
            request_callback(url, params,
                (response, params) => {
                    console.log(response);
                    console.log('用户名:' + params.name);
                    console.log('密码:' + params.sec);
                },
                (error) => {
                    console.log(error);
                }
            )
        },
        (error) => {
            console.log(error);
        }
    )
    

    这样的嵌套方式,简直就是灾难,使用Promise的链式语法,可以将上述代码重构成这样:

    function requestInitUrl(url) {
        return new Promise((resolve, reject) => {
            console.log('请求' + url + '前需先请求' + initUrl);
            console.log('请求:' + initUrl + '中...');
            setTimeout(resolve, 4000, url);
        });
    }
    
    function request(url, params) {
        return new Promise((resolve, reject) => {
            console.log('开始请求...');
            setTimeout(resolve, 4000, url);
        });
    }
    
    //执行
    request(url, params)
        .then(requestInitUrl)
        .then(request_promise)
        .then((response) => {
            console.log(response);
            console.log('用户名:' + params.name);
            console.log('密码:' + params.sec);
        })
    

    Generator实现异步流程控制

    通过上个Section代码的实现,看样子是解决了问题,但是我们却需要实现一个requestInitUrl函数来封装第一次init请求,并且第二次请求的参数和调用并不在一起,总感觉不是很优雅。值得开心的是,ES6中提供了新的解决方案Generator,上述代码可以重构成这样,无需再重新封装一个初始化请求的Promise对象:

    function* fullRequest_generator(){
        yield request_promise(initUrl)
        .then((response)=>{
            console.log('初始化' + response);
        });
        yield request_promise(url, params)
        .then((response)=>{
            console.log('登录' + response);
        });
        return 'finish';
    }
    
    //执行
    var fr_g = fullRequest_generator();
    //错误的调用方式
    // fr_g.next();
    // fr_g.next();
    // fr_g.next();
    

    执行后我们会发现,程序执行的结果并不是我们想要的异步控制,所有的请求其实是【同步】执行的。

    所以,一个单纯的Generator并不能完成异步控制。Generator异步需要两个条件,一是每个yield表达式返回一个Promise对象,二是需要一个执行器。通过引入'co'模块,解决了异步执行的问题,将以上代码改为:

    //执行
    const co = require('co')
    co(fr_g)
    

    ps: co模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator函数的自动执行。

    Async/Await

    作为一个傲娇的程序员,引入别人的模块万一出问题还要去读源码,不是自己写的代码总有点不放心。所以在ES7中,提供了Generator函数的语法糖Async/Await解决了Generator异步执行的问题。Async函数的实现,就是将Generator函数和自动执行器,包装在一个函数里。我们将上述代码重构成如下:

    async function fullRequest_async(){
        console.log('开始任务');
        await request_promise(initUrl)
        .then((response)=>{
            console.log('初始化' + response);
        });
        await request_promise(url, params)
        .then((response)=>{
            console.log('登录' + response);
        });
        console.log('结束任务');
    }
    
    //执行
    fullRequest_async();
    

    看到以上代码的实现,是不是感觉非常优雅美丽并且酷炫,我们不用单独声明流程函数,也不用引入执行器,就完成了异步任务的流程控制,并且提升了代码的可读性。(๑•̀ㅂ•́)و✧

    如果要将任务结果抛出,则上述代码还可以重构为:

    async function fullRequest_async(comp, fail){
        console.log('开始任务');
        try {
            await request_promise(initUrl)
            .then((response)=>{
                console.log('初始化' + response);
            });
            await request_promise(url, params)
            .then((response)=>{
                console.log('登录' + response);
            })
            comp();
        } catch (error) {
            fail(error);
        }
    }
    
    fullRequest_async(
    ()=>{
        console.log('结束任务');
    },
    (err)=>{
        console.log(err);
        console.log('终止任务');
    });
    

    事件循环机制

    讲了代码的实现方式,让我们简单探究下js底层对异步任务的处理机制

    bg2014100802.png

    由于js的单线程机制,所以我们可以暂时不管线程这个概念,将所有即将执行的任务分为同步任务(synchronous),和异步任务(asynchronous)。同步任务指的是,在内存栈上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,当某个事件完成触发而进入"任务队列"(task queue)的任务。而读取任务队列中任务的操作,永远是在栈中任务执行完成后的。

    主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

    一个例子:

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();
    
    var req = new XMLHttpRequest();
    req.open('GET', url);        
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};
    

    如果用iOS开发的角度来看,我们是不确定onloadonerror回调时机的,但是在js的事件循环中,这两种写法是等价的

    总结

    从几种异步回调方式来看,不论是回调函数、事件还是Promise,免不了套娃的感觉。于我个人的开发习惯,比较喜欢使用类似微信小程序Api的回调方式,将参数和回调通过对象封装后传参的形式,这样的写法更加自由,清爽,并且容易控制(最主要的是作为一个iOSer毫无学习压力)。

    事件监听的方式可以将回调的实现和调用分离开,达到解耦的目的,逻辑分离所带来的问题就是代码理解成本、维护成本的提高。

    Promise封装了整个异步流程,相对于回调函数和事件,可以用更优雅的方式做更多的事情,例如多个异步任务的串行实现,但需要深刻理解Promise对象的实现机制,提高了学习成本。

    ES6提供的Generator函数将异步代码使用同步代码的形式表现,但并不能直接解决异步流程控制,需要引入或实现执行器。

    ES7提供的Async/Await语法糖,则进一步封装了整个流程,相比较Generator实现,无需实现执行器,task函数可直接执行,使代码书写更优雅,可读性更好。

    所以,在实际开发中,如果无需负责流程控制的异步操作或者需要提供给外部使用的Api建议直接使用回调函数的方式进行封装。若需要对异步流程进行过控制,则将异步操作封装成Promise对象,用Async/Await的方式进行控制

    相关文章

      网友评论

          本文标题:JavaScript异步和事件循环机制

          本文链接:https://www.haomeiwen.com/subject/oixgoktx.html