JS异步编程方案总结

前言

Javcscript是单线程机制,单线程模型指的是,JavaScript只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。JavaScript 之所以采用单线程,而不是多线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。

异步编程解决了什么问题?

单线程的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段JavaScript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决JavaScript执行任务只能一个一个排队执行得问题(同步执行)引入了异步编程方案来实现(异步并行执行任务),对于几种常见异步编程方案有:

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise对象
  • Generator/yield(ES6)
  • async/await(ES7)

同步和异步任务

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

  • 同步任务:是指那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

  • 异步任务:是指那些被引擎放在一边,不进入主线程、而进入任务队列的任务。

只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。JavaScript运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

下面总结一下异步操作的几种模式。

1、回调函数

回调函数是异步操作最基本的方法,一般指函数里面嵌套函数来调用其他函数。

function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);
  • 优点:简单、容易理解和实现。
  • 缺点:不利于代码的阅读和维护,容易形成‘回调地狱’,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

2、事件监听

事件监听是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

f1.on('done', f2);

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done'); // 触发done事情
  }, 1000);
}

上面代码使用JQuery编写, 首先,为f1绑定一个事件,当f1发生done事件,就执行f2,而f1执行完成后,立即触发done事件,从而开始执行f2

  • 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。
  • 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。

3、发布/订阅

事件完全可以理解成信号,如果存在一个信号中心,某个任务执行完成,就向信号中心发布一个信号,其他任务可以向信号中心订阅这个信号,从而知道什么时候自己可以开始执行。这就叫做发布/订阅模式,又称观察者模式

// 订阅信号
jQuery.subscribe('done', f2);

function f1() {
  setTimeout(function () {
    // ...
    // 向信号中心jQuery发布done信号,从而引发f2的执行
    jQuery.publish('done');
  }, 1000);
}

// 取消订阅
jQuery.unsubscribe('done', f2);

上面代码使用JQuery编写,首先,f2向信号中心jQuery订阅done信号。f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。

这种方法的性质与事件监听类似,但是明显优于后者。因为可以通过查看消息中心,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

4、Promise

Promise 实际就是一个对象, 从它可以获得异步操作的消息,Promise 对象有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise 的状态一旦改变之后,就不会在发生任何变化,将回调函数变成了链式调用。

Promise 的设计思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,用来指定下一步的回调函数。

var p1 = new Promise(f1);
p1.then(f2);

上面代码中,f1的异步操作执行完成,就会执行f2

function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
        // f1的任务代码
        dfd.resolve();
    }, 500);
    return dfd.promise;
}

每次调用返回的都是一个新的Promise实例(then可用链式调用的原因)

// 传统写法
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // ...
      });
    });
  });
});

// Promise 的写法
(new Promise(step1))
  .then(step2)
  .then(step3)
  .then(step4);

传统的写法可能需要把f2作为回调函数传入f1,比如写成f1(f2),异步操作完成后,在f1内部调用f2Promise使得f1f2变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。

5、Generator

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)

Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,使用该对象的 next() 方法,可以遍历 Generator 函数内部的每一个状态,直到 return 语句。

Generator 函数的特征:

  • function 关键字与函数名之间有一个星号
  • 函数体内部使用 yield 表达式,yield 是暂停执行的标记
  • next() 方法遇到 yield 表达式,就暂停执行后面的操作,并返回后面的值。
function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值3。第二个 next 方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)。

6、async/await

async 函数就是 Generator 函数的语法糖。async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

async/await的特征有:

  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/awaitPromise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
async function async1() {
  return 2
}
console.log(async1()) // Promise {<resolved>: 2}

上面代码中,函数前面加上 async 就会返回一个 promise 对象。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};
// Generator 函数写法
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// aynsc 函数写法
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async函数就是将Generator函数的星号*替换成async,将yield替换成await,仅此而已。

优点:更好的语义,更广的适用性,返回值是 Promise。

总结

JS 异步编程发展史:callback -> promise -> generator -> async + awaitasync/await函数的实现是将Generator函数和自动执行器,包装在一个函数里。它也是目前异步最好的解决方案了。

更多优质文章可以访问GitHub博客,欢迎帅哥美女前来Star!!!

参考文章

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

推荐阅读更多精彩内容

  • 前言 我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须...
    浪里行舟阅读 14,264评论 1 27
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,707评论 0 5
  • 一、Javascript实现异步编程的过程以及原理 1、为什么要用Javascript异步编程 众所周知,Java...
    Ebony_7c03阅读 855评论 0 2
  • 前言 最近,小伙伴S 问了我一段代码: const funB = (value) => { console.lo...
    飞奔小码农阅读 370评论 0 0
  • 前言 编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续...
    李向_c52d阅读 1,066评论 0 2