实现JS异步的多种方案详解

前言

我们都知道js是属于单线程执行的,单线程及代表程序中的所有任务都按顺序依次执行,在代码体量小,运行场景单一的情况下并不会暴露问题,但是,当其中一个任务执行中,下面的任务都无法得到执行,给客户端展示出的样子就是浏览器卡住,或者是‘假死’,这样显然是行不通的。

为解决以上问题,Javascript语言将任务的执行模式分成两种:同步和异步。下面我们就来梳理下异步的多种实现方式吧。

同步任务

同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。

异步任务

异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务

function fun1() {
  console.log(1);
}
function fun2() {
  console.log(2);
}
function fun3() {
  console.log(3);
}
fun1();
setTimeout(function(){
  fun2();
},0);
fun3();
 
// 输出
1
3
2

异步的执行

JavaScript的执行过程中,先按顺序执行程序中的同步任务,当遇到异步任务时,将异步任务添加到异步队列中,继续执行程序中的同步任务;

当同步任务执行完成,进入到异步队列中执行,优先执行排在前面的任务,将任务放入执行栈中;

每当执行栈任务被清空,都会去异步队列中查找任务,直到有新任务添加,以此往复,这被称作任务循环,因为每一个任务都是由事件触发,所以又称作事件循环

image-20200623114210987

1、所有同步任务都在主线程上执行,行成一个执行栈
2、主线程之外,还存在一个任务队列,只要异步任务有了结果(与主线程同时进行),就会在任务队列中放置一个事件
3、一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列(event queue),看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
4、主线程不断的重复上面的第三步

在上图中的异步执行过程中,在异步队列中有多种异步方式,如:ajax()、setTimeout()、onClick()、Promise(),其中ajax()、setTimeout()、onClick()这些都是通过回调函数来实现的异步的;

Promise则是ES6提供的异步实现方式;

不同的异步方式执行顺序也是不同的,这块涉及到了我们的宏任务微任务,其微任务都是优先于宏任务执行的;

下面我们一起来看下JS的多种异步的实现方式。

异步的实现方式

回调函数(callBack)

回调函数大家都不陌生,例如dom添加事件监听,定时器异步

element.addEventListener('click',function(){
    //response to user click    
});
setTimeout(function(){
    //do something 1s later 
}, 1000);

callBack(function(){
  callBack1(function(){
    callBack2(function(){
      callBack3(function(){
        ...
        ...
      })
        })
  })
})

回调函数的方式不适用于复杂的业务逻辑中,回调函数之间的嵌套层级太多就容易进入回调地狱,不利于代码的阅读和解耦和维护;

promise

E6提供的Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。

Promise是js异步编程的解决方案,Promise是一个对象,内部会存在一个异步操作,Promise对象提供统一的api来获取异步操作的结果。

const promise = new Promise(function(resolve, reject) {
  // ... some code
    // 执行异步操作
  // ...
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
  // return other promise 嵌套回调
}, function(error) {
  // failure
})
.then(function(data){
  // success
});

引用官方流程示例:

image-20200616163453572

流程解析:创建Promise对象,传入一个立即执行函数executor,将resolved和rejected方法作为参数传入,初始状态为“pending”,异步执行完成触发Promise对象的then方法,将传入resolved和rejected做为参数传入,如果拿到resolved,则执行resolved(),状态变为“resolved”,失败或者被拒绝触发rejected(),状态变为“rejected”;

使用Promise,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数;

下面我们看一下setTimeout和promise任务执行顺序:

let promise = new Promise(function(resolve, reject){
    console.log("1");
    resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");
//输出
// 1,4,3,2
let promise = new Promise(function(resolve, reject){
  console.log("1");
  resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => {console.log("5");setTimeout(()=>{console.log("3")},100)});
console.log("4");
// 1,4,5,2,3

注:这块涉及到任务队列分为宏任务和微任务队列,执行栈清空后会优先去查找微任务队列,没有的话才去查找宏任务队列;

宏任务和微任务的小测试

image-20200617171650050

思考流程:

遍历执行栈,查找到1,7,执行栈清空,去查找微任务:6,8,执行栈清空,再次查找微任务:无,查找宏任务:2,4,执行栈清空,查找微任务:3,5,执行栈清空,查找微任务:无,查找宏任务:9,11,执行栈清空,查找微任务:10,12

Promise api

1、Promise.resolve() 有时需要将现有对象转为Promise对象,这个方法就起到这个作用

2、Promise.reject() 也是返回一个Promise对象,状态为rejected

3、then()

4、catch() 发生错误的回调函数

5、Promise.all() 适合用于所有的结果都完成了才去执行then()成功的操作,相当于且

​ 6、Promise.race() // 完成一个即可,相当或

Promise功能固然强大,但是依旧无法摆脱一旦函数开始执行,就必须等到所有任务执行完毕才结束。

Generators

ES6中新增了generator函数,不同于普通的函数,它具有如下特点

(1)分段执行:generator函数允许在运行的过程中暂停一次或多次,随后再恢复运行。暂停的过程中允许其它的代码执行。

(2)yield暂停:普通函数在开始的时候获取参数,在结束的时候return一个值,而generator函数可以在每次yield的时候返回值,并且在下一次重新启动的时候再传入值。

(3)next()继续执行:将上次yield返回的值作为参数传入继续执行。

使用语法

// 在function name前加*标识是generator函数,通过调用next()方法
function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}
var it = foo();
console.log(it.next()); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }
console.log( it.next() ); // { value:undefined, done:true }

注:每次遍历到关键字yield就执行return 返回yield后面的值;

function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// 注意这里在调用next()方法时没有传入任何值
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }

注:next传入的值是上一次yield上次return的值

首次遍历到yield return yield(x+1),x的值为调用it时传入的参数;

再次遍历时,next传入12,代表上一次yield(x+1)=12;y=2*12;遇到yield(24/3),return 8;

第三次遍历,next传入13,代表上一次yield(y/3)=13,则z=13,x+y+z=5+24+13 = 42,所以return42;

以上内容只是对Generator函数做了一个初级的讲解,有兴趣的可以深入研究下它的语法糖,Generator 函数是比Promise写法稍微科学的一种写法,可以暂定可以继续,处理起来异步更加人性化,当然了,async/await写法才是终极大法。

async/await

ES7的语法糖,我们来根据名字看下,async可以理解为异步,await理解为等待异步,那么它的优势是什么呢?

上面讲过,我们用Promise的.then()来处理多步链式回调的问题,每一步回调都依赖上一次返回的值,走到这里,是不是想起上面讲过的Generator函数的.next(),对于比较简洁的单层链式调用,用户可能不会有太大困惑,但是对于n级回调的情况下,如果是Promise的话是如何书写的呢,让我们参考下这个小例子:

Promise写法:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

上面的写法相较于回调嵌套来说肯定是可视化程度更高,但是结合了PromiseGenerator优势的async/await是如何优化的呢?

async/await写法:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

对比两个例子是不是觉的Promise的传递参数特别麻烦,async/await它的内部封装了Promise和Generator的组合使用方式,代码结构更清晰,更加语义化,在复杂的回调中优势很明显,从代码的可读性到开发的便捷性都有了很大的提升;

语法分析

async和await的用法有一个规范,就是await要放到async中来等待执行,第一次看到这,感觉比较奇怪,开始我认为async和await应该是同级的,那下面我们具体来看下为什么要按照这种规范走:

async function testAsync() {
  return "hello async";
}

const result = testAsync();
console.log(result);

通过上面的代码示例,我们输出了这个async,发现输出的是一个Promise对象,此时它的状态为“resolved”,在resolved方法中输出了异步return的值,原理用的还是我们的Promise方法,当前的await相当于Promise.then()中执行的回调resolved();

image-20200618201329050

如果想要深入去了解async/await,可以结合Promise和Generator手动实现一个async/await。

总结

以上内容都是对各种异步实现方式的一个简单的讲解,感兴趣的同学可以继续深入研究它们的实现方式及原理,异步有多种实现方式,这些方式并没有说哪种最好,在实际业务开发中还是要结合不同的场景来选择不同的实现方式,最终的目的是只要使用方便,可读性强,便于维护。

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

推荐阅读更多精彩内容