JavaScript异步和事件循环机制

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的方式进行控制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容