JavaScript异步编程方案回顾

Posted on:2019-10-9 02:16
13 min read
    技术学习 JavaScrtipt

如标题所述,本文主要是重新梳理JavaScript的异步编程方案,部分内容来自网络

同步

在开始之前,先简单说说JavaScript中的同步。先来个两个简单的例子:

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('危!');

  let pElem = document.createElement('p');
  pElem.textContent = '哈哈哈';
  document.body.appendChild(pElem);
});

这段代码, 一行一行的顺序执行:

  1. 先取得一个在DOM里面的 <button> 引用。
  2. 点击按钮的时候,添加一个 click 事件监听器:
  3. alert() 消息出现。
  4. 一旦alert 结束,创建一个 <p> 元素。
  5. 给它的文本内容赋值。
  6. 最后,把这个段落放进网页。

再比如,模拟一个现实中网页可能遇到的情况:因为渲染UI而阻塞用户的互动,这个例子有2个按钮:

function expensiveOperation() {
  for(let i = 0; i < 1000000; i++) {
    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
    ctx.beginPath();
    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
    ctx.fill()
  }
}

fillBtn.addEventListener('click', expensiveOperation);

alertBtn.addEventListener('click', () =>
  alert('You clicked me!')
);
  1. 点击的时候用1百万个蓝色的圆填满整个<canvas>
  2. 点击显示 alert 消息

在浏览器中只有一个主线程(main thread)能够任务,也就是我们常说的JavaScript是单线程的(single threaded)。在一个线程中,任务只能one by one 执行,这也就是所谓的“同步阻塞”。

但是在实际场景中,只是同步无法满足日常需求,比如用户代理(User Agent)提供了一些API特性,特别是从外部的设备上获取资源,譬如,从网络获取文件,访问数据库,从网络摄像头获得视频流等等,这些都是耗时操作。同步则一定导致阻塞,在用户使用的角度上来说,感受不够友好。

接下来回顾一下JavaScript中异步解决方案

老派Callback

以DOM事件和XMLHttpRequest为代表

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = type;

  xhr.onload = function() {
    callback(xhr.response);
  };

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

上述两个例子中,都会将callback注册到EventLoop的任务队列中,这两类都属于宏任务。在 main thread 空闲时执行。

传统的callback处理方式容易陷入回调地域

5d5b4b180001a9f311761290.jpeg

Promise

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation

Promise 对象本质上表示的是一系列操作的中间状态,或者说是未来某时刻一个操作完成或失败后返回的结果。Promise并不保证操作在何时完成并返回结果,但是保证在当前操作成功后执行您对操作结果的处理代码,或在操作失败后,优雅地处理操作失败的情况。

早在 1980 年代,早期的 promise 和 future(类似/相关概念) 实现开始出现。单词 “promise” 的使用由 Barbara Liskov 和 Liuba Shrira 在 1988 年创造 wiki:Future and promises。Liuba的论文pdf版本

CommonJS组织提出了 Promises/A 制定异步模式编程规范,最早的时间可以追溯到2010年。比较出名的实现有: QjQuery

我第一次在 JavaScript 中听说 promise 时,在2015年实习阶段使用Angular的时候。在此之前也稍微接触了jQuery的Defferd对象。典型的案例就是使用jQuery提供的ajax方法,举个例子:

var jqxhr = $.ajax( "example.php" )
  .done(function() {
    alert( "success" );
  })
  .fail(function() {
    alert( "error" );
  })
  .always(function() {
    alert( "complete" );
  });
 
jqxhr.always(function() {
  alert( "second complete" );
});

由于在那段时间 jQuery 极其流行,它迅速成为了使用最广泛的 promise 实现。jQuery 正式使得 JavaScript promise 成为主流。一些更好的独立 promise 库如 Q、When、Bulebird 开始流行。jQuery 的一些不一致的实现驱使 Promise 标准做了一些重要的阐明,重写并更名为 Promises/A+

历史在发展,时代在进步。经过一段时间各种Libary的锤炼,ES6 中也新增了Promise。不过要注意一点。ES6中的Promise规范完全覆盖了的Promise/A+规范。Promise/A+规范设计成最小规范。ES6的Promise规范完全兼容A+,同时又包含诸如catch,finially等特性

关于Promise的使用本文不再赘述,网上已经有很丰富的资料内容了。

Generator

类似的,Generator的概念也是计算机科学中普遍存在的概念之一

In computer science, a generator is a routine that can be used to control the iteration behaviour of a loop.

其本质是一个可以控制一个循环迭代行为的子程序(routine)。可以理解就是函数。可以简单的认为它是一个返回数组的函数。在ES6中,也把它翻译成生成器函数。讨论Generator必然离不开协程。协程又称微线程,纤程,英文名 Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.

协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用。我们也正是利用协程随时切换的特性来实现异步编程。

在真正使用Generator来实现异步编程时,你还得先搞清楚什么是Iterator Protocal,什么是Iteratable Protocal。同样本文也不再赘述。虽然从根本上来说Generator诞生之初并不是为了解决异步任务,但是不妨碍广大前端朋友的机智创新。举个简单例子:

let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
  console.log(data) //data=>2.txt
  let { value, done } = it.next(data)
  value.then(function(data) {
    console.log(data) //data=>3.txt
    let { value, done } = it.next(data)
    value.then(function(data) {
      console.log(data) //data=>结束
    })
  })
})

// 2.txt=>3.txt=>结束

可以看到手动迭代 Generator 函数很麻烦。

Async/Await

简单的说async函数就相当于自执行的Generator函数,在await的部分等待返回,返回后自动执行下一步。而且相较于Promise,async的优越性就是把每次异步返回的结果从then中拿到最外层的方法中,不需要链式调用,只要用同步的写法就可以了。更加直观而且,更适合处理并发调用的问题。但是async必须以一个Promise对象开始 ,所以async通常是和Promise结合使用的。babel和typescript的async/await是用yield/generator实现的 如下所示同样的代码中

async function a () {
	return 1
}
async function b () {
	await a()
}

b()

在[babel repl]中设置presets中勾选es2016es2017时,编译后的结果中使用了 PromiseGeneraotr

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { 
  try { 
    var info = gen[key](arg); 
    var value = info.value; 
  } catch (error) { 
    reject(error); return; 
  } 
  
  if (info.done) {
    resolve(value); 
  } else { 
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) { 
  return function () { 
    var self = this, 
    args = arguments; 
    return new Promise(function (resolve, reject) { 
      var gen = fn.apply(self, args); 
      function _next(value) { 
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); 
      } 
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); 
      } 
      
      _next(undefined);
    }); 
  }; 
}

function a() {
  return _a.apply(this, arguments);
}

function _a() {
  _a = _asyncToGenerator(function* () {
    return 1;
  });
  return _a.apply(this, arguments);
}

function b() {
  return _b.apply(this, arguments);
}

function _b() {
  _b = _asyncToGenerator(function* () {
    yield a();
  });
  return _b.apply(this, arguments);
}

b();

当输出的js版本修改为更低版本时,编译后的代码将会包含使用regenerator提供的regeneratorRuntime来实现 generator 函数

Typescript Repl中将Target设置为es2015,可以看到编译结果中同样也以后PromiseGenerator的身影

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function a() {
    return __awaiter(this, void 0, void 0, function* () {
        return 1;
    });
}
function b() {
    return __awaiter(this, void 0, void 0, function* () {
        yield a();
    });
}
b();

将输出版本修改为ES5时,可以看到些许差别。Typesciprt编译的结果中包含generator的实现

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
function a() {
    return __awaiter(this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            return [2 /*return*/, 1];
        });
    });
}
function b() {
    return __awaiter(this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, a()];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    });
}
b();

所以现阶段可以这么认为:

Async/Await = Generaotr + Promise
Toggle theme