查看原文
其他

ECMAScript 双月报告:Array.fromAsync 进入 Stage 3

穹心 阿里巴巴终端技术 2022-10-16

作者:穹心
审校:昭朗

在本次 TC39 会议中,或许是由于在亚洲时区(东京时间)举办的原因,整体提交的提案数量较少,也仅有三个提案取得了阶段性进展。另外,本次会议中没有提案进入到 Stage 4 阶段。

Stage 2 → Stage 3

提案从 Stage 2 进入到 Stage 3 有以下几个门槛:

  1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
  2. ECMAScript 编辑签署了同意意见。

Array.fromAsync

提案链接:proposal-array-from-async[1]

在 JavaScript 中,Array.from 方法用于从一个类数组或可迭代对象(Iterable,即部署了 [Symbol.iterator] 接口的对象)创建一个新的数组。

在从可迭代对象创建数组时,其实际上等价于以下的代码:

const arr = [];
for (const v of iterable) {
  arr.push(v);
}

// 等价于
const arr = Array.from(iterable);

然而还有一种常见的场景是,从异步迭代对象(Async Iteratable,即部署了 [Symbol.asyncIterator] 接口的对象)类型创建数组,此时常见的方式是使用 for await of 语法:

const arr = [];
for await (const v of asyncIterable) {
  arr.push(v);
}

而为了在语言层面支持这一能力,此提案引入了 Array.fromAsync 方法,来从异步迭代对象生成数组。

此方法会从迭代对象(包括 Iteratable 与 Async Iteratable)立即生成一个 Promise ,其成功 resolve 将返回一个数组:

function sleep() {
  return new Promise((res, rej) => {
    setTimeout(res, 1000);
  });
}

Array.fromAsync = async (source) => {
  const arr = [];

  for await (const entry of source) {
    arr.push(entry);
  }

  return arr;
};

const arr = [1, 2, 3, 4];

// 异步迭代
async functionasyncGen() {
  for (const i of arr) {
    await sleep();
    yield i;
  }
}

// 同步迭代
functionsyncGen() {
  for (const i of arr) {
    yield i;
  }
}

(async () => {
  console.log(Array.fromAsync(syncGen()));
  console.log(Array.fromAsync(asyncGen()));
})();

以上调用均会立刻输出两个 Promise :

Promise { <pending> }
Promise { <pending> }

而如果我们 await 这两个 Promise,那么对 asyncGen() 的调用将会创建一个异步迭代器(Async Iterator),然后依次等待每一个内部的 Promise resolve,再将其值添加进结果数组,最后返回这个数组:

// 来自于 syncGen() 的调用会立刻返回
[ 1, 2, 3, 4 ]
// 来自于 asyncGen() 的调用等待 4s 后才打印
[ 1, 2, 3, 4 ]

而如果同步可迭代对象也返回了 Promise ,那么 fromAsync 同样会顺序地依次等待每一个 Promise resolve:

// 异步迭代
async functionasyncGen() {
  for (const i of arr) {
    await sleep();
    yield i;
  }
}

// 生成 Promise 的同步迭代
functionsyncGenWithPromise() {
  for (const i of arr) {
    yield sleep().then(() => i);
  }
}
(async () => {
  console.log(await Array.fromAsync(syncGenWithPromise()));
  console.log(await Array.fromAsync(asyncGen()));
})();
// 等待 4s 后打印
[ 1, 2, 3, 4 ]
// 再等待 4s 后打印
[ 1, 2, 3, 4 ]

但如果使用 Array.from 方法来迭代返回 Promise 的同步可迭代对象,实际上其中的各个 Promise 会是彼此独立的,即无需等待上一个 Promise settle :

// 生成 Promise 的同步迭代
functionsyncGenWithPromise() {
  for (const i of arr) {
    // 越往后,越快 resolve
    yield sleep(2000 - i * 100).then(() => {
      console.log(`${i} resolved`);
      return i;
    });
  }
}
(async () => {
  console.log(await Promise.all(Array.from(syncGenWithPromise())));
})();
4 resolved
3 resolved
2 resolved
1 resolved
[ 1, 2, 3, 4 ]

最后,你可能会想到与 Array.fromAsync 有些相似的 Promise.all 方法,但Promise.all将并行地等待内部所有的 Promise resolve,然后一次性返回所有结果:

// 生成 Promise 的同步迭代
functionsyncGenWithPromise() {
  for (const i of arr) {
    yield sleep().then(() => i);
  }
}

(async () => {
  console.log(await Promise.all(Array.from(syncGenWithPromise())));
})();
// 只需等待 1s
[ 1, 2, 3, 4 ]

Stage 1 → Stage 2

从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。

Well-formed Unicode strings

提案链接:proposal-is-usv-string[2]

ECMAScript 字符串都是 UTF-16 编码的字符串。在 Web API 中,我们可以发现有些 API (如 URL、URLSearchParams 等等系列 API)都声明了需要 USVString 作为参数。什么是 USVString?USV 代表 Unicode Scalar Value,即 Unicode 标量值。根据 Unicode 定义,Unicode 的码位(Code Point)可以分成几个类别,分别是图形码(Graphic),格式码(Format),控制码(Control),私有码(Private-Use),代理码(Surrogate),非字符码(Noncharacter),与保留码(Reserved)。而其中的代理码又分成了高位代理码与低位代码码,只有当一个高位代码码与一个低位代理码组合成一个代理码对,才是一个合法的 Unicode 字符。

目前,JavaScript 字符串并不限制这个字符串的值是否是合法的 Unicode 值,比如我们可以编码一个字符串只有高位代理码,而没有低位代理码等等。而如严格的 Web URL API 定义必须要求参数字符串是合法的 Unicode 标量值,因此我们需要有方法能够去区分一个字符串是否是合法的 Unicode 标量值。

这个提案提出为 ECMAScript 引入新的内置方法 String.prototype.isWellFormed,  用于检查这个字符串是否是一个合法的 Unicode 标量值:

'\ud800'.isWellFormed(); // => false
'\ud800\udc00'.isWellFormed(); // => true

另外此提案也提供了 String.prototype.toWellFormed 方法,来将普通字符串转换到一个格式正确的 USV 字符串。类似的,NodeJs 中也提供了 util.toUSVString 这样的方法来实现此功能。

Stage 0 → Stage 1

从 Stage 0 进入到 Stage 1 有以下门槛:

  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  3. 有问题、解决方案的例子;
  4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。

Extractor Objects

提案链接:proposal-extractors[3]

提取器语法是 Scala 中用于快速提取实例属性的语法糖,在 Scala 中,我们可以通过 apply 方法定义类的实例化方法,通过 unapply 方法(即提取器)反转这个过程——从实例获得实例化时的入参。

如以下的 Scala 代码:

object UserId:

 // 生成一个 UserId 字符串
  def apply(name: String) = s"userId--$name"

 // 从 UserId 字符串获得生成时的 name
  def unapply(userId: String): Option[String] =
    val stringArray: Array[String] = userId.split("--")
    if stringArray.tail.nonEmpty then Some(stringArray.tail) else None

// 定义了 apply 方法后,才能通过这种方式进行实例化
val userId1 = UserId("小明")  // userId-小明

// 通过提取器获得其 name 
val UserId(name1) = userId1
println(name1)  // 小明

// 也可以直接应用于字符串,在无法提取时会返回一个 None 类型
val UserId(name2) = "userId-大明"
println(name2)  // 大明

而其提案即旨在为 ECMAScript 引入提取器语法,包括数组提取器与对象提取器两种使用形式,如以下 JavaScript 代码:

class Foo {
  constructor(foo, bar, baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

const foo = new Foo();

// 提取 foo bar
const Foo(arg1, arg2) = foo;
// 提取 foo baz
const Foo{foo, baz} = foo;

以上代码使用的是绑定模式语法(Binding Pattern),你也可以使用分配模式(Assignment Pattern),有点类似函数声明与函数表达式的区别:

Foo(arg1, arg2) = foo;
Foo{foo, baz} = foo;

而提取器语法也可以和 Pattern Matching[4] 提案协作,我们还是先看看 Scala 中这两种语法的组合:

userId1 match
  case UserId(name1) => println(name1)  // 小明
  case _ => println("提取用户 ID 失败")

而在 ECMAScript 中,结合提取器语法和模式匹配,我们能够实现在解构赋值的同时进行校验或是二次处理,如以下的例子:

// 确保值为 Instance 类型,即一个不包含时区信息的精确时间
const InstantExtractor = {
  // 通过部署 Symbol.matcher 接口实现自定义匹配
  [Symbol.matcher]: value =>
    value instanceof Temporal.Instant ? { matched: true, value: [value] } :
    value instanceof Date ? { matched: true, value: [Temporal.Instant.fromEpochMilliseconds(value.getTime())] } :
    typeof value === "string" ? { matched: true, value: [Temporal.Instant.from(value)] } :
    { matched: false };
  }
};

class Book {
  constructor({
    title,
    // 在解构出这个值的同时,对其进行格式转换
    createdAt: InstantExtractor(createdAt) = Temporal.Now.instant(),
    modifiedAt: InstantExtractor(modifiedAt) = createdAt
  }) {
    this.title = title;
    this.createdAt = createdAt;
    this.modifiedAt = modifiedAt;
  }
}

而这也是解构赋值自 ES6 加入 JavaScript 以来一个呼声强烈的功能——解构时的额外处理逻辑。通过解构赋值结合提取器,我们能够将值的读取、校验与处理合并在一处,确保在后续消费时可以直接使用。

总结

由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:esdiscuss。

参考资料

[1]

proposal-array-from-async: https://github.com/tc39/proposal-array-from-async

[2]

proposal-is-usv-string: https://github.com/tc39/proposal-is-usv-string

[3]

proposal-extractors: https://github.com/tc39/proposal-extractors

[4]

Pattern Matching: https://github.com/tc39/proposal-pattern-matching



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存