JS用法:Map,Set和异步函数

Set & Map

Set

Es6提供的新数据结构Set,类似于数组,但是成员值均唯一,不重复。

set本身就是一个构造函数,用来生成Set数据结构。其中Set函数可以接受一个数组,也可以接受具有iterable的其他数据结构作为参数来实现初始化。

dart 复制代码
const set = new Set([1, 2, 3, 4, 4]);   // 1,2,3,4

set的属性和方法

Set的属性包含size,用于获取当前set的长度;

set的方法包含操作方法和遍历方法两大类

其中,操作方法主要如下:

  • add(value):添加某个值,返回 Set 结构本身。

    set在加入新的内容的时候,不会发生类型转换,它内部判断两个值是否相等类似于严格等于(===),单对于NAN却认为它等于自身。

csharp 复制代码
set2.add(NaN).add(NaN)  // size:1
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

set的遍历方法如下:

  • keys():返回键名的遍历器

  • values():返回键值的遍历器

  • entries():返回键值对的遍历器

  • forEach():使用回调函数遍历每个成员

    由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。在Set结构中,它默认的遍历器生成方法就是上述的values()方法,这意味这可以直接省略values方法,使用for...of即可

  • 代码演示

javascript 复制代码
let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}

console.log('--------使用values()----------')

for (let item of set.values()) {
  console.log(item);
}
console.log('------------------')

for (let item of set.entries()) {
  console.log(item);
}
console.log('--------直接使用of----------')
console.log(Set.prototype[Symbol.iterator] === Set.prototype.values)
for (let x of set) {
  console.log(x);
}

set的应用

  • 由于扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构,这里就出现了我们常用的数组去重方法:[...new Set(array)] 同时也可以用于字符串去重:
sql 复制代码
[...new Set('ababbc')].join('')
  • 使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

    • 代码演示
javascript 复制代码
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
console.log(union)

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
console.log(intersect)


// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
console.log(difference)
  • 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

    • 代码演示
javascript 复制代码
// 方法一
let set1 = new Set([1, 2, 3]);
set1 = new Set([...set1].map(val => val * 2));
console.log(set1);

// 方法二
let set2 = new Set([1, 2, 3]);
set2 = new Set(Array.from(set2, val => val * 2));
console.log(set2)

WeakSet

weakSet和set类似,主要的区别时:

首先,WeakSet 的成员只能是对象和** Symbol** 值,而不能是其他类型的值;

其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存

这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。**因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。**由于这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。

另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历

基本使用

WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

javascript 复制代码
const a = [[1,2],[2,3]]
const ws = new WeakSet(a);
ws.add(1) // 报错
const b = [1,2]
const ws1 = new WeakSet(b);  // error
console.log(ws1)

WeakSet 结构有以下三个方法。

  • add(value):向 WeakSet 实例添加一个新成员,返回 WeakSet 结构本身。
  • delete(value) :清除 WeakSet 实例的指定成员,清除成功返回true,如果在 WeakSet 中找不到该成员或该成员不是对象,返回false
  • has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
weakSet的应用
  • 可以使用 WeakSet 跟踪一组对象,而不影响这些对象的垃圾回收。这在调试和监控工具中尤为有用。

    • 代码演示
javascript 复制代码
const trackedObjects = new WeakSet();

class MyClass {
  constructor() {
    trackedObjects.add(this);
  }

  isTracked() {
    return trackedObjects.has(this);
  }
}

const obj1 = new MyClass();
console.log(obj1.isTracked()); // true

const obj2 = new MyClass();
trackedObjects.delete(obj2);
console.log(obj2.isTracked()); // false
  • 可以使用 WeakSet 实现类的私有属性。这样,私有属性不会暴露给外部,只能通过类的方法来访问。

    • 代码演示
javascript 复制代码
const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
    }
  }
}
let f = new Foo()
f.method() // 通过
Foo.prototype.method() // 报错

Map

js的对象(Object) 本质上时键值对的集合,但是传统上只能使用字符串当作键值,这在使用上产生了很多的限制。

  • 代码演示
xml 复制代码
<body>
  <div id="box">123</div>
  <script>
    const data = {};
    const element = document.getElementById('box');
    console.log(element); 
    data[element] = 'metadata';
    console.log("data",data)
    console.log(data['[object HTMLDivElement]']) // "metadata"
  </script>
</body>

由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。为了解决这个问题,ES6提供了Map数据结构,与对象的主要区别在于"键"的范围不在局限于字符串,各种类型的值都可以作为键。通俗点说,对象提供了了"字符串-值"的对应,Map提供了"值-值"的对应。还是上面的例子,我们使用Map来实现,效果如下:

map也可以接受数组作为参数,数组的成员是一个个表示键值对的数组,如果对同一个键多次赋值,后面的值将覆盖前面的值,这里我们要注意,只有对同一个对象的引用,才算是同一个键值 。如果读取一个未知的键,则返回undefined

  • 代码演示
dart 复制代码
const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
console.log('-----------------------')
const map1 = new Map();
map1.set(1,'aaa').set(1,'bbb');
console.log(map1.get(1));  // "bbb" 会覆盖前面的值
console.log(map1.get(2));  // undefined
console.log('-----------------------')
const map2 = new Map();
const k1 = ['a'];
map2.set(['a'], 555);
map2.set(k1, 666);
console.log(map2.get(['a']) ) // undefined
console.log(map2.get(k1) ) // 666
console.log(map2.size)
console.log('-----------------------')
const map3 = new Map();
map3.set(NaN, 123);
console.log(map3.get(NaN)) // 123
map3.set(NaN, 456);
console.log(map3.get(NaN)) // 456

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然 **NaN**不严格相等于自身,但 Map 将其视为同一个键

map的属性和方法

属性:size,返回map结构的成员总数

操作方法

  • set(key,value): set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。可以采用链式写法。
  • get(key):get方法读取key对应的键值,如果找不到key,返回undefined
  • has(key): has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中.
  • delete(key):delete方法删除某个键,返回true。如果删除失败,返回false
  • clear(): clear方法清除所有成员,没有返回值。

遍历方法

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

需要特别注意的是,Map 的遍历顺序就是插入顺序。

与set类似,直接使用for...of,等同于使用map.entries(),表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

arduino 复制代码
map[Symbol.iterator] === map.entries   // true

map的应用

  • 实现图结构

    • 代码演示
    javascript 复制代码
    class Graph {
      constructor() {
        this.adjacencyList = new Map();
      }
    
      addVertex(vertex) {
        this.adjacencyList.set(vertex, []);
      }
    
      addEdge(vertex1, vertex2) {
        this.adjacencyList.get(vertex1).push(vertex2);
        this.adjacencyList.get(vertex2).push(vertex1);
      }
    }
    
    const graph = new Graph();
    graph.addVertex("A");
    graph.addVertex("B");
    graph.addVertex("C");
    
    graph.addEdge("A", "B");
    graph.addEdge("A", "C");
    
    console.log(graph.adjacencyList);
  • 记录元数据

    • 代码演示
    ini 复制代码
    const metadata = new Map();
    
    const user1 = { id: 1, name: "John" };
    const user2 = { id: 2, name: "Jane" };
    
    metadata.set(user1, { role: "admin" });
    metadata.set(user2, { role: "user" });
    
    console.log(metadata.get(user1)); // { role: 'admin' }
    console.log(metadata.get(user2)); // { role: 'user' }

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。区别有两点。首先,WeakMap只接受对象(null除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

一般情况下,我们想在一个对象上存放一些数据,会形成对这个对象的引用,一旦不再需要对象就必须手动删除引用,否则垃圾回收机制不会使用占用的内存,造成内存泄漏。WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。也就是说,其他位置对该对象的引用一旦消除,该对象占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对,也会自动消失。

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

下面我们可以看一下垃圾回收在weakmap上的效果:

  • 代码演示

    首先我们通过手动执行垃圾回收机制,并查看当前内存使用情况。当前使用大概在6M的样子

    scss 复制代码
    $ node --expose-gc    // 允许手动执行垃圾回收机制
    > global.gc()
    > process.memoryUsage()

然后我们创建一个weakmap和一个数组类型的key。这时的数组被引用了两次,一次时变量key的引用,一次时weakmap的引用,但是weakmap时弱引用。

vbnet 复制代码
> let wm = new WeakMap()
> let key = new Array(5 * 1024 * 1024)
> key
> wm.set(key,1)

这个是时候我们再看一下内存的使用情况,会发现内存被占用了很多

markdown 复制代码
> global.gc()
> process.memoryUsage()

此时我们清空变量key对数组的引用,单不需要手动清除weakmap的实例对键名的引用,并手动执行垃圾回收机制,查看内存的使用情况,会发现内存占用回到了原本的6M,可以看出weakmap的键名没有阻止gc对内存的回收。

markdown 复制代码
> key = null
> global.gc()
> process.memoryUsage()
基本使用

WeakMap 与 Map 在 API 上的区别主要是两个。

一是没有遍历操作(即没有keys()values()entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。

二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()set()has()delete()

weakmap的应用

场景一:WeakMap 应用的典型场合就是 DOM 节点作为键名

javascript 复制代码
 let myWeakmap = new WeakMap();

    myWeakmap.set(
      document.getElementById('logo'),
      {timesClicked: 0})
    ;

    document.getElementById('logo').addEventListener('click', function() {
      let logoData = myWeakmap.get(document.getElementById('logo'));
      logoData.timesClicked++;
      console.log(myWeakmap.get(document.getElementById('logo')))
    }, false);

document.getElementById('logo')是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。

场景二:部署私有属性

javascript 复制代码
// 创建一个 WeakMap 用于存储私有属性
const privateData = new WeakMap();

class MyClass {
  constructor(name) {
    // 将私有属性存储在 WeakMap 中
    privateData.set(this, {name});
  }

  // 公共方法可以访问私有属性
  getName() {
    return privateData.get(this).name;
  }

  // 修改私有属性的方法
  setName(newName) {
    privateData.get(this).name = newName;
  }
}

const person = new MyClass("Lily");
console.log(person.getName()); 
person.setName("Tom");
console.log(person.getName()); 

// 私有属性无法从实例直接访问
console.log(person.name); 

Promise & Generator & Async

Promise

Promise是异步编程的一种解决方案,简单点说,promise是一个容器里面保存着未来才结束的事件的结果。其主要特点有两个:

(1)promise有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

(2)一旦状态发生了变化就不会在改变了。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

基本使用

Promise对象是一个构造函数,用来生成Promise实例。构造函数接受一个函数作为参数,函数的两个参数分别为resolvereject

注意,调用resolve或reject并不会中断promise的操作,一般来说最好在他们前面加上return语句。

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。**finally本质上是 then**方法的特例

  • 链式调用example
javascript 复制代码
function firstTask() {
  return new Promise((resolve) => {
    console.log("First task");
    resolve();
  });
}

function secondTask() {
  return new Promise((resolve) => {
    console.log("Second task");
    resolve();
  });
}

function thirdTask() {
  return new Promise((resolve) => {
    console.log("Third task");
    resolve();
  });
}

firstTask()
  .then(() => {
    return secondTask();
  })
  .then(() => {
    return thirdTask();
  })
  .catch((error) => {
    console.error("An error occurred:", error);
  });

其他API:

  • Promise.all(): 用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    • const p = Promise.all([p1, p2, p3])

      p的状态由p1p2p3决定,只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

      只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

      注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

    • example

    javascript 复制代码
    const p1 = new Promise((resolve, reject) => {
      resolve('1');
    })
    .then(result => result)
    .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      resolve('2');
    })
    .then(result => result)
    .catch(e => e);
    
    const p3 = new Promise((resolve, reject) => {
      throw new Error('报错了');
      // resolve('3');
    })
    .then(result => result)
    // .catch(e => e);
    
    Promise.all([p1, p2, p3])
    .then(result => console.log('res',result))
    .catch(e => console.log('error',e));
  • Promise.race():同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

    • const p = Promise.race([p1, p2, p3]);

      只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

  • Promise.allSettled():有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作**。****Promise.all()方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。为了解决这个问题,ES2020 引入了 Promise.allSettled()****方法,**用来确定一组异步操作是否都结束了(不管成功或失败)。

    • example
    typescript 复制代码
    const promise1 = Promise.resolve("Promise 1");
    const promise2 = Promise.reject(new Error("Promise 2 rejected"));
    const promise3 = new Promise((resolve) => setTimeout(() => resolve("Promise 3"), 1000));
    
    // 使用 Promise.allSettled()
    Promise.allSettled([promise1, promise2, promise3])
      .then((results) => {
        results.forEach((result, index) => {
          if (result.status === "fulfilled") {
            console.log(`Promise ${index + 1} is fulfilled with value: ${result.value}`);
          } else if (result.status === "rejected") {
            console.log(`Promise ${index + 1} is rejected with reason: ${result.reason}`);
          }
        });
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
    
    // Output:
    // Promise 1 is fulfilled with value: Promise 1
    // Promise 2 is rejected with reason: Error: Promise 2 rejected
    // Promise 3 is fulfilled with value: Promise 3
    
    // 使用 Promise.all()
    Promise.all([promise1, promise2, promise3])
      .then((values) => {
        values.forEach((value, index) => {
          console.log(`Promise ${index + 1} is fulfilled with value: ${value}`);
        });
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
    
    // Output:
    // An error occurred: Error: Promise 2 rejected

    Promise.all() 会在遇到第一个 rejected Promise 时立即拒绝整个组,而 Promise.allSettled() 会等待所有 Promise 结束,不管是完成还是拒绝。

  • Promise.any():只要参数实例有一个变成 **fulfilled状态,包装实例就会变成 fulfilled状态如果所有参数实例都变成 rejected状态,包装实例就会变成 rejected**状态Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

  • Promise.resolve():将现有对象转为 Promise 对象

    • 等价关系
    javascript 复制代码
    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
  • 如果参数是promise对象,Promise.resolve()将原封返回这个实例。

  • 如果参数是一个具有then方法的对象,Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

  • 如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

  • Promise.reject()

    • Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected
    • 等价关系
    javascript 复制代码
    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))

应用

  • 加载图片:将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

    • example
    javascript 复制代码
    const { Image } = require("canvas");
    const fs = require("fs");
    
    function loadImage(path) {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => resolve(image);
        image.onerror = (error) => reject(error);
        image.src = path;
      });
    }
    
    const imagePath = "./O1CN01jJUykX1zwNMUeekbT_!!1990086778.jpg";
    
    loadImage(imagePath)
      .then((image) => {
        console.log("Image loaded:", image);
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
  • ajax封装

Generator

Generator是ES6提供的一种异步编程的解决方案,语法上可以把他理解成一个状态机,封装了内部多个状态。由于执行generator函数会返回迭代器对象,我们还可以把generator函数理解成遍历器对象的生成函数。

在形式上,generator函数是一个普通函数,主要有两个特征,一是,function关键字和函数名之间有一个*;二是函数内部使用yield表达式定义不同状态。

在调用上,主要特点是调用之后,函数并不执行,返回的也不是函数运行的结果,而是一个执行内部状态的指针对象(遍历器对象),内次调用next方法,会让内部指针从上一次停下来的地方开始执行,知道遇到下一个yield语句或者return语句。可以理解成yield是暂停执行的标识,next是恢复执行的方法。

其中,yield表达式只有在next方法调用,内部执行指向该语句时才会执行,类似于"惰性求值"的语法功能。即下面例子中的123+456不会立即求值。

javascript 复制代码
function* gen() {
  yield  123 + 456;
}

每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

前面我们提到过,Generator 函数就是遍历器生成函数,那么我们可以把generator赋值给对象的Symbol.iterator属性,从而使得对象具备Iterator接口,从而可以被...运算符遍历。

基本使用

Generator 函数可以暂停执行和恢复执行 ,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制

数据交换

javascript 复制代码
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**接收。

错误处理 :使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获

javascript 复制代码
function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

异步任务封装

ini 复制代码
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log('res',result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

基于Thunk函数的自动执行

Thunk 函数是自动执行 Generator 函数的一种方法,可以用于 Generator 函数的自动流程管理。下面我们以文件读取为例来了解一下使用。

首先什么是Thunk函数,编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。举例说明:

javascript 复制代码
function f(m) {
  return m * 2;
}
f(x + 5);

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

function f(thunk) {
  return thunk() * 2;
}

js语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。举例说明

ini 复制代码
// 正常版本的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);

Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。

  • example

    内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

scss 复制代码
function run(gen){
  var g = gen();
  // 层层添加回调函数
  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}

run(gen);

co模块

co 也可以自动执行 Generator 函数,前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。

  • 使用

    • example
    javascript 复制代码
    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);
        });
      });
    };
    var gen = function* () {
      var f1 = yield readFile('../Async/file1.txt');
      var f2 = yield readFile('../Async/file2.txt');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    
    var co = require('co');
    co(gen).then(function () {
      console.log('Generator 函数执行完成');
    });

Async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。我们可以使用generator和async分别实现两个文件读取的异步操作。一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

但async对Generator的主要改进在以下几方面:

  • Generator 函数的执行必须靠执行器 ,所以才有了co模块,而async函数自带执行器。不同于Genertor需要co模块或next方法才能执行
  • async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。语义上比*和yeild更明确。
  • yeild语句后面只能时Thunk函数或Promise对象,但是await后面可以是Promise对象或原始类型的值。
  • async返回的是Promise对象,比generator返回Iterator对象方便,可以直接使用then指定下一个操作。

基本用法

async

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。其中return语句返回的值会成为then方法回调函数的参数。

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

  • example
javascript 复制代码
async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)</title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)

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

await命令

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象,这里我们可以看一个休眠的例子。

  • sleep example
javascript 复制代码
class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  // then (resolve, reject) {
  //   const startTime = Date.now();
  //   setTimeout(
  //     () => resolve(Date.now() - startTime),
  //     this.timeout
  //   );
  // }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();

如果没有then方法直接返回一个sleep对象,定义了then方法会将它看作一个promise处理。

使用注意点
  • 防止中断处理

    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject而任何一个 **await语句后面的 Promise 对象变为 reject状态,那么整个 async函数都会中断执行。但是很多情况下,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将 第一个****await放在 try...catch结构里面或者单独给第一个await加上一个catch,这样不管这个异步操作是否成功,第二个 await**都会执行

    • example
javascript 复制代码
// reject
async function f1() {
  await Promise.reject('出错了');
  return await Promise.resolve('hello world'); 
}
f1().then(v => console.log(v)).catch(e => console.log(e))
//error
async function f4() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
  return await Promise.resolve('hello world');
}
f4().then(v => console.log(v)).catch(e => console.log(e))

优化处理

javascript 复制代码
// reject
async function f2() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
    console.log(e)
  }
  return await Promise.resolve('hello world'); 
}
f2().then(v => console.log(v)).catch(e => console.log(e))
// error
async function f5() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
    // console.log(e)
  }
  return await Promise.resolve('hello world');
}
f5().then(v => console.log(v)).catch(e => console.log(e))
  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。通常我们可以使用promise.all

  • async函数可以保留运行堆栈,当使用 async 函数时,它会在执行异步操作时暂停函数的执行,并在操作完成后恢复执行。这种暂停和恢复不会导致堆栈信息丢失,因为 async 函数会确保在恢复执行时保留堆栈信息。

    • example
    vbnet 复制代码
    async function fetchData() {
      try {
        const response = await fetch("https://api.example.com/data");
        const data = await response.json();
        return data;
      } catch (error) {
        console.error("Error:", error);
        // Error stack contains the correct context of the error
        console.error("Error stack:", error.stack);
      }
    }
    
    fetchData();

async 函数可以保留运行堆栈,使得在处理异步操作时,可以轻松获取错误发生的上下文,进行调试。这使得错误处理更加简洁和直观,提高了代码的可读性和可维护性。

基本原理

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

javascript 复制代码
async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

javascript 复制代码
function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
  **  function step(nextF) {**
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      **if(next.done) {
        return resolve(next.value);
      }**
      Promise.resolve(next.value).then(function(v) {
       ** step(function() { return gen.next(v); });**
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
   ** step(function() { return gen.next(undefined); });**
  });
}

总结:

三种方案都是为解决传统的回调函数而提出的,所以它们相对于回调函数的优势不言而喻。而async/await又是Generator函数的语法糖。

  • Promise的内部错误使用try catch捕获不到,只能只用then的第二个回调或catch来捕获,而async/await的错误可以用try catch捕获
  • Promise一旦新建就会立即执行,不会阻塞后面的代码,而async函数中await后面是Promise对象会阻塞后面的代码。
  • async函数会隐式地返回一个promise,该promisereosolve值就是函数return的值。
  • 使用async函数可以让代码更加简洁,不需要像Promise一样需要调用then方法来获取返回值,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。
相关推荐
烛阴2 分钟前
简单入门Python装饰器
前端·python
袁煦丞1 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作
天天扭码1 小时前
从图片到语音:我是如何用两大模型API打造沉浸式英语学习工具的
前端·人工智能·github
Liudef061 小时前
2048小游戏实现
javascript·css·css3
鱼樱前端2 小时前
今天介绍下最新更新的Vite7
前端·vue.js
coder_pig3 小时前
跟🤡杰哥一起学Flutter (三十四、玩转Flutter手势✋)
前端·flutter·harmonyos
万少3 小时前
01-自然壁纸实战教程-免费开放啦
前端
独立开阀者_FwtCoder3 小时前
【Augment】 Augment技巧之 Rewrite Prompt(重写提示) 有神奇的魔法
前端·javascript·github
yuki_uix3 小时前
AI辅助网页设计:从图片到代码的实践探索
前端
我想说一句3 小时前
事件机制与委托:从冒泡捕获到高效编程的奇妙之旅
前端·javascript