JS Promise迷思- 比较return Promise.resolve(val) 和return val

最近碰到这样一个问题,JavaScript可以通过Promise.resolve()来转化一个值或者thenable对象成为一个Promise。

Promise可以通过then串联起来(chain),then的函数中,可以直接return val,也可以return Promise.resolve(val),那么这两种方式有什么区别么?

我听到的回答是没什么不同,但是我理解不深,所以打算自己研究一下。构造以下代码例子,猜猜运行结果是什么呢?

Promise.resolve(1)
    .then(v => Promise.resolve(v + 1))
    .then(v => Promise.resolve(v + 3))
    .then(v => console.log(`First: ${v}`));
Promise.resolve(1)
    .then(v => v + 1)
    .then(v => Promise.resolve(v + 5))
    .then(v => console.log(`Second: ${v}`));
Promise.resolve(1)
    .then(v => v + 1)
    .then(v => v + 7)
    .then(v => console.log(`Third: ${v}`));

根据我对并发asynchronous的理解,如果这两种形式一样的话,结果有可能是顺序的First,Second,Third。也有可能是乱序的,First Second Third顺序不一定,在浏览器运行有可能因为某种原因,一直出现同样的结果,所以我们分别试试Chrome和Safari的操作台。但是结果很一致,哪怕在本地node直接跑nodejs也是一样。

结论是什么呢?return Promise.resolve(val)和return val这两种方式在结果上是一样的,对于不关注运行顺序的代码来说效果也是一样的。但对于开发人员来说,需要知道这两种方式的运行时间点是不同的,这个不同在某些复杂情况下需要考虑到。简单的说,如果不是故意要延迟结果的产生返回,return val这种方式既简单又快捷,用它就好了。

BTW。

参考这篇SO回答https://stackoverflow.com/questions/58217603/what-is-the-difference-between-returned-promise 以及这一篇https://dev.to/deepal/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-2f57 可以知道Promise.resolve是一种微任务。

我大概能理解开发人员对于这些名词的怨念,微任务(microtask)又是什么?参考MDN这篇文章https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide 大概就是如下图所示。一个task完成以后,开始查询微任务队列microtask queue然后运行,接着运行下一个任务或者开始render。

这些内容有些高阶了,但对于JavaScript框架作者来说是必须的,想写框架的,可以参考这一篇https://blog.risingstack.com/writing-a-javascript-framework-execution-timing-beyond-settimeout/

(又一个)一行代码导致的血案-“is-promise”问题的粗浅分析

(Updated)开发者自己做了一个事后分析,比我讲的清晰多了,当然主要原因还是JS module依赖设计的复杂度https://medium.com/javascript-in-plain-english/is-promise-post-mortem-cab807f18dcc 在这一点上我很看好deno这个新工具。

——————————-

看到微信群里有人转关于“is-promise”更新导致的问题,公众号文章说的非常夸张,但又语焉不详,看了毫无头绪。于是看了些英文文章,做了点技术分析。

首先声明,因为水平不够理解不深,这篇文章并没有解释清楚问题的原因,只是说了一下我看到的现象,确保将来写类似代码的时候,心里有点谱。

“is-promise“有问题的版本更新是2.2.0,看了更新diff没发现什么明显问题。更新的目的主要是增加ES module风格的默认import。

首先得熟悉一下ES Module的import,还有commonjs的require。

先说一下ES6标准化的module,在浏览器中可以通过<script type=”module”>导入模块脚本,代码是这样的:”import xxx from ‘module-name'”。如果用CommonJS呢,就是“const package = require(‘module-name’)”。

在Nodejs v12之前使用的是commonjs,在v12的时候引入了ES Module的支持。在node中可以通过条件导入同时支持两种方式(麻烦!)https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_conditional_exports

在nodejs的官方文档中https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_dual_commonjs_es_module_packages 特意解释了双模式package的问题,其中特意提到了“Dual Package Hazard”(双Package风险),说实话这部分有些复杂,我也没有太理解。在后面提到了“Differences Between ES Modules and CommonJS“,其中第一条就是”Mandatory file extensions“,但是文档说的还是不清不楚的。

我们看一下is-promise的bug fix能理解更深一些。https://github.com/then/is-promise/commit/0b69f52ee73c9f6ad4480f02c9bccb14d2038656 可以看到主要就是对文件路径指定,变成使用”./xxx.js”方式。为什么原来有问题,现在就能fix,说实话我还是没看懂。我在本地做了一个极为简单的node js脚本文件,引入了is-promise 2.2.0版本,尝试了import和require两种方式,没发现有问题。我也尝试从bug report中找点线索,但是大多数都是说xxx有问题了,没有细节。我甚至到create-react-app中查找is-promise,根本就没有,简直是磕了。

先这样吧,等我理解更多了再更新此文。

另外在HN上关于这件事的讨论https://news.ycombinator.com/item?id=22979245 也是非常热烈,不过没人分析原因,要么吐槽,要么建议用npm指定版本方式(版本号不用^和~)。

async/await仅仅是promise的语法糖么?

简单的说结论:可以这么说。但我不能确定它是百分百等同,因为标准没有这么说。

根据这篇文章https://mathiasbynens.be/notes/async-stack-traces Mathisas提到的,以及MDN上面的这一篇https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await 当我们使用await的时候,JS Engine会block当前运行代码,然后启动新的Promise(task)运行,等待它的结束或者异常抛出。

这个部分我可能还要看一下emcascript的spec,确定真的理解了。但是很明显这与C#是截然不同的,差别就在于JS Engine是单线程的,而C#中会自动启动一个新的Task来做事情,当前的主Task和新启动Task是并行运行的。

在async文档中有slow async和fast async的例子,便于更深入的理解https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

那到底是用async/await还是promise?我的建议是,能用async/await的尽量用,也尽量不要和promise混用(我见过这么干的)。更细节上的判断可以参考MDN https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach

另外需要注意的一点是,如果await多个promise,那么await和Promise.all在时序上和错误处理上是有所不同的。知道了这一点,trouble shooting的时候可以更仔细的研究细节。在这里Google Chrome团队就写了一篇很实用的文章,里面提到了如何对应转换promise代码成为async/await代码 https://developers.google.com/web/fundamentals/primers/async-functions

先理解到这层面吧,如果有更深入了解再更新。

重新学习JavaScript(2)

继续看mdn的文档,今天看regexp部分。我想到一个小功能,把字符串中每个首字母大写。我看到的代码使用了split加上reduce,我呢,对于reduce总有些疑虑,因为它不直观,需要想一想才能把流程在脑子里理清楚。这样的功能用正则表达式正好,虽然正则也不容易理解,但是这貌似就是正则能打的领域,不用都可惜了。

看了一会文档,拼凑出一个代码来,在chrome devtool跑了一下是期望的结果。

function replacer(match, p1, p2, p3, offset, string) {
    console.log(arguments);
    const x = match.charAt(0).toLowerCase();
    return x + match.substring(1);
}
let re = /(\w+)/g;
let str = 'john smith haha';
let newstr = str.replace(re, replacer);
console.log(newstr); 

其实我并不是从string的replace函数看起的,而是看到了一个奇怪的用法。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@replace 我在怎么写replacer一开始没搞懂,因为没有例子。甚至跑到这里看测试用例https://github.com/tc39/test262/tree/master/test/built-ins/RegExp/prototype/Symbol.replace 后来看ecmascript的文档才发现提到了string.replace,这才把知识点串联起来。

var re = /-/g; 
var replacer = function() {
    console.log(arguments);
    return '.';
};
var newstr = re[Symbol.replace]('2016-01-01', replacer);

今天少写一点,就这些吧。

重新学习JavaScript(1)

学习JavaScript,就从MDN开始,其中包含了大量的值得深入学习和理解的内容。在这篇博客我介绍一些最近看到的。

1,class expression

首先就是类表达式https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/class 说实话这是我最近才知道的用法。它的语法如下,非常类似正常的类定义语句(class declaration statement),但是可以赋值给一个变量。如果使用typeof查一下就知道,MyClass类型还是function

const MyClass = class [className] [extends otherClassName] {
    // class body
};

类表达式可以重复定义,这个和类定义方式不同。另外className可以省略,如果命名了,那这个名字也只在class body有效,这个和function expression是一样的 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function

2, this

this在JavaScript面试中几乎是必考的点,https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this 这里值得注意的几点,一个是call和apply怎么记忆呢?最简单的办法就是apply第二个参数接受的是数组类型,也就是array,a对a就能容易记住了。

下面这段代码,其中有个知识点就是如果第一个参数不是object类型,就会调用内部的ToObject函数,比如7就会变成Number,而字符串字面值‘foo’就会转成String。

function bar() {
  console.log(Object.prototype.toString.call(this));
}
bar.call(7);     // [object Number]
bar.call('foo'); // [object String]

而bind需要注意的是只会绑定一次

function f() {
  return this.a;
}
var g = f.bind({a: 'azerty'});
console.log(g()); // azerty
var h = g.bind({a: 'yoo'}); // bind only works once!
console.log(h()); // azerty

3,Destructuring assignment解构赋值

这个词有点难理解,多看看里面的示例会好很多,大致上分成两类数组解构和对象解构 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

下面这个用法有点费解,最后得到的两个变量不是a和b,而是aa和bb。

const {a: aa = 10, b: bb = 5} = {a: 3};
console.log(aa); // 3
console.log(bb); // 5

继续加深理解

const user = {
  id: 42,
  displayName: 'jdoe',
  fullName: {
    firstName: 'John',
    lastName: 'Doe'
  }
};
function userId({id}) {
  return id;
}
function whois({displayName, fullName: {firstName: name}}) {
  return `${displayName} is ${name}`;
}
console.log(userId(user)); // 42
console.log(whois(user));  // "jdoe is John"

注意与下面的例子的区别,主要在参数的声明格式上。一个用的等号,一个用的冒号。

function drawChart({size = 'big', coords = {x: 0, y: 0}, radius = 25} = {}) {
  console.log(size, coords, radius);
}
drawChart({
  coords: {x: 18, y: 30},
  radius: 30
});

这个例子更复杂些,属性是计算得来

let key = 'z';
let {[key]: foo} = {z: 'bar'};
console.log(foo); // "bar"

还可以用rest 属性… 顺序也没有关系,只要能和属性名对上就行

let {c, a, ...rest} = {a: 10, b: 20, c: 30, d: 40}
a; // 10
c; // 30
rest; // { b: 20, d: 40 }
const foo = { 'fizz-buzz': true };
const { 'fizz-buzz': fizzBuzz } = foo;
console.log(fizzBuzz); // "true"

数组解构和对象解构组合起来

const props = [
  { id: 1, name: 'Fizz'},
  { id: 2, name: 'Buzz'},
  { id: 3, name: 'FizzBuzz'}
];
const [,, { name }] = props;
console.log(name); // "FizzBuzz"