ZooMze's World

vuePress-theme-reco ZooMze    2018 - 2021
ZooMze's World ZooMze's World

Choose mode

  • dark
  • auto
  • light
主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
author-avatar

ZooMze

35

Article

23

Tag

主页
分类
  • 基础
  • 备忘
  • 教程
  • 扩展
  • 框架
  • 组件
  • 季度分享
标签
时光轴
GitHub
  • 从 Generator 到 Async

    • 前言
      • 什么是异步?
        • 回调函数
          • Promise
            • 协程的概念
              • Generator
                • Generator 的数据接力和错误处理
                  • 数据接力
                  • 错误处理
                • Generator实践
                  • Async是什么?
                    • Async的错误处理

                    从 Generator 到 Async

                    vuePress-theme-reco ZooMze    2018 - 2021

                    从 Generator 到 Async


                    ZooMze 2020-01-12 JavaScriptES6

                    异步编程的终极解决方案

                    # 前言

                    JavaScript 是单线程语言, 如果没有异步编程, 那整个执行过程将长的可怕, 甚至可能会直接卡死。

                    在以往, 异步编程主要体现在下面几种类型:

                    • 回调函数
                    • 事件监听
                    • 发布 / 订阅
                    • Promise

                    ES6的出现, 将异步编程提升到了全新的高度, 异步编程的终极目标就是: 让代码变得更像同步编程!

                    # 什么是异步?

                    将原本一个任务分段, 先执行其中的一段, 然后执行本任务以外的其他任务, 等第一段做好了准备, 在回来本任务执行第二段, 这种就是异步。

                    例如一个请求并处理文件, 首先系统请求文件, 请求文件本身是异步任务, 在等待请求完成过程中程序会执行其余的任务, 等到文件请求完成后, 再回来继续执行处理本文件的任务(也就是第二段任务)。

                    反之, 所有任务都是依次等待的情况就称为 同步。

                    刚刚那个请求文件的例子, 如果没有异步, 则整个程序在文件请求完成之前都是处于等待状态。

                    # 回调函数

                    JavaScript 语言对异步编程的实现,就是回调函数。

                    所谓回调函数(callback),就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

                    上述的请求文件的例子在异步的代码中就长这样:

                    // 第二个参数即是 callback函数, 他将在读取文件完成后执行
                    fs.readFile('/etc/passwd', function (err, data) {
                      if (err) throw err;
                      console.log(data);
                    });
                    
                    1
                    2
                    3
                    4
                    5
                    康康这个

                    一个有趣的问题是,为什么 Node.js 约定,回调函数的第一个参数,必须是错误对象 err(如果没有错误,该参数就是 null )?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。

                    # Promise

                    回调函数在单个简单任务执行时没毛病, 但是如果嵌套就会使代码非常繁杂, 回调地狱(callback hell)就是描述的这种情况。

                    Promise的出现就是为了解决这个问题, 他不是新的语法功能, 是回调函数的新的写法, 将回调地狱的横向发展改回纵向发展, 现在我们使用Promise来读取多个文件, 代码大约长这样:

                    var readFile = require('fs-readfile-promise');
                    
                    readFile(fileA)
                    .then(function(data){
                      console.log(data.toString());
                    })
                    .then(function(){
                      return readFile(fileB);
                    })
                    .then(function(data){
                      console.log(data.toString());
                    })
                    .catch(function(err) {
                      console.log(err);
                    });
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15

                    Promise虽然改进了单纯回调函数横向复杂度的缺点, 但是其本身仍然有缺陷: 原本有语义的任务被一堆 then() 包裹, 代码逻辑完全被回调逻辑取代

                    # 协程的概念

                    在很多其他编程语言中, 早有异步编程的解决方案, 那就是 协程: 多个线程互相协作完成异步任务。

                    协程的运行流程是这样的:

                    1. 协程① 开始执行
                    2. 协程① 执行到一部分, 暂停, 执行权转交给别的 协程②
                    3. (一段时间后) 协程② 转交执行权
                    4. 协程① 恢复执行

                    我们再回到刚刚得请求文件的例子



                     



                    function asnycJob() {
                      // ...其他代码
                      var f = yield readFile(fileA); 
                      // ...其他代码
                    }
                    
                    1
                    2
                    3
                    4
                    5

                    asyncJob 其实就是一个协程, 它在执行了一段后进入暂停阶段(由yield命令转交执行权), 再等到执行权返回时重新继续执行 yield 暂停以后的内容, 如此一来, 你的代码如果去掉 yield, 它看起来就跟同步代码一模一样!

                    # Generator

                    你也可以本站的这篇内容: 迭代器 Iterator & 生成器 Generator 来了解更多。


                     







                    function* gen(x){
                      var y = yield x + 2; // function will pause here until then next runing
                      return y;
                    }
                    
                    var g = gen(1);
                    g.next() // { value: 3, done: false }
                    g.next() // { value: undefined, done: true }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8

                    Generator函数 即是协程在ES6的实现, 你如果看了上述的内容,你应该知道Generator是一个生成器, 他是一个特殊的函数, 执行生成器是不会有任何返回结果, 他返回的是一个 迭代器Interator。迭代器再调用 next() 方法, 开始执行直到遇到 yield, 并返回yield 当前阶段的状态:

                    {
                      value: '???', // yield 后的表达式的值
                      done: true || false // 生成器内的内容是否全部执行结束
                    }
                    
                    1
                    2
                    3
                    4

                    上例中初次执行 next() 时遇到了yield 并返回了 x + 2 的值, 第二次 next() 时, 由于是从 yield 后开始执行的, y 没有任何赋值操作, 所以返回了 undefined , done 状态也变成了 true。

                    # Generator 的数据接力和错误处理

                    # 数据接力

                    迭代器调用 next() 其实是可以带参数的, 正常书写异步代码时也应当附带参数, 参数的值表示上一个阶段异步任务( yield )的执行结果, 所以你在最开始就给 next() 添加参数, 迭代器并不会理会它。

                    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 } 接替了第一阶段的执行结果, 将值赋给了y
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8

                    # 错误处理

                    来看这段代码:












                     

                    function* gen(x){
                      try {
                        var y = yield x + 2;
                      } catch (e){ 
                        console.log(e);
                      }
                      return y;
                    }
                    
                    var g = gen(1);
                    g.next();
                    g.throw('出错了!'); // 出错了! << console from line 5
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12

                    # Generator实践

                    我们先定义一个生成器:

                    var fetch = require('node-fetch');
                    
                    function* gen(){
                      var url = 'someURL';
                      var result = yield fetch(url); // 获取数据
                      console.log(result.bio);
                    }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7

                    然后执行它:

                    var g = gen();
                    var result = g.next(); // 执行至next()后暂停
                    
                    // 注意Fetch返回的是Promise对象. 这里需要使用then()
                    result.value.then(function(data){
                      return data.json();
                    }).then(function(data){ // 完成return后 继续执行, 再打印出console的内容
                      g.next(data);
                    });
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9

                    * 前文即大致地介绍了Generator的意义的使用方法, 接下来, 就得搬出终极方案: async / await

                    # Async是什么?

                    async 函数其实就是 Generator 函数的语法糖, 已更加简洁的方式出现在你的代码中。

                    现在来把前文提到的的读取多个文件的方法重新用通常的Generator来重新写一下, 就大约是这个亚子:

                    var fs = require('fs');
                    
                    // 定义一个异步的Promise用来读取文件
                    var readFile = function (fileName){
                      return new Promise(function (resolve, reject){
                        fs.readFile(fileName, function(error, data){
                          if (error) reject(error);
                          resolve(data);
                        });
                      });
                    };
                    
                    var gen = function* (){
                      var f1 = yield readFile('/etc/fstab');
                      var f2 = yield readFile('/etc/shells');
                      console.log(f1.toString()); // 这里其实输出的是function代码而非文件结果
                      console.log(f2.toString());
                    };
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15
                    16
                    17
                    18

                    然后改写成 async:

                    var asyncReadFile = async function (){
                      var f1 = await readFile('/etc/fstab');
                      var f2 = await readFile('/etc/shells');
                      console.log(f1.toString());
                      console.log(f2.toString());
                    };
                    
                    1
                    2
                    3
                    4
                    5
                    6

                    发现了吗, 其实就是把 * 提到前面换成了 async, 把 yield 换成了 await !

                    # Async的错误处理

                    async 函数返回一个Promise对象, 所以有可能会返回 reject, 这时候就需要进行错误处理, 我们使用 try catch 进行错误捕捉:

                    async function myFunction() {
                      try {
                        await somethingThatReturnsAPromise();
                      } catch (err) {
                        console.log(err);
                      }
                    }
                    
                    // 或者写得更简单些
                    
                    async function myFunction() {
                      await somethingThatReturnsAPromise().catch(function (err){
                        console.log(err);
                      });
                    }
                    
                    1
                    2
                    3
                    4
                    5
                    6
                    7
                    8
                    9
                    10
                    11
                    12
                    13
                    14
                    15

                    注意

                    await 命令只能用在 async 函数内, 否则会报错, 就跟 (*) 包含了 yield 一样。