zoukankan      html  css  js  c++  java
  • ES6读书笔记(三)

    前言

    前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》《ES6读书笔记(二)》,现在为第三篇,本篇内容包括:

    • 一、Promise
    • 二、Iterator和for of循环
    • 三、Generator
    • 四、async

    本文笔记也主要是根据阮一峰老师的《ECMAScript 6 入门》和平时的理解进行整理的,希望对你有所帮助,喜欢的就点个赞吧!

    一、Promise

    1. 执行顺序

    let promise = new Promise(function(resolve, reject) {
      console.log('Promise');
      resolve();
    });
    
    promise.then(function() {
      console.log('resolved.');
    });
    
    console.log('Hi!');
    
    // Promise
    // Hi!
    // resolved
    

    2.异步加载图片:

    function loadImageAsync(url) {
      return new Promise(function(resolve, reject) {
        const image = new Image();
    
        image.onload = function() {
          resolve(image);
        };
    
        image.onerror = function() {
          reject(new Error('Could not load image at ' + url));
        };
    
        image.src = url;
      });
    }
    

    3.用Promise对象实现Ajax:

    const getJSON = function(url) {
      const promise = new Promise(function(resolve, reject){
        const handler = function() {
          if (this.readyState !== 4) {
            return;
          }
          if (this.status === 200) {
            resolve(this.response);
          } else {
            reject(new Error(this.statusText));
          }
        };
        const client = new XMLHttpRequest();
        client.open("GET", url);
        client.onreadystatechange = handler;
        client.responseType = "json";
        client.setRequestHeader("Accept", "application/json");
        client.send();
    
      });
    
      return promise;
    };
    
    getJSON("/posts.json").then(function(json) {
      console.log('Contents: ' + json);
    }, function(error) {
      console.error('出错了', error);
    });
    

    4.then方法返回的是一个新的Promise实例(不是原来那个Promise实例)

    5.如果 Promise 状态已经变成resolved,再抛出错误是无效的:

    const promise = new Promise(function(resolve, reject) {
      resolve('ok');
      throw new Error('test');
    });
    promise
      .then(function(value) { console.log(value) })
      .catch(function(error) { console.log(error) });
    // ok
    

    上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了

    6.跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应,通俗的说法就是“Promise 会吃掉错误”:

    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        resolve(x + 2);   // 会报错,因为x没有声明
      });
    };
    
    someAsyncThing().then(function() {
      console.log('everything is great');
    });
    
    setTimeout(() => { console.log(123) }, 2000);  // 虽然以上有错误,但没有阻塞后面的代码
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123
    

    7.

    const promise = new Promise(function (resolve, reject) {
      resolve('ok');
      setTimeout(function () { throw new Error('test') }, 0)
    });
    promise.then(function (value) { console.log(value) });
    // ok
    // Uncaught Error: test
    

    上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误,相当于是js引擎去执行了这个回调,而不是在promise内部执行。

    一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法:

    • ①如果有错误,但没有去catch,则会阻塞promise内部的代码,但不会阻塞外部的代码;
    • ②如果有catch,但是没有错误,则会跳过catch,继续执行后面的代码;
    • ③如果有catch,然后被catch捕获了错误,那依旧可以继续执行后面的代码;
    • ④如果有catch,catch捕获到了前面的错误,但catch内部又有错误的话,则会阻塞后面的代码,除非后面再链式调用catch捕获该错误。

    以上总结就是只要promise内部有错误没有被捕获,就会阻塞内部代码,但不会阻塞外部代码。

    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
      });
    };
    
    someAsyncThing()
    .catch(function(error) {
      console.log('oh no', error);
    })
    .then(function() {
      console.log('carry on');
    });
    // oh no [ReferenceError: x is not defined]
    // carry on
    //--------------------------------------------
    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
      });
    };
    
    someAsyncThing().then(function() {
      return someOtherAsyncThing();
    }).catch(function(error) {
      console.log('oh no', error);
        y + 2;  // y 没有声明会报错,且这个错误未被捕获,会阻塞后面的代码
    }).then(function() {
      console.log('carry on');
    });
    // oh no [ReferenceError: x is not defined]
    

    8.Promise.prototype.finally()

    • ①finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作:
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    

    上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

    • ②finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

    • ③finally本质上是then方法的特例:

    promise
    .finally(() => {
      // 语句
    });
    
    // 等同于
    promise
    .then(
      result => {
        // 语句
        return result;
      },
      error => {
        // 语句
        throw error;
      }
    );
    
    • ④finally方法总是会返回原来的值:
    // resolve 的值是 undefined
    Promise.resolve(2).then(() => {}, () => {})
    
    // resolve 的值是 2
    Promise.resolve(2).finally(() => {})
    
    // reject 的值是 undefined
    Promise.reject(3).then(() => {}, () => {})
    
    // reject 的值是 3
    Promise.reject(3).finally(() => {})
    

    9.Promise.all(数组或具有 Iterator 接口,且返回的每个成员都是 Promise 实例)

    • ①如果参数全为fulfilled,则返回对应的数组结果(是等全部得到结果了再一起返回),但如果有一个是rejected,则返回第一个rejected的返回值,状态就为rejected。

    • ②catch后会变为resolved:

    const p1 = new Promise((resolve, reject) => {
      resolve('hello');
    })
    .then(result => result)
    .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了');
    // reject(“world”);
    })
    .then(result => result)
    .catch(e => e);        // catch后会变为resolved
    
    Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e));   // 传入的p2有自己的catch,所以不会触发这里的catch,所以没有捕获到错误,所以就相当于都是执行正确的,所以会有结果
    // ["hello", Error: 报错了]
    

    10.Promise.race

    参数中谁率先改变了状态,就返回谁的状态,这意味着只返回一个结果

    11.Promise.resolve()

    • ①Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。 所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法:
    const p = Promise.resolve();
    
    p.then(function () {
      // ...
    });
    
    • ②立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
    setTimeout(function () {
      console.log('three');
    }, 0);
    
    Promise.resolve().then(function () {
      console.log('two');
    });
    
    console.log('one');
    
    // one
    // two
    // three
    

    12.Promise.reject()

    Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致:

    const thenable = {
      then(resolve, reject) {
        reject('出错了');
      }
    };
    Promise.reject(thenable)
    .catch(e => {
      console.log(e === thenable)
    })
    // true
    

    上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

    13.如果对于一个函数,不管是同步或异步,都想使用then方法指定下一流程,可使用以下方式,让它是同步时就按同步执行,是异步时就按异步执行:

    不要直接使用promise.resolve(),因为如果是同步函数,会在本轮事件循环末尾才会执行:

    const f = () => console.log('now');
    Promise.resolve().then(f);  // then才是微任务,resolve时还是同步的
    console.log('next');
    // next
    // now
    
    • ①使用async:
    const f = () => console.log('now');
    (async () => f())()
    .then(...)
    .catch(...);
    
    console.log('next');
    // now
    // next
    
    • ②使用new Promise():
    const f = () => console.log('now');
    (
      () => new Promise(
        resolve => resolve(f())
      )
    )();
    console.log('next');
    // now
    // next
    
    • ③一个提案,提供Promise.try方法替代上面的写法:浏览器目前会报错
    const f = () => console.log('now');
    Promise.try(f);
    console.log('next');
    // now
    // next
    

    二、Iterator和for of循环

    1. Iterator(遍历器)的概念

    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

    Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。

    遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

    Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

    Iterator 的遍历过程是这样的:

    • (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
    • (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
    • (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
    • (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

    每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

    一个模拟next方法返回值的例子:

    var it = makeIterator(['a', 'b']);
    
    it.next() // { value: "a", done: false }
    it.next() // { value: "b", done: false }
    it.next() // { value: undefined, done: true }
    
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++], done: false} :
            {value: undefined, done: true};
        }
      };
    }
    

    2.ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。

    const obj = {
      [Symbol.iterator] : function () {
        return {
          next: function () {
            return {
              value: 1,
              done: true
            };
          }
        };
      }
    };
    

    3.原生具备 Iterator 接口的数据结构如下:不含对象

    • Array
    • Map
    • Set
    • String
    • TypedArray
    • 函数的 arguments 对象
    • NodeList 对象

    4.数组的Symbol.iterator属性:

    let arr = ['a', 'b', 'c'];
    let iter = arr[Symbol.iterator]();
    
    iter.next() // { value: 'a', done: false }
    iter.next() // { value: 'b', done: false }
    iter.next() // { value: 'c', done: false }
    iter.next() // { value: undefined, done: true }
    

    5.对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

    6.有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法):

    • ①解构赋值
    • ②扩展运算符:这样就可对有Iterator接口的数据结构使用扩展运算符转为数组,而对于没有Iterator接口的类数组,可采用Array.from转为数组,这样就具有了Iterator接口
    • ③yield*

    yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口:

    let generator = function* () {
      yield 1;
      yield* [2,3,4];
      yield 5;
    };
    
    var iterator = generator();
    
    iterator.next() // { value: 1, done: false }
    iterator.next() // { value: 2, done: false }
    iterator.next() // { value: 3, done: false }
    iterator.next() // { value: 4, done: false }
    iterator.next() // { value: 5, done: false }
    iterator.next() // { value: undefined, done: true }
    
    • ④其他场合 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口:

    • for...of

    • Array.from()

    • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))

    • Promise.all()

    • Promise.race()

    7. Iterator接口与Generator函数

    Symbol.iterator方法的最简单实现:

    let myIterable = {
      [Symbol.iterator]: function* () {
        yield 1;
        yield 2;
        yield 3;
      }
    }
    [...myIterable] // [1, 2, 3]
    
    // 或者采用下面的简洁写法
    
    let obj = {
      * [Symbol.iterator]() {
        yield 'hello';
        yield 'world';
      }
    };
    
    for (let x of obj) {
      console.log(x);
    }
    // "hello"
    // "world"
    

    8.for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

    let arr = [3, 5, 7];
    arr.foo = 'hello';
    
    for (let i in arr) {
      console.log(i); // "0", "1", "2", "foo" 这也说明了for in遍历了自身及原型上的可枚举属性
    }
    
    for (let i of arr) {
      console.log(i); //  "3", "5", "7"
    }
    

    9. Map遍历得到的是数组,Set遍历得到的是单个值:

    let map = new Map().set('a', 1).set('b', 2);
    Map;   // {"a" => 1, "b" => 2}
    for (let pair of map) {
      console.log(pair);
    }
    // ['a', 1]
    // ['b', 2]
    
    for (let [key, value] of map) {
      console.log(key + ' : ' + value);
    }
    // a : 1
    // b : 2
    

    10. 可用Array.from将不具有iterator接口的类数组对象转为数组,这样也就具有了iterator接口:

    let arrayLike = { length: 2, 0: 'a', 1: 'b' };
    
    // 报错
    for (let x of arrayLike) {
      console.log(x);
    }
    
    // 正确
    for (let x of Array.from(arrayLike)) {
      console.log(x);
    }
    

    11.循环对比:

    • for in 会遍历原型可枚举属性,为遍历对象而生,尽管对象没有iterator接口
    • forEach不能中途跳出循环
    • for of 可中途跳出循环,不会遍历原型可枚举属性,针对数组

    三、Generator

    1. 执行Generator(生成器)返回一个遍历器对象,这个遍历器对象可以依次遍历Generator函数内部的每一个状态,yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”):

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();
    hw.next()  // 遇到第一个yield,暂停,然后返回yield后面的表达式的值
    // { value: 'hello', done: false }
    
    hw.next()  // 从上次暂停的地方往下执行,遇到第二个yield后暂停,返回值
    // { value: 'world', done: false }
    
    hw.next()  // 从上次暂停的地方往下执行,发现没有yield了,所以一直往下执行,直到遇到// return,如果没有return则返回undefined
    // { value: 'ending', done: true }
    
    hw.next()
    // { value: undefined, done: true }
    

    定义方式:

    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· }   // 推荐这种写法
    function*foo(x, y) { ··· }
    

    2. 遍历器对象的next方法的运行逻辑如下:

    • (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

    • (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

    • (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

    • (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

    需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

    3. Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

    function* f() {
      console.log('执行了!')
    }
    
    var generator = f();
    
    setTimeout(function () {
      generator.next()
    }, 2000);
    

    4. yield表达式如果用在另一个表达式之中,必须放在圆括号里面:

    function* demo() {
      console.log('Hello' + yield);     // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
    
      console.log('Hello' + (yield));     // OK
      console.log('Hello' + (yield 123)); // OK
    }
    

    yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号:

    function* demo() {
      foo(yield 'a', yield 'b'); // OK
      let input = yield; // OK
    }
    

    5.与 Iterator 接口的关系:

    任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

    由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口:

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    
    [...myIterable] // [1, 2, 3]
    

    上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

    Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身:

    function* gen(){
      // some code
    }
    
    var g = gen();
    
    g[Symbol.iterator]() === g
    // true
    

    6.yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;  // yield i表达式是没有返回值的,或者说返回undefined
        if(reset) { i = -1; }
      }
    }
    
    var g = f();
    
    g.next() // { value: 0, done: false }
    g.next() // { value: 1, done: false }
    g.next(true) // { value: 0, done: false } 相当于给reset赋值为true,重置了i的值
    

    next参数的值是传给上一个yield表达式的返回值,所以这也意味着第一个next的参数是无效的,所以不需要传,即第一个next是用于启动遍历器对象:

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}  yield (x + 1)的返回值是undefined,所以乘2再除3得到的是NaN
    a.next() // Object{value:NaN, done:true}  5 + NaN + undefined为NaN
    
    var b = foo(5);
    b.next() // { value:6, done:false }  5+1得到6
    b.next(12) // { value:8, done:false }  12赋给yield (x + 1),然后乘2除3得到8,即(y / 3)的值
    b.next(13) // { value:42, done:true }  同理
    
    //-----------------------------------------
    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`);
      console.log(`2. ${yield}`);
      return 'result';
    }
    
    let genObj = dataConsumer();
    genObj.next();
    // Started
    // {value: undefined, done: false}
    genObj.next('a')
    // 1. a
    // {value: undefined, done: false}
    genObj.next('b')
    // 2. b
    // {value: "result", done: true}
    

    7. for of 循环:

    for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法:

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3 4 5 没有6,因为一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象
    

    8. Generator.prototype.throw()

    Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获:

    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('内部捕获', e);
      }
    };
    
    var i = g();
    i.next();  // 要捕获错误,必须先执行一次next来启动遍历器对象
    
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
    

    9. throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法:

    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
    

    10. Generator.prototype.return()

    Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数:

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next()        // { value: undefined, done: true }
    

    如果return方法调用时,不提供参数,则返回值的value属性为undefined:

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

    如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。

    function* numbers () {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers();
    g.next() // { value: 1, done: false }
    g.next() // { value: 2, done: false }
    g.return(7) // { value: 4, done: false }
    g.next() // { value: 5, done: false }
    g.next() // { value: 7, done: true }
    

    上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。

    11. 在一个 Generator 函数里面执行另一个 Generator 函数:

    function* bar() {
      yield 'x';
      yield* foo();  // 加了*号就是返回遍历器内部值,相当于调用了*后面变量的iterator接口,否则返回遍历器对象
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      yield 'a';
      yield 'b';
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    
    3.1 异步应用

    1.异步简单说就是不连续的执行任务,类似一个协程的过程:

    function* asyncJob() {
      // ...其他代码
      var f = yield readFile(fileA);
      // ...其他代码
    }
    

    其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线,协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。

    2.Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

    3.传值调用:先计算参数值再传入函数体内使用。 传名调用:直接将参数表达式传入函数体内,使用到时再进行求值。

    4.Generator的异步应用中何时调用第一步,何时调用第二步,此时就需要使用thunk函数,相当于“传名调用”,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做 Thunk 函数:

    function f(m) {
      return m * 2;
    }
    
    f(x + 5);
    
    // 等同于
    
    var thunk = function () {
      return x + 5;
    };
    
    function f(thunk) {
      return thunk() * 2;
    }
    

    5.JavaScript 语言的 Thunk 函数

    JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数,类似柯里化:

    // 正常版本的readFile(多参数版本)
    fs.readFile(fileName, callback);
    
    // Thunk版本的readFile(单参数版本)
    var Thunk = function (fileName) {
      return function (callback) {
        return fs.readFile(fileName, callback);
      };
    };
    
    var readFileThunk = Thunk(fileName);
    readFileThunk(callback);
    

    四、async

    1. async 函数其实就是 Generator 函数的语法糖,可以再等待第一阶段得到结果后自动执行第二阶段,而不是像Generator那样手动执行。*换成了async,yield换成了await。

    2. async函数对 Generator 函数的改进,体现在以下加点:

    • ①内置执行器
    • ②更语义化
    • ③适应性
    • ④返回promise

    3. async函数返回一个 Promise 对象。

    async函数内部return语句返回的值,会成为then方法回调函数的参数:

    async function f() {
      return 'hello world';
    }
    
    f().then(v => console.log(v))
    // "hello world"
    

    如果没有return,则then方法回调函数的参数则得到的是undefined

    async function f() {
      await Promise.resolve('hello world'); // 不会执行
    }
    f().then(a=>{console.log(a)})   // undefined
    

    4. Promise 对象的状态变化

    async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数:

    async function getTitle(url) {
      let response = await fetch(url);
      let html = await response.text();
      return html.match(/<title>([sS]+)</title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log)
    // "ECMAScript 2017 Language Specification"
    

    上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

    5. 一般await后面是接promise对象,返回该对象的结果,如果不是promise对象,则直接返回对应的值:

    async function f() {
      // 等同于
      // return 123;
      return await 123;  // return要放await前面,否则会报错
    }
    
    f().then(v => console.log(v))
    // 123
    

    6. await后面的promise状态如果为reject,则会被catch到:

    async function f() {
      await Promise.reject('出错了');
    }
    
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // 出错了
    

    任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

    async function f() {
      await Promise.reject('出错了');
      await Promise.resolve('hello world'); // 不会执行
    }
    

    为了防止有错误或reject中断代码的执行,则需要使用catch来处理,或者使用try catch:

    async function f() {
      await Promise.reject('出错了')
        .catch(e => console.log(e));
      return await Promise.resolve('hello world');
    }
    
    f()
    .then(v => console.log(v))
    // 出错了
    // hello world
    

    如果有多个await命令,可以统一放在try...catch结构中:

    async function main() {
      try {
        const val1 = await firstStep();
        const val2 = await secondStep(val1);
        const val3 = await thirdStep(val1, val2);
    
        console.log('Final: ', val3);
      }
      catch (err) {
        console.error(err);
      }
    }
    

    7. 使用async注意点:

    • ①catch错误,防止代码中断
    • ②对于不存在继发关系的异步操作,应该让它们同步进行,而不是顺序执行:
    let foo = await getFoo();
    let bar = await getBar();
    
    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    
    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
    
    • ③await命令只能用在async函数之中,如果用在普通函数,就会报错,如用在forEach中会报错,因为是并发执行,应该使用for循环:
    unction dbFuc(db) { //这里不需要 async
      let docs = [{}, {}, {}];
    
      // 可能得到错误结果
      docs.forEach(async function (doc) {
        await db.post(doc);
      });
    }
    

    上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      for (let doc of docs) {
        await db.post(doc);
      }
    }
    
    • ④async 函数可以保留运行堆栈。
    const a = () => {
      b().then(() => c());
    };
    

    上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。

    改成async函数:

    const a = async () => {
      await b();
      c();
    };
    

    上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c(),错误堆栈将包括a()。

    8. async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里:

    async function fn(args) {
      // ...
    }
    
    // 等同于
    
    function fn(args) {
      return spawn(function* () {
        // ...
      });
    }
    

    9. 继发/并发顺序输出:

    async function logInOrder(urls) {
      for (const url of urls) {
        const response = await fetch(url);
        console.log(await response.text());
      }
    }
    

    上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求:

    async function logInOrder(urls) {
      // 并发读取远程URL
      const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
      });
    
      // 按次序输出
      for (const textPromise of textPromises) {
        console.log(await textPromise);
      }
    }
    

    10. 异步遍历器:asyncIterator,部署在Symbol.asyncIterator属性上面,最大的语法特点就是调用遍历器的next方法,返回的是一个 Promise 对象。

    const asyncIterable = createAsyncIterable(['a', 'b']);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();
    
    asyncIterator
    .next()
    .then(iterResult1 => {
      console.log(iterResult1); // { value: 'a', done: false }
      return asyncIterator.next();
    })
    .then(iterResult2 => {
      console.log(iterResult2); // { value: 'b', done: false }
      return asyncIterator.next();
    })
    .then(iterResult3 => {
      console.log(iterResult3); // { value: undefined, done: true }
    });
    

    可改写为:

    async function f() {
      const asyncIterable = createAsyncIterable(['a', 'b']);
      const asyncIterator = asyncIterable[Symbol.asyncIterator]();
      console.log(await asyncIterator.next());
      // { value: 'a', done: false }
      console.log(await asyncIterator.next());
      // { value: 'b', done: false }
      console.log(await asyncIterator.next());
      // { value: undefined, done: true }
    }
    

    异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去,所以也可以这样:

    const asyncIterable = createAsyncIterable(['a', 'b']);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();
    const [{value: v1}, {value: v2}] = await Promise.all([
      asyncIterator.next(), asyncIterator.next()
    ]);
    
    console.log(v1, v2); // a b
    

    11. for await...of

    for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口:

    async function f() {
      for await (const x of createAsyncIterable(['a', 'b'])) {
        console.log(x);
      }
    }
    // a
    // b
    

    如果next方法返回的 Promise 对象被reject,for await...of就会报错,要用try...catch捕捉。

    async function () {
      try {
        for await (const x of createRejectingIterable()) {
          console.log(x);
        }
      } catch (e) {
        console.error(e);
      }
    }
    

    注意,for await...of循环也可以用于同步遍历器:

    (async function () {
      for await (const x of ['a', 'b']) {
        console.log(x);
      }
    })();
    // a
    // b
    

    12. 异步 Generator 函数

    就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象:

    async function* gen() {
      yield 'hello';
    }
    const genObj = gen();
    genObj.next().then(x => console.log(x));
    // { value: 'hello', done: false }
    

    13. yield* 语句

    yield*语句也可以跟一个异步遍历器:

    async function* gen1() {
      yield 'a';
      yield 'b';
      return 2;
    }
    
    async function* gen2() {
      // result 最终会等于 2
      const result = yield* gen1();
    }
    

    与同步 Generator 函数一样,for await...of循环会展开yield*:

    (async function () {
      for await (const x of gen2()) {  // 也是相当于执行了gen的遍历器
        console.log(x);
      }
    })();
    // a
    // b
    

    最后

    因为比较多,所以目前只整理到这里,后续有些比较重要难懂的模块会分开更新,同时包括ES6的部分,希望对你有所帮助,如有不合理的地方欢迎指正,喜欢的就关注一波吧,后续会持续更新。

  • 相关阅读:
    18-10-11 关于触发器的学习
    18-10-29 关于设计器机器人等安装遇到的问题的解决方法
    18-10-25 全局函数测试总结 创建时间的目录 网页获取数据 写入数据
    18-09-08 关于Linux 的安装遇到的一些小坑
    18-08-27 机器人自动化之页面表格数据的定位拾取
    day 96 关于分页的使用
    day73 母版 中间件
    通过 U 盘启动重装 macOS 系统
    MAMP 环境下为 php 添加 pcntl 扩展
    使用 Composer 安装 Laravel 框架
  • 原文地址:https://www.cnblogs.com/BetterMan-/p/10428013.html
Copyright © 2011-2022 走看看