本篇文章是笔者近期对 Promise 的几点思考的总结。
https://github.com/zhangxiang958/zhangxiang958.github.io/issues/42
1
azh7138m 2019-01-27 23:55:35 +08:00 via Android
> 有利于消除副作用
不清楚你是怎么理解副作用的,不知能不能给个例子。 callback 和 thenable 的设计,都是构造一个函数传进去,为啥会有不一样。 为啥一个 callback 会关心代码是同步调用还是异步调用。 想不明白有什么场景下会有这种问题。 |
2
Sparetire 2019-01-28 03:04:36 +08:00 via Android
我觉得楼主的附录里还应该包含一个 You don't know javascript 的 Promise 章节。。
|
3
shawncheung OP @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 不知道以上能不能解决你的疑问 |
4
shawncheung OP @Sparetire 已添加,感谢提醒
|
5
azh7138m 2019-01-28 11:03:59 +08:00 via Android
关于副作用,这里我不明白的是,thenable 和传统 callback 的设计,你都要创建一个函数,通常来说,这两种写法,你创建函数的地方都一样,这就意味着,他们的词法作用域是一样的,这种时候,这两种写法,可以修改的 外部 变量是一样的,那这里的。有助于消除副作用 指的是什么。
关于一个 api 有的时候是异步调用 callback 有的时候是同步这个,我不明白,一个 callback 在什么时候需要关心这个。 不知道我的问题描述清楚了没有,我想要的是这两种问题对应的举例。 |
6
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. 大概是这么个意思...但是可能因为楼主是总结精简了一些, 导致理解产生了一些偏差... |
7
hoyixi 2019-01-28 15:53:05 +08:00
个人觉得,Promise, async/await 这些个玩意,其实就是个语法糖,是 JS 标准制定者为了让程序员写代码的时候,少费心少出错,异步回调写起来、看上去和从上到下的代码执行顺序达到某种“一致”罢了。
这玩意根本就不是啥高深技术,就是 callback 的语法糖,结果各种写博客的、写教程的,搞来搞去,写来写去,分析来分析去,抄来抄去,把简单的玩意搞得复杂无比。 如果是纯 JS 新手,看了这些中文教程,别说看明白了,估计想死。 |
8
libook 2019-01-28 16:02:19 +08:00
@azh7138m 不是 callback 和 Promise 的本质上的区别,因为 callback 其实也是可以逐级传递错误信息的,只不过用 callback 可以有多重实现方案,而 Promise 只提供了唯一方案,相比来说 Promise 更标准更可靠,标准化的好处就是任何第三方包都遵循标准任何人都可以直接使用,不会遇到错误传递方案不同导致的不兼容。
楼主举得例子不恰,外部放置临时变量来记录异步操作是否成功的方法只是 callback 传递状态的众多办法中的一种,而且可能是最差且几乎没人用的方法,这个方法有很多弊端,比如作用域混乱导致的调试成本太高,以及可读性差等,callback 在调用链上做状态传递是能规避这个方案带来的很多问题的,比较起来,Promise 是用统一标准方案来实现的状态传递,兼容性有保障。 |
9
azh7138m 2019-01-28 16:32:57 +08:00 via Android
|
10
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。 |
13
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 文档吧。 |