hax.github.com icon indicating copy to clipboard operation
hax.github.com copied to clipboard

Why Callback/Thunk Sucks and Promise Rocks

Open hax opened this issue 10 years ago • 22 comments

Callback 在大型编程时的一般性问题

Callback hell 就不谈了

无法区分异步任务 callback(针对单一结果的处理) 和 listener(针对一类结果的处理)

例子:fs.watch 在fs包中是特殊的

调用时是无法区分的,所以需要api文档说明。但是文档往往缺乏此说明。

无法区分 sync callback 和 async callback

请阅读:Designing APIs for Asynchronyshort version

调用时是无法区分的,所以需要api文档说明。但是文档往往缺乏此说明,且api设计者也很可能忽略这点而release zalgo。

Node callback convention 的问题

node callback 相关的签名是基于约定的:

function asyncCall(...params, asyncCallback)
function asyncCallback(err, ...params)

基于约定导致:

约定会有例外

  • fs.exists 的 callback 签名是 function (boolean)
  • Timers 的 callback 参数位置是在第一个而不是最后一个

实际上,Timers的情况是一个普遍问题。Timers来自于Web Platform API,而 Web API 不可能遵循 Node callback 约定,所以一切使用 Web API 或需要保持和 Web API 兼容的,都面临约定不一致问题。

err 参数应该是 Error 对象

参见:A String is not an Error

尽管此问题与callback本身并无必然关系(你也可以直接 throw 'string' ),但是 callback 形式更容易忽视。例子:async.js的代码示例

callback 也更难 enforce 此规则。比较:

function f(x) {
  if (invalid(x)) throw 'invalid x'
  ...
}
function f(x, callback) {
  if (invalid(x)) callback('invalid x')
  ...
}

对于前者,我们可以检查所有 throw 子句。而后者就困难多了。

特别注意,引擎其实可以对throw进行特殊处理,比如Chrome就是如此。

更容易 release Zalgo

前一个代码示例表明在做参数检查时很容易就同步调用callback从而破坏了前述async/sync区分的必要性。

thunks 的问题

具有前述所有问题(除了callback hell)

形式极简的API未必是好的

  • API形式的简单其实把复杂性留到了别处
  • API命名和概念上的不清晰:
    • thunk(一般概念 VS 特定所指——参数为node callback约定形式的函数)
    • thunks(包的export,实际是提供类似Domain的机制)
    • Thunk(构造器)
    • 链式调用返回thunk(接受的参数是node风格的回调,但是返回值处理有特例)
  • 构造器的签名和后续调用实际是不同的
  • 人类需要冗余信息来辅助阅读

微妙的语义差异

  • 不像then可以被调用多次
  • 如果thunk调用返回一个函数会被当成thunk,但是实际上无法区分这里需要thunk还是真的希望返回一个函数(实际上既和CPS风格不一致,又破坏函数式编程)

当前实现就是 releasing Zalgo 的!

而当前thunks的所谓性能优势(其实跟bluebird/when/rsvp等典型promise实现比完全没有优势)其实也来自于尽量同步执行,代价就是 releasing Zalgo。

由于实现方式的问题,当调用链过长时会 Maximum call stack size exceeded 。而原生Promise和所有主要的 Promise/A 实现都没有这个问题。

Thenjs的问题

名同实异

  • then 方法和 Promise 的 then 不一样

实现方式问题

  • 和 thunks 一样也有 call stack size 问题

Promise 的特点

语义清晰

  • 一定只针对单一结果
  • 必然是异步
  • 基于明确的API,而不是基于约定
  • 不需要语言外的错误处理机制

标准

  • 基于社区共识,大部分开发者对它有一致的认知,有充分的实践,也经过编程语言专家的充分研究和探讨而定型
  • 性能不是问题——现有的Promise/A的主流实现性能非常非常好,而native实现早晚会得到引擎的充分优化
  • 更好的错误处理——如chrome开发版中已经把reject和throw一样可以trace普通值
  • 未来语言设施(ES6 loader、ES 7 async/await 等)和新API(ServiceWorker、Stream等)的相容性

不足

  • Cancelable (ES8?)
  • Observable (ES7)

hax avatar Dec 25 '14 08:12 hax

前面看到那篇讲 Promise 是 Monad 实现的那篇博客当时着迷了一会, 现在中文版在浏览器里开了两天居然没看完: http://www.ituring.com.cn/article/50561 那篇文章讲到 Promise 是可以复合的操作(而 callback 不能)的时候, 我开始困惑了 包括前天晚上尝试了一下 Promise.all, 抽象确实比 callback 合理.. 挺希望有机会搞清楚 FP 理论当中是如何处理这些概念的, 但又是个巨坑 >_<

tiye avatar Dec 31 '14 17:12 tiye

从我认识 @ZENSH严清,接触他写的 Thenjs 开始,相继又出现了 co,和 Thunkjs。他们的目的都是为了解决同一类问题,异步流程管理。Thenjs 相比较于 async 我认为好用多了,async 每次用我都要看它的 API。而 co 的源码逻辑太绕,刚看懂我又忘记了,看不懂我根本不会使用。Thunk 其实到了 co 中 thunk 概念的启发。然后,现在 @ZENSH严清 正在搞 toa,要以一个异步流程模式的书写方式,搭起一个帝国,我个人认为是不现实的。我连 koa 都不看好。

Thenjs、Thunk和Promise要解决的问题,和解决问题的方式基本上是一致的。既然标准有了Promise,而且Promise比起前者,更容易理解,更加从细节上弥补了callback的不足,为什么还要一条路走到黑?

期待以后node API 支持 Promise。

island205 avatar Dec 31 '14 22:12 island205

@island205 本文其实主要是献给 @zensh 的。我认为说问题不在于重造轮子,或者搭帝国是不是现实的问题,而是方案本身的优劣。thunk和promise其实本质是一样的,本文希望解释清楚在本质一样的前提下,为什么promise比thunk好。 @jiyinyiyong 按我理解,在纯函数式语言中,异步不是大问题,因为没有副作用,执行先后顺序没关系。

hax avatar Jan 01 '15 00:01 hax

不太明白什么是 releasing Zalgo, 于是找到了这篇文章 Designing APIs for Asynchrony

fengmk2 avatar Jan 01 '15 01:01 fengmk2

@hax 纯函数语言里不涉及 IO 的代码都是无所谓的, 但是 IO 还是需要按顺序执行. 为了在没有顺序的代码里强制指定执行顺序, 就要让后一个操作依赖前一个的结果.. 总之用 Monad 实现了很怪的办法. 语法上相当于把回调强制写成了平级, 比如 LiveScript 里的类似一个例子(这里没有 Haskell 的 IO 类型实际上有差别): http://livescript.net/#functions-backcalls

do
  data <-! $.get 'ajaxtest'
  $ '.result' .html data
  processed <-! $.get 'ajaxprocess', data
  $ '.result' .append processed

alert 'hi'
$.get('ajaxtest', function(data){
  $('.result').html(data);
  $.get('ajaxprocess', data, function(processed){
    $('.result').append(processed);
  });
});
alert('hi');

但是我还不知道这样的方案怎样处理报错, 还有同时管理多个请求的方案. 加个问题先: http://segmentfault.com/q/1010000002456002


补充一下第二天想到的:

Haskell 里应该是这样, 一个 比如一个类型为 String -> IO String 的函数 fetchString, 返回值设为 ioResString, 这个函数执行需要额外的时间, 那么基于下面这样的考虑:

  • Haskell 里的函数都是并行的(除非参数另一个函数的结果有依赖)
  • IO String 是一个异步函数返回的值, 完全不同于 JavaScript 异步的 undefined

那么 ioResString 在 Haskell 当中直接用 IO String 表示一个异步调用完成才生成的值, 相比而言, Promise 当中需要用 function(resString){/* control flow */} 模拟 就是说 Promise 想要模拟的返回值实际上在 Haskell 当中默认以值的形式存在 总之能拿到返回值, 控制流程就比较方便了. callback 没有返回值, 对控制流进行组合的能力就是不存在的, 非常弱

tiye avatar Jan 01 '15 01:01 tiye

有了 Promise 为什么还是需要 thunks

答复 @hax 的 https://github.com/hax/hax.github.com/issues/11#issuecomment-68478326

关于 release Zalgo

详细描述在这里 http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony ,简而言之,下面这个 demo 就是 release Zalgo:

var cache = {};
function maybeSync(arg, callback) {  
  if (cache[arg]) return callback(null, cache[arg]);

  fs.readFile(arg, function (err, data) {
    if (err) return callback(err);
    cache[arg] = data;
    callback(null, data);
  })
}

callback 可能同步执行也可能异步执行,防止 release Zalgo(dezalgo) 即把

if (cache[arg]) return callback(null, cache[arg]);

强制转换成异步执行,如:

if (cache[arg]) return process.nextTick(function() {
  callback(null, cache[arg]);
});

那么 release Zalgo 会有什么问题呢?文章中举例同步锁问题,但我个人认为那是程序写法有问题,我从来不会那么写。我遇见的问题可能就是 Zalgo API 可能导致一些异步库出现 Maximum call stack size exceeded,比如 co v3 就不应该就接受 Zalgo API。thunks 则不会受 Zalgo API 的困扰。

关于 thenjs

经测试,thenjs 在以下同步模式中确实出现 Maximum call stack size exceeded

function nextThen(cont, res) {
  // 如果这里是异步,则不会出错,
  return cont(null, res + Math.random());
}

var thenjs = Thenjs(0);
for (var i = 0; i < 10000; i++) thenjs = thenjs.then(nextThen);

之前确实未考虑通过 then 链来处理上千的同步顺序逻辑。这类需求一般用 thenjs.series 来处理的话,不管同步还是异步,都不会出错。

当时写 thenjs 纯粹为了方便异步流程控制,那时候我接触的 Promise 还是老式的 var deferred = Q.defer() 形式,像裹脚布一样,我不喜欢。由于异步流程控制主要用于 node.js,我便选择了 callback 形式,结合 then 链,后又融合了 async 的主要 API。thenjs 结合 node.js 原生的 callback API,写异步流程控制是非常方便的,运行速度也是极快的。

但它的缺点正如 @hax 所说,Promise 的 then 大行其道的时候,thenjs 的 then 就不标准了,可能会导致第三方库无法辨识。所以 thenjs 也只适合做异步流程控制,不适合用于异步 API,因为第三方库不会对它封装的 API 做兼容(只有 thenjs 自己和 thunks 能识别 thenjs 封装的 API)。

那么 thenjs 还好用吗?在 promise API 还未大量普及的时候,如果你喜欢 callback 风格,如果你追求极致速度,那么就放心的使用。对于偏向 callback 风格的我建议使用 thunks 进行流程控制,否则就使用 bluebird 之类的 Promise 库,原生 Promise 是不适合用作流程控制的。

关于 Promise

Promise 的最大优势是标准,各类异步工具库都认同,未来的 async/await 也基于它,用它封装 API 通用性强,用起来简单。thunks 也完全支持 Promise 封装的 API,所以如果 node.js 的异步 API 全部转 promise,thunks 也是无缝对接。

在我的认识中,Promise 不仅仅是统一了异步接口(用于封装 API),它更强大的地方应该是组合能力,像搭积木一样,将各种各样的异步 API 调用组合成一个简单的新 promise。所以,我对它的期望变得更高:它应该是一个完美的异步流程工具,但显然,原生 Promise 无法胜任这一角色。如果要用 promise 做异步流程控制,必须引入第三方 Promise 库(那么引入 bluebird 与引入 thunks 又有何区别?后者更小巧更强大)

原生 Promise 无法胜任异步流程管理的理由:

  1. 缺乏 debug 机制,在一个复杂的异步流程中,很难追踪出问题的点;
  2. 缺乏语法糖 API,如 sequence 队列,promiseify,delay 之类,在业务场景中都是很有用的东西,原生 Promise 显然不会加入这些东西;
  3. 缺乏 generator 支持,无疑,用 generator/yield 书写异步流程简单优美,用上了就再也丢不掉,原生 Promise 显然也不会加入这些东西;
  4. 缺乏自定义 context 支持,如果熟悉了 koa 便知道,支持自定义 context 的异步库写异步业务将是多么简洁优美;

总之,原生 Promise 的定位就是提供基础的异步能力,没有更多。

关于 thunks

再来看看最纯粹的 thunk 的定义:

一个封装了业务逻辑的函数,它接受唯一一个参数callback,当业务逻辑执行完毕会调用这个callback,它的第一个参数是 error,第二个往后的参数是业务逻辑执行结果

一个简单的 demo 如下:

//假设 fs.readFile 返回 thunk
var thunk = fs.readFile('./package.json');
thunk(function(err, file) {
  console.log(err, file);
});

或者直接简化为

//假设 fs.readFile 返回 thunk
fs.readFile('./package.json')(function(err, file) {
  console.log(err, file);
});

有两点需要注意:

  1. 纯粹的 thunk 并不依赖 thunks 库,也不依赖任何其它东西,node.js 诞生之初完全可以将异步 API 设计成返回 thunk 的形式(那么,当时为什么没有这么做呢?)
  2. thunk 是惰性求值的,也就是说 thunk = fs.readFile('./package.json') 并没有开始读文件,调用 thunk(callback) 后才开始读文件的,这一点与现有的 node.js API fs.readFile('./package.json', callback) 并无区别。而 promise 是及早求值的,如执行 promise = fs.readFile('./package.json') 时就已经开始读文件了。

从统一异步接口的角度来看,thunk 也具有高度的一致性,相对 promise,它更简单,纯函数天然支持,无需额外的 Promise 实现。

另外 async/await 既然支持 promise,那么支持 thunk 也应该毫无压力。

它唯一缺的就是异步组合能力(但底层 API 不需要依赖这个来输出 thunk),这就是 thunks 所做的事。

thunks 把 Promise 的异步组合能力融入了 thunk,可以像组合 promise 一样来组合 thunk 函数。更进一步的是,thunks 不仅仅能组合 thunk 函数,还能组合 promise、generator 甚至一般值。简而言之,thunks 能组合一切,不管是同步还是异步,不管是 thunk、 promise、generator还是其它。具体可参阅 API 和 examples。

我渴望的 node.js 的美好场景是,底层异步 API(如 fs模块、redis 客户端等)输出纯粹的 thunk 函数,而用户业务层 API 则基于 thunks 输出 thunk 函数。

当然,现实中 node.js 还是提供着 readFile(arg, callback) 之类的缺乏一致性的原始 API,而社区群众呼吁的却是 promise API。thunks 也不担心这些,thunks 通吃这两类 API。

关于 @hax 提的几个问题:

  1. releasing Zalgo 和 Maximum call stack size exceeded,thunks 已经不存在这个问题了,thunks 不害怕 releasing Zalgo API 爆栈。
  2. promise 的 then 可多次调用,而 thunks 的 thunk 只能调用一次。用两个比喻:一,我让张三去打一桶水,张三打了水交给我,我再次找张三要他还能交给我一桶水?二,我让李四临摹一副蒙拉丽莎,李四完成后交给我,我再找他要他还能立即给我一副?所以,在我看来,then 的可重复调用真的不符合逻辑,即便有这种逆天需求,也应该是用户实现,而不是基础库实现。再比如用 promise.then,函数 A 从中取了一个对象 {},并做了修改,函数 B 再从这个 then 中取值,那么 B 期待的数据是已修改的还是未修改的呢?
  3. thunk 函数中 return 函数的问题,的确,thunks 会把返回的函数当做 thunk 函数或 generator 函数处理,如果 return 了一个不符合 thunk 函数格式的普通函数,就会出问题。如果要返回函数,只能放在包在数组或对象中了。我目前还没有接触过这类需求。

先写到这里,后面想到了再完善。总而言之,Promise 强势崛起的时候,thunks 只会强调自己是一个强大的异步流程管理库,能完美处理 promise API。当然,我更期望 (纯粹的)thunk 也能成为一个标准的异步 API 接口,获得 ES 的原生支持。

zensh avatar Jan 04 '15 12:01 zensh

  1. 你还没有搞明白releasing zalgo的害处。我最近因为要准备css conf,所以暂时没时间写例子了。但是还是希望可以再读读isaac那篇文章体会一下。注意问题不是受不受影响,而是thunks本身现在就是releasing zalgo的。你注意,promise就不会releasing Zalgo,promise.resolve(x).then(f),这个f一定是异步执行的。为什么promise标准要这样,为什么node的api文档也特别强调了这一点,你需要好好考虑下。(虽然很多人不注意node其实有这个要求——这正好是node的一个缺点,因为callback无法确保这一点,只能靠文档强调和自觉,而promise从基础上确保了这一点。)
  2. max call stack size exceeded跟releasing zalgo没有直接关系(因为顶楼是一个提纲,所以也许表达的不够清楚)。本身这不是一个编程模式上的问题,而是实现上的不足。从某种角度说可归咎为js语言没有尾递归优化导致的问题。解决方法其实你看看bluebird或when的实现即可(具体说看when里的scheduler的实现和用途)。注意thenjs.series之类的api之类只能处理在调用时就已确定一系列处理过程的情况,所以不能解决问题本身。测试代码只是方便而写成了这样,实际上是不能用then.series解决的。
  3. 原生promise本身异步流程管理能力不够。这一点我是赞同的。但是标准只是把最核心的部分做掉。而整个异步管理中最最最重要的是什么?其实我顶楼已经总结了,就是语义本身的清晰(thunks在这点上恰好是反例)。此外的utils都是不难实现的,没有必要在标准库里包含,或者可等到大家有更多共识再标准化(目前只提供了promise.all和promise.race)。另外注意认为原生promise有debug问题是不对的,去看看chrome canary上的原生promise对调试的支持你就知道了,这是native promise的优势而不是劣势。
  4. thunk并不立即执行,这点我之前还没意识到——这正好也说明语义不清晰的问题,我不知道有多少人看了api接口后会意识到这点。另外这是否是大部分人编程中想要的行为?注意,我们不能笼统的说惰性求值是好的,因为在一个本身极少见惰性求值的语言中,以这样一种无法从调用api上分辨是否惰性的方式来引入惰性求值,并作为基础编程模式,是值得斟酌的。顺便注意co在使用thunk时并不会利用到惰性这一点,因为它yield一个thunk之后这个thunk就执行了。
  5. ES7 async/await不可能支持thunk。因为thunk不是标准。而thunk为什么不是标准?因为它不如promise。为什么不如?就是我之前讲的那些点。
  6. thunks能处理promise,也能处理thunk或其他东西,这谈不上是优点。我们根本没有必要组合一切异步方式,当promise成为标准,我们只需要promise这唯一的异步原语而已。比如co 4转向promise,它还支持thunk只是保持向后兼容而已。其他类似co的库就不支持thunk而只支持promise。
  7. nodejs的api不可能输出thunk。它唯一的可能是走向输出promise,尽管现在核心开发者还是对此持保守态度。这一判断的根据其实无需多言。
  8. then多次调用是一个非常明显的需求,比如 http://weibo.com/2041028560/BE0R1tgpt 。实际上注意一点promise的关键前面说过,是把异步流程的嵌套变成值的依赖。thunks这样只能调一次就完全失去了这个好处。当然按co的通常方式是可以不需要多次调用的,这大概是为什么当初co只要用thunk抽象而不用看上去更复杂的promise。但是如果你要一个可独立于co之类的基础的异步抽象,那肯定是promise比thunk更适合。
  9. thunks的return的问题还是api的缺陷。其实就算抛开promise不谈,整个thunks的设计在api上大有问题,可以看看我的homepage(http://johnhax.net/ )上列的老演讲 Learning API Design for JavaScript Libraries or Frameworks ,可以对照里面列的那些api设计原则。

hax avatar Jan 04 '15 14:01 hax

IMHO 有一点非常明确,给 thunks 的 callback 是只为副作用而生的,返回 Promise 的函数可以极大程度避免函数的副作用。

undoZen avatar Jan 15 '15 09:01 undoZen

@undoZen 异步过程本身才是副作用.

tiye avatar Jan 15 '15 10:01 tiye

@jiyinyiyong @undoZen 的意思是,扔给thunk的函数只是为了通过调用它而继续执行,而不是用来求值的。而promise则相反。因为异步被包到了promise内部,所以在外部看来promise是一个可反复使用的值,可以被传递给纯函数。这就是promise是monad的意思。

hax avatar Jan 19 '15 16:01 hax

吐槽 thunks 那点我懂. 不过 Haskell 里面对副作用的定义依然适用 Pomise 内部的行为才对.

tiye avatar Jan 19 '15 17:01 tiye

@jiyinyiyong 他的意思是返回promise的函数可以把副作用包在promise里面,所以本身可以是纯的。

hax avatar Jan 20 '15 03:01 hax

  • -! 我们是在争术语的使用问题么.. 我认为应该是"隔离副作用"而不是"避免副作用".

tiye avatar Jan 20 '15 05:01 tiye

呵呵,在外部看来promise是一个可反复使用的值 是吗?不是,一个函数要用一个 promise 的值,必须要把函数丢给promise执行才行,也就是说是需要异步值得业务逻辑永远是在promise.then()内部执行,而不是你所说的函数调用promise,在promise的外部执行。

既然如此,假设三个业务fn1,fn2,fn3都需要同一个异步值的反复使用场景, promise 形式就是这样:

var promise = getAsyncValue();
promise.then(fn1).catch(errHandler);
promise.then(fn2).catch(errHandler);
promise.then(fn3).catch(errHandler);

thunks 形式就是这样:

var thunk = getAsyncValue();
thunk(function(err, value) {
  if (err) return errHandler(err);
  fn1(value);
  fn2(value);
  fn3(value);
});

两个是等价的,promise 的可反复使用的值根本没有优势。

zensh avatar Jan 20 '15 09:01 zensh

@jiyinyiyong 理解意思就好。

@zensh 你完全没有理解“反复使用”的意思。promise代表了一个最终完成的值,所以它很容易被传递和分别使用。你看你写的thunks里fn1、fn2、fn3必须被捏到一个函数里,而前面promise的版本fn*可以是在别的接受promise的函数里附加到这个promise上的。

来一个稍有结构的例子:

var numbers = [1, 2, 3]
for (var i = 0; i < SIZE; i++) numbers.push(inputNumber())

showNumbers(numbers)
showAvg(numbers)

function inputNumber() {
  // return a Promise<number>
}

var log = console.log.bind(console)

function showNumbers(numbers) {
  numbers.forEach(function (p) {
    p.then(log)
  })
}

function showAvg(numbers) {
  Promise.all(numbers).then(avg).then(log)
}

function avg(list) {
  return list.reduce(function (a, b) { return a + b}) / list.length
}

你用thunks写一版对比下。

hax avatar Jan 20 '15 12:01 hax

我关注是实际需求,这个特性要解决一个什么样的需求。我也算写了两年 JS 代码,从来没碰到这类需求。

“反复使用” 对 thunks 来说,只是一个小小的修改而已,如果确实存在这种需求, 我可出一个新版,通过 thunks(options) 得到可“反复使用” thunk 的 Thunk 生成器。

就我目前的理解,“反复使用”没有用,只增加代码复杂性(跨函数),复杂系统中还可能造成隐晦的bug。

zensh avatar Jan 20 '15 14:01 zensh

对于你上面这段代码,showNumbers 如果仅仅是为了调试 log 一下看看结果及谁先谁后,那么 thunks 直接开启 debug 就好了,无需编个showNumbers函数。

如果 showNumbers 要对数据进行处理,那么问题来了,它会不会对showAvg产生影响。不管会还是不会,起码要考虑这个问题了,不考虑肯定会出问题,考虑的话也未必躲得过。

zensh avatar Jan 20 '15 14:01 zensh

争吵太多了,我在实际使用 Promise 过程中还真遇到内存问题。不知道算 co 的 bug 还是 Promise 的 bug。详情可查看 https://github.com/cnpm/mirrors/pull/41

  • 如果是 co 的问题,那么为何 thunk 版的 co 没问题?
  • 如果是 Promise 的问题,那么是我的使用co方式不对,还是 co 使用 Promise 的方式不对?

fengmk2 avatar Jan 20 '15 17:01 fengmk2

haskell 中只有 thunk 的概念,没有 promise 概念。

promise 是对异步任务的结果的“保证”,它象征的是结果,所以是及早求值和可反复使用 thunk 是对异步任务本身的“保证”,它象征的是异步任务本身,需要执行这个thunk来取得结果,所以它是惰性求值且不可反复使用的。

zensh avatar Jan 21 '15 04:01 zensh

答@zensh

在我的例子里,showNumbers和showAvg都是确定的需求。也就是一个(不管是不是异步的)结果可能被多个地方用到,这不是很正常的吗?在debug里开显然不是符合要求的方式。debug设施和本身的语义不可混为一谈。

showNumbers对数据处理是不是对其他产生影响?显然是不影响的,因为你拿到的是最终的值,不可能影响之前的resolve/reject步骤。至于说,如果最终值是一个引用,如果多个地方持有相同引用,可以通过引用改变属性,这是对象引用的语义问题,跟promise毫无关系,跟异步毫无关系。即使在完全同步的编程里你也有一样的问题,所以编程里我们要对参数做保护性拷贝,或者有struct等自定义值类型的语言(js将来也会有)会传递值类型而不是对象引用,或者虽然是对象但是用Immutable模式。总之,你不要老是把完全不同层面的问题混到一起。

hax avatar Jan 22 '15 06:01 hax

罗列几个链接:

  • https://github.com/tc39/proposal-async-iteration
  • https://github.com/tc39/ecmascript-asyncawait
  • https://github.com/tc39/proposal-cancelable-promises
  • https://github.com/tc39/proposal-promise-finally
  • https://github.com/whatwg/streams

虽然不知道什么时候可以用上,但社区一直寻找最佳“姿势”(实践)。

fundon avatar Oct 16 '16 08:10 fundon

官方Promise对始终异步的坚持使得AsyncFunction具有传染性,thenable的定义十分危险。 我的实现:https://github.com/Clarence-pan/shortcut-promise/issues/1

dou4cc avatar Mar 06 '18 03:03 dou4cc