细说JS异步发展历程及应用

论坛 期权论坛 期权     
imooc编程手记   2019-6-9 21:29   1579   0

作者 | 刘小夕,Web前端工程师,慕课网认证作者
来源 | 前端宇宙




知其然知其所以然,首先了解三个概念:
1.什么是同步?
所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。此调用执行完之前,阻塞之后的代码执行。
2.什么是异步?
"调用"在发出之后,这个调用就直接返回了,没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步调用发出后,不影响后面代码的执行。
3.JavaScript 中为什么需要异步?
首先我们知道JavaScript是单线程的(即使新增了webworker,但是本质上JS还是单线程)。同步代码意味着什么呢?意味着有可能会阻塞,当我们有一个任务需要时间较长时,如果使用同步方式,那么就会阻塞之后的代码执行。而异步则不会,我们不会等待异步代码的执行,继续执行异步任务之后的代码。


概念了解完了,我们就要进入今天的正题了。首先大家思考一下:平时在工作中,主要使用了哪些异步解决方案,这些异步方案有什么优缺点?


异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明);
为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise一定程度上解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。
ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
回调函数 ---> Promise ---> Generator ---> async/await.
[h2]1.callback[/h2]
    1. //node读取文件
    复制代码
    1. fs.readFile(xxx, 'utf-8', function(err, data) {
    复制代码
    1.     //code
    复制代码
    1. });
    复制代码
回调函数的使用场景(包括但不限于):
  • 事件回调
  • Node API
  • setTimeout/setInterval中的回调函数
  • ajax 请求
回调函数的优点: 简单。
回调函数的缺点:
异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能
  1. trycatch
复制代码
和 回调地狱(如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C...)。
    1. fs.readFile(A, 'utf-8', function(err, data) {
    复制代码
    1.     fs.readFile(B, 'utf-8', function(err, data) {
    复制代码
    1.         fs.readFile(C, 'utf-8', function(err, data) {
    复制代码
    1.             fs.readFile(D, 'utf-8', function(err, data) {
    复制代码
    1.                 //....
    复制代码
    1.             });
    复制代码
    1.         });
    复制代码
    1.     });
    复制代码
    1. });
    复制代码
[h2]2.Promise[/h2]Promise 一定程度上解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
那么我们看看Promise是如何解决回调地狱问题的,仍然以上文的readFile 为例(先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C)。
    1. function read(url) {
    复制代码
    1.     return new Promise((resolve, reject) => {
    复制代码
    1.         fs.readFile(url, 'utf8', (err, data) => {
    复制代码
    1.             if(err) reject(err);
    复制代码
    1.             resolve(data);
    复制代码
    1.         });
    复制代码
    1.     });
    复制代码
    1. }
    复制代码
    1. read(A).then(data => {
    复制代码
    1.     return read(B);
    复制代码
    1. }).then(data => {
    复制代码
    1.     return read(C);
    复制代码
    1. }).then(data => {
    复制代码
    1.     return read(D);
    复制代码
    1. }).catch(reason => {
    复制代码
    1.     console.log(reason);
    复制代码
    1. });
    复制代码
Promise 的优点:
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果
  • 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
缺点:
  • 无法取消 Promise
  • 当处于pending状态时,无法得知目前进展到哪一个阶段
  • 错误不能被
    1. trycatch
    复制代码
假设有这样一个需求:读取A,B,C三个文件内容,都读取成功后,再输出最终的结果。在Promise之前,我们一般可以借助发布订阅模式去实现:
    1. let pubsub = {
    复制代码
    1.     arry: [],
    复制代码
    1.     emit() {
    复制代码
    1.         this.arry.forEach(fn => fn());
    复制代码
    1.     },
    复制代码
    1.     on(fn) {
    复制代码
    1.         this.arry.push(fn);
    复制代码
    1.     }
    复制代码
    1. }
    复制代码


    1. let data = [];
    复制代码
    1. pubsub.on(() => {
    复制代码
    1.     if(data.length === 3) {
    复制代码
    1.         console.log(data);
    复制代码
    1.     }
    复制代码
    1. });
    复制代码
    1. fs.readFile(A, 'utf-8', (err, value) => {
    复制代码
    1.     data.push(value);
    复制代码
    1.     pubsub.emit();
    复制代码
    1. });
    复制代码
    1. fs.readFile(B, 'utf-8', (err, value) => {
    复制代码
    1.     data.push(value);
    复制代码
    1.     pubsub.emit();
    复制代码
    1. });
    复制代码
    1. fs.readFile(C, 'utf-8', (err, value) => {
    复制代码
    1.     data.push(value);
    复制代码
    1.     pubsub.emit();
    复制代码
    1. });
    复制代码
Promise给我们提供了
  1. Promise.all
复制代码
的方法,对于这个需求,我们可以使用
  1. Promise.all
复制代码
来实现。
    1. /**
    复制代码
    1. * 将 fs.readFile 包装成promise接口
    复制代码
    1. */
    复制代码
    1. function read(url) {
    复制代码
    1.     return new Promise((resolve, reject) => {
    复制代码
    1.         fs.readFile(url, 'utf8', (err, data) => {
    复制代码
    1.             if(err) reject(err);
    复制代码
    1.             resolve(data);
    复制代码
    1.         });
    复制代码
    1.     });
    复制代码
    1. }
    复制代码


    1. /**
    复制代码
    1. * 使用 Promise
    复制代码
    1. *
    复制代码
    1. * 通过 Promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题
    复制代码
    1. */
    复制代码
    1. Promise.all([
    复制代码
    1.     read(A),
    复制代码
    1.     read(B),
    复制代码
    1.     read(C)
    复制代码
    1. ]).then(data => {
    复制代码
    1.     console.log(data);
    复制代码
    1. }).catch(err => console.log(err));
    复制代码
可执行代码可戳: https://github.com/YvetteLau/Blog/blob/master/JS/Async/index.js
[h2]3.Generator[/h2]Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
Generator 函数一般配合 yield 或 Promise 使用。Generator函数返回的是迭代器。对生成器和迭代器不了解的同学,请自行补习下基础。下面我们看一下 Generator 的简单使用:
    1. function* gen() {
    复制代码
    1.     let a = yield 111;
    复制代码
    1.     console.log(a);
    复制代码
    1.     let b = yield 222;
    复制代码
    1.     console.log(b);
    复制代码
    1.     let c = yield 333;
    复制代码
    1.     console.log(c);
    复制代码
    1.     let d = yield 444;
    复制代码
    1.     console.log(d);
    复制代码
    1. }
    复制代码
    1. let t = gen();
    复制代码
    1. //next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
    复制代码
    1. t.next(1); //第一次调用next函数时,传递的参数无效
    复制代码
    1. t.next(2); //a输出2;
    复制代码
    1. t.next(3); //b输出3;
    复制代码
    1. t.next(4); //c输出4;
    复制代码
    1. t.next(5); //d输出5;
    复制代码
为了让大家更好的理解上面代码是如何执行的,我画了一张图,分别对应每一次的next方法调用:


仍然以上文的 readFile (先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C)为例,使用 Generator + co库来实现:
    1. const fs = require('fs');
    复制代码
    1. const co = require('co');
    复制代码
    1. const bluebird = require('bluebird');
    复制代码
    1. const readFile = bluebird.promisify(fs.readFile);
    复制代码


    1. function* read() {
    复制代码
    1.     yield readFile(A, 'utf-8');
    复制代码
    1.     yield readFile(B, 'utf-8');
    复制代码
    1.     yield readFile(C, 'utf-8');
    复制代码
    1.     //....
    复制代码
    1. }
    复制代码
    1. co(read()).then(data => {
    复制代码
    1.     //code
    复制代码
    1. }).catch(err => {
    复制代码
    1.     //code
    复制代码
    1. });
    复制代码
Generator的缺点大约不用我说了,除非是找虐,不然一般不会直接使用 Generator 来解决异步的(当然也不排除是因为我不熟练)~~~
不使用co库,如何实现?能否自己写一个最简的 my_co,有助于理解 async/await 的实现原理 ?请戳: https://github.com/YvetteLau/Blog/blob/master/JS/Async/generator.js
PS: 如果你还不太了解 Generator/yield,建议阅读ES6相关文档。
[h2]4.async/await[/h2]ES7中引入了 async/await 概念。async 其实是一个语法糖,它的实现就是将 Generator函数和自动执行器(co),包装在一个函数中。
async/await 的优点是代码清晰,不用像 Promise 写很多 then 链,就可以处理回调地狱的问题。并且错误可以被try catch。
仍然以上文的readFile (先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C) 为例,使用 async/await 来实现:
    1. const fs = require('fs');
    复制代码
    1. const bluebird = require('bluebird');
    复制代码
    1. const readFile = bluebird.promisify(fs.readFile);
    复制代码




    1. async function read() {
    复制代码
    1.     await readFile(A, 'utf-8');
    复制代码
    1.     await readFile(B, 'utf-8');
    复制代码
    1.     await readFile(C, 'utf-8');
    复制代码
    1.     //code
    复制代码
    1. }
    复制代码


    1. read().then((data) => {
    复制代码
    1.     //code
    复制代码
    1. }).catch(err => {
    复制代码
    1.     //code
    复制代码
    1. });
    复制代码
使用 async/await 实现此需求:读取A,B,C三个文件内容,都读取成功后,再输出最终的结果。
    1. function read(url) {
    复制代码
    1.     return new Promise((resolve, reject) => {
    复制代码
    1.         fs.readFile(url, 'utf8', (err, data) => {
    复制代码
    1.             if(err) reject(err);
    复制代码
    1.             resolve(data);
    复制代码
    1.         });
    复制代码
    1.     });
    复制代码
    1. }
    复制代码


    1. async function readAsync() {
    复制代码
    1.     let data = await Promise.all([
    复制代码
    1.         read(A),
    复制代码
    1.         read(B),
    复制代码
    1.         read(C)
    复制代码
    1.     ]);
    复制代码
    1.     return data;
    复制代码
    1. }
    复制代码


    1. readAsync().then(data => {
    复制代码
    1.     console.log(data);
    复制代码
    1. });
    复制代码
所以JS的异步发展史,可以认为是从 callback -> promise -> generator -> async/await。async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
因本人水平有限,文中内容未必百分百正确,如有不对的地方,请给我留言,谢谢。
[h2]邀请你加入 Step-By-Step 项目[/h2]
不积跬步无以至千里。 我是公众号【前端宇宙】作者刘小夕,我将和大家一起一步一个脚印,向前端专家迈进。Step-By-Step
每个工作日我会发布一个前端相关的问题(目的是为了切实掌握相关的知识点),欢迎在 Issue 区留下你的答案。
节假日不会发布任何问题,希望大家能够利用节假日回顾一周所学。每周末我会进行一次汇总(整理出最优答案),以便大家回顾。
参考文章:
[1] 细说JavaScript异步函数发展历程
[2] ES6 Promise
[3] ES6 Generator
[4] ES6 async
[5] JavaScript异步编程
谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。https://github.com/YvetteLau/Blog
喜欢小姐姐作品的小伙伴也可以戳原文链接去慕课网关注她哦~









用CSS3实现王者荣耀匹配人员加载页面
JAVA 泛型意淫之旅
为什么部分程序员下班后只关显示器不关电脑?
龙岗一个月350的出租房,我搬出来了


— 完 —









分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:10
帖子:2
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP