Promise 杂谈

2019-01-27 18:50:17 +08:00
 shawncheung

本篇文章是笔者近期对 Promise 的几点思考的总结。

https://github.com/zhangxiang958/zhangxiang958.github.io/issues/42

4629 次点击
所在节点    Node.js
15 条回复
azh7138m
2019-01-27 23:55:35 +08:00
> 有利于消除副作用

不清楚你是怎么理解副作用的,不知能不能给个例子。
callback 和 thenable 的设计,都是构造一个函数传进去,为啥会有不一样。

为啥一个 callback 会关心代码是同步调用还是异步调用。
想不明白有什么场景下会有这种问题。
Sparetire
2019-01-28 03:04:36 +08:00
我觉得楼主的附录里还应该包含一个 You don't know javascript 的 Promise 章节。。
shawncheung
2019-01-28 09:47:30 +08:00
@azh7138m 我在文章中有注明副作用的意思:函数副作用指调用函数时除了返回函数值还会修改函数外的变量 https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8
(在文章中第一个例子就说明了这个问题,也就是通过一个外部变量来控制调用次数,而这个变量是其他代码逻辑无需关心的)
我在文章中说明了 callback 的一些在编码中的危害,而这部分在 you don't know 中也有提及,很多模型设计都是基于 callback 的,而不同模型的特点就在于它们用于解决什么问题,thenable 或者说 Promise 是用于解决回调中调用时机,调用次数的种种问题的。

文章中的意思并不是使用 callback 需要关心函数内部是同步或者是异步的,而是希望通过 Promise 这个机制来尽可能写出易于掌控的代码。而对于 callback 的调用时机不确定是同步还是异步或者是都有可能的情况下,会引发 release zalgo 问题,而这个问题是 API 设计哲学,也就是设计 API 要么完全同步,要么完全异步,详细可以看 https://blog.izs.me/2013/08/designing-apis-for-asynchrony 或者 https://oren.github.io/blog/zalgo.html

不知道以上能不能解决你的疑问
shawncheung
2019-01-28 09:47:46 +08:00
@Sparetire 已添加,感谢提醒
azh7138m
2019-01-28 11:03:59 +08:00
关于副作用,这里我不明白的是,thenable 和传统 callback 的设计,你都要创建一个函数,通常来说,这两种写法,你创建函数的地方都一样,这就意味着,他们的词法作用域是一样的,这种时候,这两种写法,可以修改的 外部 变量是一样的,那这里的。有助于消除副作用 指的是什么。


关于一个 api 有的时候是异步调用 callback 有的时候是同步这个,我不明白,一个 callback 在什么时候需要关心这个。

不知道我的问题描述清楚了没有,我想要的是这两种问题对应的举例。
Sparetire
2019-01-28 14:38:41 +08:00
@azh7138m 关于 callback 是同步异步调用的, 因为楼主看了 You don't know JavaScript 总结的(非贬义), 这部分也基本上和原书的意思差不多
大意是很多人看着 callback 的 API, 就想当然认为这个 callback 会是异步执行, 于是自己的代码依赖于这样的执行顺序, 导致可能结果不符合预期. 但其实我们都知道, 一个 callback 是同步调用还是异步调用从 API 的签名是看不出来的, 即我们不能信任 /依赖 API 对 callback 的执行顺序, 这个执行顺序是不可靠的, 也是书中提到信任问题的一部分.
书中认为这一问题的原因是各个库之间对于 callback 的调用时机缺乏统一的规范, 而 Promise 作为一个规范提供了信任背书, 明确赋予了 callback 异步调用的含义, 所以解决了之前的信任问题, 让人可以看到 API 的签名返回 Promise 就可以确定这是异步 API.
大概是这么个意思...但是可能因为楼主是总结精简了一些, 导致理解产生了一些偏差...
hoyixi
2019-01-28 15:53:05 +08:00
个人觉得,Promise, async/await 这些个玩意,其实就是个语法糖,是 JS 标准制定者为了让程序员写代码的时候,少费心少出错,异步回调写起来、看上去和从上到下的代码执行顺序达到某种“一致”罢了。

这玩意根本就不是啥高深技术,就是 callback 的语法糖,结果各种写博客的、写教程的,搞来搞去,写来写去,分析来分析去,抄来抄去,把简单的玩意搞得复杂无比。

如果是纯 JS 新手,看了这些中文教程,别说看明白了,估计想死。
libook
2019-01-28 16:02:19 +08:00
@azh7138m 不是 callback 和 Promise 的本质上的区别,因为 callback 其实也是可以逐级传递错误信息的,只不过用 callback 可以有多重实现方案,而 Promise 只提供了唯一方案,相比来说 Promise 更标准更可靠,标准化的好处就是任何第三方包都遵循标准任何人都可以直接使用,不会遇到错误传递方案不同导致的不兼容。

楼主举得例子不恰,外部放置临时变量来记录异步操作是否成功的方法只是 callback 传递状态的众多办法中的一种,而且可能是最差且几乎没人用的方法,这个方法有很多弊端,比如作用域混乱导致的调试成本太高,以及可读性差等,callback 在调用链上做状态传递是能规避这个方案带来的很多问题的,比较起来,Promise 是用统一标准方案来实现的状态传递,兼容性有保障。
azh7138m
2019-01-28 16:32:57 +08:00
@Sparetire 我能懂他看书了,就像我上面写的,什么场景下,一个函数(一个回调 thenable 的或是传统的)需要关心自己是同步被调用还是异步被调用,我这里不明白。


@hoyixi 我也觉得就是个糖,咋还就不一样了


@libook callback 你也可以写成 fp 的,用到的东西都通过参数传入。这里说的,thenable 更可靠,这个可靠性是怎么体现的呢?
作用域混乱的问题,和我上面写的一样,你都是要定义函数的,这个时候,(两种写法 改写前 改写后)通常都是一个词法作用域,还能一个混乱一个不混乱的吗?
libook
2019-01-28 19:05:13 +08:00
@azh7138m 可靠性的体现是:

//Promise 的错误捕捉机制
(new Promise((res, rej) => {
haha;//这里因为找不到"haha"是什么,所以会报一个错
}
)).catch((error) => {
console.log('捕捉到错误了');
return [];
}).then((result)=>{
console.log(`得到的数组的长度为${result.length}`);
});

//一般值传递回调函数的错误捕捉机制
function cb1(cb) {
let result;
try {
result = Math.random();//这里假设做了某种操作,我用 Math.random()来举例
} catch (error) {
cb(error);
}
haha;//这里因为找不到"haha"是什么,所以会报一个错
cb(null, result);
}

function cb2(error, result) {
if (error) {
console.error(error);
} else {
console.log(result);
}
}

cb1(cb2);

Promise 内部有任何问题,包括语法问题,都可以通过后边挂载的 catch 回调捕捉到;但 callback 不一样,得自己在自己觉得可能会有问题的地方加 try/catch,如果 try 没有覆盖到出错误的点,就可能会造成整个程序崩溃。相比来说,因为 Promise 的错误捕捉机制用起来更加简洁、覆盖面更广,所以反映在规模性的项目上可能可靠性更好。你也可以抬杠说在回调函数里每次都全局 try,但相比 Promise 来说要多写一点代码吧,而且看起来也不如 Promise 那么言简意赅。

作用域混乱我是特指的是楼主博客里的一个用法:

let urls = {};

function rpc (url, callback) {
if (urls[url]) {
return callback(urls[url]);
}
request(url, (err, res) => {
urls[url] = res;
callback(err, res);
});
}

urls['domain.com']=3;

oo();//这里可能是手误修改了 urls

rpc('domain.com',console.log);

function oo() {
urls['domain.com'] = 2;
}


这里 urls 暴露在一个相对宽广的的作用域下,如果在调用 rpc 之前还有其他的代码执行,则有可能会有意无意地修改 urls 的值,有意的还好,无意的就会产生逻辑 BUG 了。

以上都是针对一些实际情况举的例子,不是为了说明 Promise 一定就比 callback 好用,而是说明 Promise 能解决一些以往使用 callback 的痛点,实际上 Promise、Generator、Async/Await 都是语法糖,都是可以用 callback 实现的(要不然 babel 就不存在了),应用语法糖在特定情况下用可以事半功倍,但如果有的地方你觉得用 callback 最合适,也没必要非要赶着上 Promise。
azh7138m
2019-01-28 21:04:21 +08:00
azh7138m
2019-01-28 21:06:08 +08:00
@libook 而且 babel 不处理 Promise 的
libook
2019-01-29 00:16:22 +08:00
@azh7138m

我看不大懂你写的。。。
你的第一个代码,api 函数执行如果是异步过程的话,你的 try 永远捕捉不到任东西。
第二个的问题不是在于 rpc 是不是用 Promise 实现,而是 urls 暴漏在那么广的作用域下会有风险(让然代码逻辑处理好也是可以没问题的),把 urls 封装在 Promise 里,通过每一级的.then 向下传播,中间不可能被外界作用域的程序篡改:

```
let urls = {};

let theP = new Promise((res, rej) => {
let urls = {};
res(urls);
}).then(urls => {
urls["domain 点 com"] = 3;
return urls;
});
urls["domain 点 com"] = 2;
theP.then(urls => {
console.log(urls["domain 点 com"]);
});
```

这里边有两个 urls,但是 Promise 能保证调用链中传递的 urls 不受外部作用域的 urls 的操作影响。

上面说的都不是绝对的,得看实际情况。

bable 可以根据需求转换语法,最著名的是把 ES6+转换成纯 ES5,最早 core.js 部分就包含 Promise 的 polyfill 了,只不过现在很多人不需要那么苛刻要求转成 ES5。
去看一下 @babel/polyfill 文档吧。
azh7138m
2019-01-29 02:31:50 +08:00
@libook 这里说 babel 只当它是 transcompiler。
try 捕获不到东西 emmmmm,可能是我学的不是 js

#9 里面我也说了
callback 你也可以写成 fp 的,用到的东西都通过参数传入

这些都不是 thenable 的优点(指楼主文中提到的部分)。
希望举例子的时候能用点心。
libook
2019-01-29 09:47:05 +08:00
@azh7138m 你开心就好

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/531057

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX