JS的异步操作手札

写在前面:在语言级别上,Javascript是单线程的,然而很多情况下需要异步操作,因此异步编程对其尤为重要。

基本方法

  • 回调函数
  • 事件监听(事件发布/订阅)
  • Promise对象
  • Generator函数(协程coroutine)
  • async和await
    ---------------------------------我是正式开始的分隔符(๑•̀ㅂ•́)و✧--------------------------------

1.回调函数

// 基本方式
let fun = (time, callback) => {
  setTimeout(() => {
    callback()
  }, time);
}
fun(2000, function(){
  console.log('两秒钟到了')
})

分析:回调函数可以初步得解决异步编程,在回调次数小于两次的时候能够发挥相当好的结果,但是随着业务逻辑的增加和趋于复杂,一旦遇到“回调地狱”的情况,重构代码将非常困难。回调函数还有一个问题就是我们在回调函数之外无法捕获到回调函数中的异常。

// 回调地狱
  doSomethingAsync1(function(){
      doSomethingAsync2(function(){
          doSomethingAsync3(function(){
              doSomethingAsync4(function(){
                  doSomethingAsync5(function(){
                      // code...
                  });
            });
         });
     });
 });

捕获异常:一般情况下,一般会使用try/catch语句捕获异常。但是为什么异步代码的回调函数中的异常无法被最外层的try/catch语句捕获?
原因是:异步调用一般分为两个阶段,提交请求和处理结果,这两个阶段之间有事件循环的调用,它们属于两个不同的事件循环(tick),彼此没有关联。异步调用一般以传callback的方式来指定异步操作完成后要执行的动作。而异步调用本体和callback属于不同的事件循环。而try/catch语句只能捕获当次事件循环的异常,对callback无能为力。

2.事件监听(事件发布/订阅)

//发布和订阅事件
let evt = document.createEvent("event"); // 创建自定义事件
 evt.initEvent("click", true, true); // 初始化完毕,即发布
 window.addEventListener("click", function(e){ // 监听事件,即订阅
     console.log(1);
 });

 window.dispatchEvent(evt); //派发事件

分析:事件监听是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。通常情况下,我们需要考虑哪些部分是不变的,哪些是容易变化的,把不变的部分封装在组件内部,供外部调用,需要自定义的部分暴露在外部处理。从某种意义上说,事件的设计就是组件的接口设计。所以这种事件监听处理的异步编程方式特别适合于一些需要高度解耦的场景。

3.Promise

//Promise基本语法和功能
function runAsync(a){
    var p = new Promise(function(resolve, reject){
            resolve('异步');
    });
    console.log('我先被执行')
    return p;            
}
runAsync().then(function(data){
    console.log(data);
});

分析:Promise是ES6里提供的一个对象,它可以扮演一个“先知”的角色在监控代码的异步操作。Promise对象有三种状态:Pending(进行中)、Resolved(已完成)、Rejected(已失败)。在构造Promise对象之后,就可以调用then方法,此方法接收一个参数,是函数,并且会拿到在构造函数中调用resolve时传的的参数。简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。这看起来和回调函数的方式很像,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。在promise身上还有几个重要的方法:reject、catch、all、race、finally、try;

reject的用法

当出现“失败”状态的情况,需要用到reject。它的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。

function getNumber(num){
    var p = new Promise(function(resolve, reject){
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
    });
    return p;            
}

getNumber(6) 
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

catch的用法

Promise对象除了then方法,还有一个catch方法,其实它和then的第二个参数一样,用来解决reject的回调,但不同的是,它能够对代码异常进行处理,从而不至于让代码报错停止运行。并且在catch之后可以继续.then();

function getNumber(num){
    var p = new Promise(function(resolve, reject){
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
    });
    return p;            
}
getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

all的用法

Promise的all方法提供了并发执行异步操作的能力,并且在所有异步操作执行完后才执行回调。所谓并发可以理解为一起执行但互不干涉,但并不是同时。用Promise.all来执行,all将接收一个数组参数,里面的值最终都算作返回Promise对象。(“算作”的意思就是all接受的参数并不需要是一个Promise对象)这样,三个异步操作的并行执行的,等到它们都执行完后的数据会放进一个数组中并传入到then里面。这种方法尤其适合于预加载资源的使用情景里。

let a = function () {
  return 'a';
}
let b = function () {
  return 'b';
}
Promise
.all([a(), b()])
.then(function (results) {
  console.log(results);
});

race的用法

在all的方法里,我们可以从另外一个角度理解其运行的机制,那就是以Resolved状态的对象最后执行完的时间点作为方法返回的时间节点,所以与all不同的是,race方法是以Resolved状态的对象最早执行完的点作为方法返回的时间节点。这种情况可以运用在请求资源是否超时不确定的时候。所以无论是all还是race,一旦遇到失败状态便立即停止。

function requestImg() {
   let p = new Promise(function (resolve, reject) {
   let img = new Image();
   img.onload = function () {
   resolve(img);
}
   img.src = 'xxxxxx';
});
  return p;
}
 //延时函数,用于给请求计时
function timeout() {
  let p = new Promise(function (resolve, reject) {
  setTimeout(function () {
    reject('图片请求超时');
  }, 5000);
});
  return p;
}
Promise
.race([requestImg(), timeout()])
.then(function (results) {
  console.log(results);
})
.catch(function (reason) {
  console.log(reason);
});

finally的用法

finally方法用于指定不管 Promise 对象最后状态如何,都会执行finally的操作指定的回调函数。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这也表明,finally方法里面的操作,是与状态无关的,不依赖于 Promise 的执行结果。所以finally本质上是then方法的特例。

Promise.reject(2)
.catch((data) =>{
  console.log(data);
})
.finally(() => { 
  console.log(1);
})

try的用法

在实际编程里不知道或者不想区分,某函数f是同步函数还是异步操作,但是想用Promise来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。所以一般就会采用下面的写法。

let f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');

以上的写法容易造成一个结果就是,同步函数却被异步执行了,因此会在事件循环的末尾执行。为解决这个问题,Promise对象提供了try方法,可以同时保证执行对象是否需要异步执行。

    const f = () => console.log('now');
    Promise.try(f);
    console.log('next');

3.Generator函数

Generator函数特征
(1)function 关键字和函数之间有一个星号(*),且内部使用yield表达式,定义不同的内部状态。
(2)调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
Generator 函数在语法上可以理解成是一个状态机,内部封装了多个状态。虽然在形式上,它是一个普通函数。但整个Generator函数是一个封装了异步任务的容器,在异步操作需要暂停的地方,使用yield语句。

function* fn(){   // 定义一个Generator函数
    yield 'hello';
    yield 'world';
    return 'end';
}
var f1 =fn();          
console.log(f1);    
console.log(f1.next()); 
console.log(f1.next()); 
console.log(f1.next()); 
console.log(f1.next());

分析:yiled语句执行暂停的效果,若要进行下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。即:每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。所以Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

yield使用的注意点
1)yield语句只能用于function* 的作用域,如果function* 的内部还定义了其他的普通函数,则函数内部不允许使用yield语句。
2)yield语句如果参与运算,必须用括号括起来。

// 定义一个Generator函数
function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
//return 8;
}
var a = foo(5);
console.log(a.next());
console.log(a.next(1));
//console.log(a.next());

从以上的例子我们还可以看出next()方法表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以可以不用带有参数。

return和yield的区别:
1)return终结遍历,之后的yield语句都失效;next返回本次yield语句的返回值。
2)return没有参数的时候,返回{ value: undefined, done: true };next没有参数的时候返回本次yield语句的返回值。
3)return有参数的时候,覆盖本次yield语句的返回值,也就是说,返回{ value: 参数, done: true };next有参数的时候,覆盖上次yield语句的返回值,返回值可能跟参数有关(参数参与计算的话),也可能跟参数无关(参数不参与计算)。

迭代对象
自动遍历 Generator 函数的迭代对象,此时可不再需要调用next方法。一旦next方法的返回对象的done属性为true,迭代循环就会中止,且不包含该返回对象。

//输出斐波那契数列
function *fibonacci(){
    let [pre, cur] = [0,1];
    for(;;){
        [pre, cur] = [cur, pre+cur];
        yield cur;
    }
}
for(let n of fibonacci()){
    if( n>1000 )
        break;
    console.log(n);
}

4.async/await

async
async/await虽然是ES7的关键字,但是通过编译器的解译,也是可以正常使用。async/await的语法很简洁,直接作为一个关键字放到函数前面,用于表示该函数是一个异步函数。

async function timeout() {
    return 'hello world'
}
console.log(timeout());

分析:可以看出其实async 函数返回的是一个promise 对象,即使其中包含非promise。所以,async内部执行的逻辑机理与promise是一致的。因此上述例子相当于

async function timeout() {
    return Promise.resolve('hello world')
}

继续看

async function timeout(flag) {
    if (flag) {
        return Promise.resolve('hello world')
    } else {
        Promise.reject('error')
    }
}
timeout(true).then((res) => {
  console.log(res)
}) ;
timeout(false).catch(err => {
    console.log(err)
})

await
await的语法是将其放在async 函数中,表示暂停的意思。可以将异步代码像同步代码一般书写了。

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

推荐阅读更多精彩内容

  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 6,376评论 9 19
  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,308评论 5 22
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,710评论 0 5
  • 在此处先列下本篇文章的主要内容 简介 next方法的参数 for...of循环 Generator.prototy...
    醉生夢死阅读 1,440评论 3 8
  • 计算机会议排名等级:https://blog.csdn.net/cserchen/article/details/...
    顾北向南阅读 1,191评论 0 0